pupistry 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 191ed356e73fb20bf75c7cf3b42a6d39dec77a9e
4
- data.tar.gz: 2379cc4af18613e37b14cf9569347c914b7f34e9
3
+ metadata.gz: 8c94c39eb6f50371e99762aee5b7546f60f9897d
4
+ data.tar.gz: 3d23604c96dd8e1a1991e71347ef2481428bf00e
5
5
  SHA512:
6
- metadata.gz: f941399858b331deb102914b56bbb854a52d97bd7c835dcf82253813c6581b00d50ec31caf72c7f69c2adefe983360f0fbb56a5e49ac4fe1ca40e6bd32192ecb
7
- data.tar.gz: 9e66e9dc688d9e8b2b2de57ea04cb48953841a369ebba025f2ad257f505d117e3635ee4703d4634c3017e406418367279d9cdbe84caaefcea49118744c1718ae
6
+ metadata.gz: 0095ac651397af41960be834d5ed202265920dc1315f3b974b057f5aa5a32a7b132dbb5fd323299f3cfcba6b9b128bc4131bb463e4028d3b4653e1ff19b0746e
7
+ data.tar.gz: 2552da2af903929b92f115ee24e017be6e2c39af8b27eab850c1f92159ef5165704ff88421fc52f46ef5e34fc88dbbe053864261ddf0f18205220d729bc167cc
data/README.md CHANGED
@@ -457,6 +457,56 @@ would be from you accidentally sharing your IAM credentials in the wrong place,
457
457
  or an exploited build server.
458
458
 
459
459
 
460
+ # Securing Hirea with HieraCrypt
461
+
462
+ In a standard Puppet master situation, the Puppet master parses the Hiera data
463
+ and then passes only the values that apply to a particular host to it. But with
464
+ masterless Puppet, all machines get a full copy of Hiera data, which could be a
465
+ major issue if one box gets expoited and the contents leaked. Generally it goes
466
+ against good practise and damanges the isolation ability of VMs if you give all
467
+ the VMs enough information to do some serious damage to themselves.
468
+
469
+ By default an out-of-the-box Pupistry installation suffers this limitation like
470
+ most master-less Puppet solutions. However, there is an optional feature built
471
+ into Pupistry called "HieraCrypt" which can be used to encrypt data and prevent
472
+ excessive exposure of information to nodes.
473
+
474
+ The solutions works, by generating a cert on each node you use with the
475
+ `pupistry hieracrypt --generate` parameter and saving the output into your
476
+ puppetcode repository at `hieracrypt/nodes/HOSTNAME`. This output includes a
477
+ x509 cert made against the host's SSH RSA host key and a JSON array of all
478
+ the facter facts on that host that correlate to values inside the hiera.yaml
479
+ file.
480
+
481
+ When you run Pupistry on your build workstation, it parses the hiera.yaml file
482
+ for each environment and generates a match of files per-node. It then encrypts
483
+ these files and creates an encrypted package for each node that only they can
484
+ decrypt.
485
+
486
+ For example, if your hiera.yaml file looks like:
487
+
488
+ :hierarchy:
489
+ - "environments/%{::environment}"
490
+ - "nodes/%{::hostname}"
491
+ - common
492
+
493
+ And your hieradata directory looks like:
494
+
495
+ hieradata/
496
+ hieradata/common.yaml
497
+ hieradata/environments
498
+ hieradata/nodes
499
+ hieradata/nodes/testhost.yaml
500
+
501
+ When Pupistry builds the artifact, it will include the `common.yaml` file for
502
+ all nodes, however the `testhost.yaml` file will only be included for the
503
+ server with that hostname.
504
+
505
+ All servers still get the encrypted data for all the other nodes as they're
506
+ shipped as part of the artifact, but nodes can only decrypt the data signed
507
+ against their key.
508
+
509
+
460
510
  # Caveats & Future Plans
461
511
 
462
512
  ## Use r10k
@@ -498,26 +548,6 @@ decide to do so, a pull request to better support CD systems out-of-the-box
498
548
  would be welcome.
499
549
 
500
550
 
501
- ## Hiera Security Still Sucks
502
-
503
- In a standard Puppet master situation, the Puppet master parses the Hiera data
504
- and then passes only the values that apply to a particular host to it. But with
505
- masterless Puppet, all machines get a full copy of Hiera data, which could be a
506
- major issue if one box gets expoited and the contents leaked. Generally it goes
507
- against good practise and damanges the isolation ability of VMs if you give all
508
- the VMs enough information to do some serious damage to themselves.
509
-
510
- Pupistry does not yet have any solution for it and it remains a fundamental
511
- limitation of the Puppet masterless approach. Longer term, we could potentially
512
- craft a solution that customises the artifacts per-machine to fix this security
513
- gap, but there's no proper solution currently.
514
-
515
- If you have an environment where you need to send lots of sensitive values to
516
- your servers, a traditional master-full Puppet environment may be a better
517
- solution for this reason. But if you can architect to avoid this or have no
518
- critical secrets in Hiera, Pupistry should be good for you.
519
-
520
-
521
551
  ## PuppetDB
522
552
 
523
553
  There's nothing stopping you from using PuppetDB other than Pupistry has no
@@ -77,6 +77,7 @@ class CLI < Thor
77
77
  artifact = Pupistry::Artifact.new
78
78
 
79
79
  artifact.fetch_r10k
80
+ artifact.hieracrypt_encrypt
80
81
  artifact.build_artifact
81
82
 
82
83
  puts '--'
@@ -146,7 +147,7 @@ class CLI < Thor
146
147
  # Pull requests welcome :-) xoxo
147
148
 
148
149
  Dir.chdir("#{$config['general']['app_cache']}/artifacts/") do
149
- unless system "diff -Nuar unpacked.#{artifact_upstream.checksum} unpacked.#{artifact_current.checksum}"
150
+ unless system "diff -Nuar --exclude hieracrypt unpacked.#{artifact_upstream.checksum} unpacked.#{artifact_current.checksum}"
150
151
  end
151
152
  end
152
153
 
@@ -252,6 +253,31 @@ class CLI < Thor
252
253
  end
253
254
  end
254
255
 
256
+ ## Hieracrypt Feature
257
+
258
+ desc 'hieracrypt', 'Manage the encryption of Hiera data to securely restrict access from nodes'
259
+ method_option :generate, type: :boolean, desc: 'Generate an export of public cert and facts for Hieracrypt usage.'
260
+ def hieracrypt
261
+ # Thor seems to force class options to be defined repeatedly? :-/
262
+ if options[:verbose]
263
+ $logger.level = Logger::DEBUG
264
+ else
265
+ $logger.level = Logger::INFO
266
+ end
267
+
268
+ if options[:config]
269
+ Pupistry::Config.load(options[:config])
270
+ else
271
+ Pupistry::Config.find_and_load
272
+ end
273
+
274
+ # TODO
275
+ if options[:generate]
276
+ Pupistry::HieraCrypt.generate_nodedata
277
+ else
278
+ puts "Run `pupistry hieracrypt --generate` on each node to get back a data file to be saved into puppetcode"
279
+ end
280
+ end
255
281
 
256
282
  ## Other Commands
257
283
 
@@ -4,6 +4,7 @@ require 'pupistry/artifact'
4
4
  require 'pupistry/bootstrap'
5
5
  require 'pupistry/config'
6
6
  require 'pupistry/gpg'
7
+ require 'pupistry/hieracrypt'
7
8
  require 'pupistry/packer'
8
9
  require 'pupistry/storage_aws'
9
10
 
@@ -89,6 +89,7 @@ module Pupistry
89
89
 
90
90
  artifact.fetch_artifact
91
91
  artifact.unpack
92
+ artifact.hieracrypt_decrypt
92
93
 
93
94
  unless artifact.install
94
95
  $logger.fatal 'An unexpected error happened when installing the latest artifact, cancelling Puppet run'
@@ -6,7 +6,6 @@ require 'time'
6
6
  require 'digest'
7
7
  require 'fileutils'
8
8
  require 'base64'
9
- require 'whichr'
10
9
 
11
10
  module Pupistry
12
11
  # Pupistry::Artifact
@@ -172,6 +171,27 @@ module Pupistry
172
171
  end
173
172
  end
174
173
 
174
+ def hieracrypt_encrypt
175
+ # Stub function, since HieraCrypt has no association with the actual
176
+ # artifact file, but rather the post-r10k checked data, it could be
177
+ # invoked directly. However it's worth wrapping here incase we ever
178
+ # do change this behavior.
179
+
180
+ Pupistry::HieraCrypt.encrypt_hieradata
181
+
182
+ end
183
+
184
+ def hieracrypt_decrypt
185
+ # Decrypt any encrypted Hieradata inside the currently unpacked artifact
186
+ # before it gets copied to the installation location.
187
+
188
+ if defined? @checksum
189
+ Pupistry::HieraCrypt.decrypt_hieradata $config['general']['app_cache'] + "/artifacts/unpacked.#{@checksum}/puppetcode"
190
+ else
191
+ $logger.warn "Tried to request hieracrypt_decrypt on no artifact."
192
+ end
193
+
194
+ end
175
195
  def push_artifact
176
196
  # The push step involves 2 steps:
177
197
  # 1. GPG sign the artifact and write it into the manifest file
@@ -280,15 +300,26 @@ module Pupistry
280
300
  # Make sure there is a directory to write artifacts into
281
301
  FileUtils.mkdir_p('artifacts')
282
302
 
283
- # Try to use GNU tar if present to work around weird issues with some
284
- # versions of BSD tar when using the tar files with GNU tar subsequently.
285
- tar = RubyWhich.new.which('gtar').first || RubyWhich.new.which('gnutar').first || 'tar'
286
- $logger.debug "Using tar at #{tar}"
287
-
288
303
  # Build the tar file - we delibertly don't compress in a single step
289
304
  # so that we can grab the checksum, since checksum will always differ
290
305
  # post-compression.
291
- unless system "#{tar} -c --exclude '.git' -f artifacts/artifact.temp.tar puppetcode/*"
306
+
307
+ tar = Pupistry::Config.which_tar
308
+ $logger.debug "Using tar at #{tar}"
309
+
310
+ tar += " -c"
311
+ tar += " --exclude '.git'"
312
+ if Pupistry::HieraCrypt.is_enabled?
313
+ # We want to exclude unencrypted hieradata (duh security) and also the node files (which aren't needed)
314
+ tar += " --exclude 'hieradata'"
315
+ tar += " --exclude 'hieracrypt/nodes'"
316
+ else
317
+ # Hieracrypt is disable, exclude any old out of date encrypted files
318
+ tar += " --exclude 'hieracrypt/encrypted'"
319
+ end
320
+ tar += " -f artifacts/artifact.temp.tar puppetcode/*"
321
+
322
+ unless system tar
292
323
  $logger.error 'Unable to create tarball'
293
324
  fail 'An unexpected error occured when executing tar'
294
325
  end
@@ -306,6 +337,11 @@ module Pupistry
306
337
  $logger.error "This artifact version (#{@checksum}) has already been built, nothing todo."
307
338
  $logger.error "Did you remember to \"git push\" your module changes?"
308
339
 
340
+ # TODO: Unfortunatly Hieracrypt breaks this, since the encrypted Hieradata is different
341
+ # on every run, which results in the checksum always being different even if nothing in
342
+ # the repo itself has changed. We need a proper fix for this at some stage, for now it's
343
+ # covered in the readme notes for HieraCrypt as a flaw.
344
+
309
345
  # Cleanup temp file
310
346
  FileUtils.rm($config['general']['app_cache'] + '/artifacts/artifact.temp.tar')
311
347
  exit 0
@@ -386,9 +422,7 @@ module Pupistry
386
422
  # Unpack the archive file
387
423
  FileUtils.mkdir_p($config['general']['app_cache'] + "/artifacts/unpacked.#{@checksum}")
388
424
  Dir.chdir($config['general']['app_cache'] + "/artifacts/unpacked.#{@checksum}") do
389
- # Try to use GNU tar if present to work around weird issues with some
390
- # versions of BSD tar when using the tar files with GNU tar subsequently.
391
- tar = RubyWhich.new.which('gtar').first || RubyWhich.new.which('gnutar').first || 'tar'
425
+ tar = Pupistry::Config.which_tar
392
426
  $logger.debug "Using tar at #{tar}"
393
427
 
394
428
  if system "#{tar} -xf ../artifact.#{@checksum}.tar.gz"
@@ -4,6 +4,7 @@ require 'fileutils'
4
4
  require 'tempfile'
5
5
  require 'yaml'
6
6
  require 'safe_yaml'
7
+ require 'whichr'
7
8
 
8
9
  module Pupistry
9
10
  # Pupistry::Config
@@ -115,6 +116,16 @@ module Pupistry
115
116
 
116
117
  load(config)
117
118
  end
119
+
120
+ # Return which tar binary to use.
121
+ def self.which_tar
122
+ # Try to use GNU tar if present to work around weird issues with some
123
+ # versions of BSD tar when using the tar files with GNU tar subsequently.
124
+ tar = RubyWhich.new.which('gtar').first || RubyWhich.new.which('gnutar').first || 'tar'
125
+
126
+ return tar
127
+ end
128
+
118
129
  end
119
130
  end
120
131
  # vim:shiftwidth=2:tabstop=2:softtabstop=2:expandtab:smartindent
@@ -0,0 +1,412 @@
1
+ # rubocop:disable Style/Documentation, Style/GlobalVars
2
+ require 'rubygems'
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'safe_yaml'
6
+ require 'fileutils'
7
+ require 'base64'
8
+
9
+ module Pupistry
10
+ # Pupistry::HieraCrypt
11
+
12
+ class HieraCrypt
13
+
14
+ # As HieraCrypt is an optional extension, we should provide calling code
15
+ # an easy way to determine if we're enabled or not.
16
+ def self.is_enabled?
17
+ begin
18
+ if $config['build']['hieracrypt'] == true
19
+ $logger.debug 'Hieracrypt is enabled.'
20
+ return true
21
+ end
22
+ rescue => ex
23
+ # Nothing todo, fall back.
24
+ end
25
+
26
+ $logger.debug 'Hieracrypt is disabled.'
27
+ return false
28
+ end
29
+
30
+
31
+ # To encrypt the Hieradata against the certs we have, there's a few things
32
+ # that we need to do.
33
+ #
34
+ # 1. Firstly we need to iterate through all the available environments in
35
+ # the app_cache/puppetcode directory and for each one, load the Hiera
36
+ # rules.
37
+ #
38
+ # 2. Secondly (assuming HieraCrypt is even enabled) we must find all the
39
+ # node files that contain the cert & fact data.
40
+ #
41
+ # 3. Apply the rules to the host and determine which files should go into
42
+ # the encrypted hieradata file for that host and copy to a dir.
43
+ #
44
+ # 4. Generate the encrypted HieraCrypt file with the files in it, one per
45
+ # each node we have.
46
+ #
47
+ # 5. Purge the unencrypted hieradata and the working files.
48
+ #
49
+ # Run after fetch_r10k and before build_artifact
50
+ #
51
+ def self.encrypt_hieradata
52
+ unless is_enabled?
53
+ return false
54
+ end
55
+
56
+ $logger.info "Encrypting Hieradata (HieraCrypt Feature)..."
57
+
58
+
59
+ # Key paths to remember inside puppetcode / BRANCH:
60
+ #
61
+ # hieracrypt/nodes/ Where the various per-host files live.
62
+ # hieradata/hiera.yaml The Hiera rules
63
+ # hieradata/* Any/all Hiera data.
64
+ #
65
+ puppetcode = $config['general']['app_cache'] + '/puppetcode'
66
+
67
+
68
+ # Run through each environment.
69
+ for env in Dir.glob(puppetcode +'/*')
70
+ env = File.basename(env)
71
+
72
+ if Dir.exists?(puppetcode + '/' + env)
73
+ $logger.debug "Processing branch: #{env}"
74
+
75
+ Dir.chdir(puppetcode + '/' + env) do
76
+ # Directory env exists, check inside it for a hiera.yaml
77
+ if File.exists?('hiera.yaml')
78
+ $logger.debug 'Found hiera file '+ puppetcode + '/' + env + '/hiera.yaml'
79
+ else
80
+ $logger.warn "No hiera.yaml could be found for branch #{env}, no logic to encrypt on"
81
+ return false
82
+ end
83
+
84
+
85
+ # Iterate through each node in the environment
86
+ unless Dir.exists?('hieradata')
87
+ $logger.warn "No hieradata found for branch #{env}, so nothing to encrypt. Skipping."
88
+ break
89
+ end
90
+
91
+ if Dir.exists?('hieracrypt')
92
+ $logger.debug 'Found hieracrypt directory'
93
+ else
94
+ $logger.warn "No hieracrypt/ directory could be found for branch #{env}, no encryption can take place there."
95
+ break
96
+ end
97
+
98
+ unless Dir.exists?('hieracrypt/nodes')
99
+ $logger.warn "No hieracrypt/nodes directory could be found for branch #{env}, no encryption can take place there."
100
+ break
101
+ end
102
+
103
+ unless Dir.exists?('hieracrypt/encrypted')
104
+ # We place the encrypted data files in here.
105
+ Dir.mkdir('hieracrypt/encrypted')
106
+ end
107
+
108
+ nodes = Dir.glob('hieracrypt/nodes/*')
109
+
110
+ if nodes
111
+ for node in nodes
112
+ node = File.basename(node)
113
+
114
+ $logger.debug "Found node #{node} for environment #{env}, processing now..."
115
+
116
+ begin
117
+ # We need to load the JSON-based facts that are appended to the
118
+ # cert file. However the JSON parser loses it's shit since it
119
+ # doesn't like the header of the cert contents, so we need to
120
+ # seek past that ourselves.
121
+ json_raw = ""
122
+
123
+ IO.readlines("hieracrypt/nodes/#{node}").each do |line|
124
+ unless json_raw.empty?
125
+ # Subsequent Lines
126
+ json_raw += line
127
+ end
128
+
129
+ if /{/.match(line)
130
+ # We have found the first {, must be a valid JSON line
131
+ json_raw += line
132
+ end
133
+ end
134
+
135
+ # Extract the facts from the json
136
+ puppet_facts = JSON.load(json_raw)
137
+
138
+ rescue Exception => ex
139
+ $logger.fatal "Unable to parse the JSON data for host/node #{node}"
140
+ fail 'A fatal error occurred when processing HieraCrypt node data'
141
+ end
142
+
143
+
144
+ # It's common to use the 'environment' fact in Hiera, however
145
+ # it's going to have been exported as null, since it wouldn't
146
+ # have been set at time of generation. Hence, if it is there
147
+ # and it is null, we should set it to the current environment
148
+ # since we know exactly what it will be because we're inside
149
+ # the environment :-)
150
+
151
+ if defined? puppet_facts['environment']
152
+ if puppet_facts['environment'] == nil
153
+ puppet_facts['environment'] = env
154
+ end
155
+ end
156
+
157
+
158
+ # Apply the Hiera rules to the directory and get back a list of
159
+ # files that would be matched by Hiera. The way we do this, is
160
+ # by filling in each line in Hiera and essentially turning them
161
+ # into a glob-able (is this even a word?) pattern which allows
162
+ # us to determine what files we need to encrypt for this
163
+ # particular node.
164
+
165
+ # Iterate through the Hiera rules for values
166
+ hiera_rules = []
167
+ hiera = YAML.load_file('hiera.yaml', safe: true, raise_on_unknown_tag: true)
168
+
169
+ if defined? hiera[':hierarchy']
170
+ if hiera[':hierarchy'].is_a?(Array)
171
+ for line in hiera[':hierarchy']
172
+ # Match syntax of %{::some_kinda_fact}
173
+ line.scan(/%{::([[:word:]]*)}/) do |match|
174
+ # Replace fact variable with actual value
175
+ unless defined? puppet_facts[match[0]]
176
+ $logger.warn "hiera.yaml references fact #{match[0]} but this fact doesn't exist in #{node}'s hieracrypt/node/#{node} JSON."
177
+ $logger.warn "Possibly out of date data, re-run `pupistry hieracrypt --generate` on the node"
178
+ else
179
+ line = line.sub("%{::#{match[0]}}", puppet_facts[match[0]])
180
+ end
181
+ end
182
+
183
+ # Add processed line to the rules file
184
+ hiera_rules.push(line)
185
+ end
186
+ else
187
+ $logger.error "Use the array format of the hierachy entry in Hiera, string format not supported because why would you?"
188
+ end
189
+ end
190
+
191
+ # We have the rules from Hiera for this machine, let's run
192
+ # through them as globs and copy each match to a new location.
193
+ begin
194
+ FileUtils.rm_r "hieracrypt.#{node}"
195
+ rescue Errno::ENOENT
196
+ # Normal error if it doesn't exist yet.
197
+ end
198
+
199
+ FileUtils.mkdir "hieracrypt.#{node}"
200
+
201
+ $logger.debug "Copying relevant hiera data files for #{node}..."
202
+
203
+ hiera_rules.each do |rule|
204
+ for file in Dir.glob("hieradata/#{rule}.*")
205
+ $logger.debug " - #{file}"
206
+
207
+ file_rel = file.sub("hieradata/", "")
208
+ #FileUtils.mkdir_p "hieracrypt.#{node}/#{File.dirname(file_rel)}"
209
+ FileUtils.mkdir_p "hieracrypt.#{node}/#{File.dirname(file_rel)}"
210
+ FileUtils.cp file, "hieracrypt.#{node}/#{file_rel}"
211
+ end
212
+ end
213
+
214
+
215
+ # Generate the encrypted file
216
+ tar = Pupistry::Config.which_tar
217
+ $logger.debug "Using tar at #{tar}"
218
+
219
+ unless system "#{tar} -c -z -f hieracrypt.#{node}.tar.gz hieracrypt.#{node}"
220
+ $logger.error 'Unable to create tarball'
221
+ fail 'An unexpected error occured when executing tar'
222
+ end
223
+
224
+ openssl = "openssl smime -encrypt -binary -aes256 -in hieracrypt.#{node}.tar.gz -out hieracrypt/encrypted/#{node}.tar.gz.enc hieracrypt/nodes/#{node}"
225
+ $logger.debug "Executing: #{openssl}"
226
+
227
+ unless system openssl
228
+ $logger.error "Generation of encrypted file failed for node #{node}"
229
+ fail 'An unexpected error occured when executing openssl'
230
+ end
231
+
232
+ # Cleanup Unencrypted
233
+ FileUtils.rm_r "hieracrypt.#{node}.tar.gz"
234
+ FileUtils.rm_r "hieracrypt.#{node}"
235
+ end
236
+ else
237
+ $logger.warn "No nodes could be found for branch #{env}, no encryption can take place there."
238
+ break
239
+ end
240
+
241
+ # We don't do the purge of hieradata unencrypted directory here,
242
+ # instead we tell the artifact creation process to exclude it from
243
+ # the artifact generation if Hieracrypt is enabled.
244
+
245
+ end
246
+ end
247
+ end
248
+
249
+ end
250
+
251
+ # Find & decrypt the data for this server, if any. This should be run
252
+ # ALWAYS regardless of the Hieracrypt parameter, since we don't want people
253
+ # to have to worry about rolling it out to clients, we can figure it out
254
+ # based on what files do (or don't) exist.
255
+ #
256
+ # Runs after unpack, but before artifact install. We get the artifact class
257
+ # to pass through the location to operate inside of.
258
+ #
259
+ def self.decrypt_hieradata puppetcode
260
+ $logger.debug "Decrypting Hieracrypt..."
261
+
262
+ hostname = get_hostname # Facter hostname value
263
+ ssh_host_rsa_key = get_ssh_rsa_private_key # We generate the SSL cert using the SSH RSA Host key
264
+
265
+
266
+ # Run through each environment.
267
+ for env in Dir.glob(puppetcode +'/*')
268
+ env = File.basename(env)
269
+
270
+ if Dir.exists?(puppetcode + '/' + env)
271
+ $logger.debug "Processing branch: #{env}"
272
+
273
+ Dir.chdir(puppetcode + '/' + env) do
274
+ unless Dir.exists?("hieracrypt/encrypted")
275
+ $logger.debug "Environment #{env} is using unencrypted hieradata."
276
+ else
277
+ $logger.debug "Environment #{env} is using HieraCrypt, searching for host..."
278
+
279
+ if File.exists?("hieracrypt/encrypted/#{hostname}.tar.gz.enc")
280
+ $logger.info "Found encrypted Hieradata for #{hostname} in #{env} branch"
281
+
282
+ # Perform decryption of this host.
283
+ openssl = "openssl smime -decrypt -inkey #{ssh_host_rsa_key} < hieracrypt/encrypted/#{hostname}.tar.gz.enc | tar -xz -f -"
284
+
285
+ unless system openssl
286
+ $logger.error "A fault occured trying to decrypt the data for #{hostname}"
287
+ end
288
+
289
+ # Move unpacked host-specific Hieradata into final location
290
+ FileUtils.mv "hieracrypt.#{hostname}", "hieradata"
291
+ else
292
+ $logger.error "Unable to find a HieraCrypt package for #{hostname} in branch #{env}, this machine will be missing all Hieradata"
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ end
300
+
301
+
302
+ # Fetch the Puppet facts and the x509 cert from the server and export them
303
+ # in a combined version for easy cut'n'paste to the puppetcode repo.
304
+ def self.generate_nodedata
305
+ $logger.info "Generating an export package of cert and facts..."
306
+
307
+ # Setup the cache so we can park various files as we work.
308
+ cache_dir = $config['general']['app_cache'] +'/hieracrypt'
309
+
310
+ unless Dir.exists?(cache_dir)
311
+ Dir.mkdir(cache_dir)
312
+ end
313
+
314
+ # Generate the SSH public cert.
315
+ ssh_host_rsa_key = get_ssh_rsa_private_key # We generate the SSL cert using the SSH RSA Host key
316
+ cert_days = '36500' # Valid for 100 years
317
+ subject_string = '/C=XX/ST=Pupistry/L=Pupistry/O=Pupistry/OU=Pupistry/CN=Pupistry/emailAddress=pupistry@example.com'
318
+
319
+ unless File.exists?(ssh_host_rsa_key)
320
+ $logger.error "Unable to find ssh_host_rsa_key file at: #{ssh_host_rsa_key}, unable to proceed."
321
+ end
322
+
323
+ # TODO: Is there a native library we can use for invoking this and is anyone brave enough to face it? For now
324
+ # system might be easier.
325
+ openssl = 'openssl req -x509 -key '+ ssh_host_rsa_key +' -nodes -days '+ cert_days +' -newkey rsa:2048 -out '+ cache_dir +'/server.pem -subj '+ subject_string
326
+ $logger.debug "Executing: #{openssl}"
327
+
328
+ unless system openssl
329
+ $logger.error "An error occured attempting to execute openssl"
330
+ end
331
+
332
+ # Grab all the facter values
333
+ puppet_facts = facts_for_hiera($config['agent']['puppetcode'])
334
+
335
+ # TODO: Hit facter natively via Rubylibs?
336
+ unless system 'facter -p -j '+ puppet_facts.join(" ") +' >> '+ cache_dir +'/server.pem 2> /dev/null'
337
+ $logger.error "An error occur attempting to execute facter"
338
+ end
339
+
340
+ # Output the whole file for the user
341
+ hostname = get_hostname
342
+ puts "The following output should be saved into `hieracrypt/nodes/#{hostname}`:"
343
+ puts IO.read(cache_dir +'/server.pem')
344
+
345
+ end
346
+
347
+
348
+ # Iterate through the puppetcode environments for all hiera.yaml files
349
+ # and suck out all the facts that Hiera cares about. We do this since
350
+ # we want to selectively return only the facts we need, since it's
351
+ # pretty common to have facts exposing stuff that's potentially a bit
352
+ # private and unwanted in the puppetcode repo.
353
+ #
354
+ # Returns
355
+ # Array of Facts
356
+
357
+ def self.facts_for_hiera(path)
358
+ $logger.debug "Searching for facts specified in Hiera rules..."
359
+
360
+ puppet_facts = []
361
+
362
+ for env in Dir.entries(path)
363
+ if Dir.exists?(path + '/' + env)
364
+ # Directory env exists, check inside it for a hiera.yaml
365
+ if File.exists?(path + '/' + env + '/hiera.yaml')
366
+ $logger.debug 'Found hiera file '+ path + '/' + env + '/hiera.yaml, checking for facts'
367
+
368
+ # Iterate through the Hiera rules for values
369
+ hiera = YAML.load_file(path + '/' + env + '/hiera.yaml', safe: true, raise_on_unknown_tag: true)
370
+
371
+ if defined? hiera[':hierarchy']
372
+ if hiera[':hierarchy'].is_a?(Array)
373
+ for line in hiera[':hierarchy']
374
+ # Match syntax of %{::some_kinda_fact}
375
+ line.scan(/%{::([[:word:]]*)}/) { |match|
376
+ puppet_facts.push(match) unless puppet_facts.include?(match)
377
+ }
378
+ end
379
+ else
380
+ $logger.error "Use the array format of the hierachy entry in Hiera, string format not supported because why would you?"
381
+ end
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ if puppet_facts.count == 0
388
+ $logger.warn "Couldn't find any facts mentioned in Hiera, possibly missing or very empty/basic hiera.yaml file in puppetcode repo"
389
+ else
390
+ $logger.debug "Facts specified in Hiera are: "+ puppet_facts.join(", ")
391
+ end
392
+
393
+ return puppet_facts
394
+ end
395
+
396
+
397
+
398
+ def self.get_ssh_rsa_private_key
399
+ # Currently hard coded
400
+ return '/etc/ssh/ssh_host_rsa_key'
401
+ end
402
+
403
+ def self.get_hostname
404
+ # TODO: Ewwww
405
+ hostname = `facter hostname`
406
+ return hostname.chomp
407
+ end
408
+
409
+ end
410
+ end
411
+
412
+ # vim:shiftwidth=2:tabstop=2:softtabstop=2:expandtab:smartindent
@@ -1,3 +1,3 @@
1
1
  module Pupistry
2
- VERSION = '1.2.2'
2
+ VERSION = '1.3.0'
3
3
  end
@@ -6,7 +6,7 @@
6
6
  # Known Issues:
7
7
  # * AWS and Digital Ocean issues:
8
8
  # http://www.jethrocarr.com/2015/04/19/freebsd-in-the-cloud/
9
- # * Puppet and PkgNg issues:
9
+ # * We use Puppet 4 from Ports to avoid limitations in older Puppet 3 release:
10
10
  # https://www.jethrocarr.com/2015/04/22/puppet-3-and-4-on-freebsd/
11
11
  # * tcsh makes capturing all the output to syslog difficult, so we don't do it.
12
12
  # * We can't rely on Bash, since it's not available in FreeBSD by default.
@@ -16,7 +16,16 @@ setenv PATH /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
16
16
 
17
17
  env ASSUME_ALWAYS_YES=YES pkg bootstrap
18
18
  env ASSUME_ALWAYS_YES=YES pkg upgrade --yes
19
- env ASSUME_ALWAYS_YES=YES pkg install --yes ruby devel/ruby-gems puppet gnupg
19
+ env ASSUME_ALWAYS_YES=YES pkg install --yes ruby devel/ruby-gems gnupg
20
+
21
+ portsnap fetch
22
+ portsnap extract
23
+
24
+ cd /usr/ports/ports-mgmt/pkg
25
+ make reinstall BATCH=yes
26
+
27
+ cd /usr/ports/sysutils/puppet4
28
+ make install BATCH=yes
20
29
 
21
30
  /usr/local/bin/gem install pupistry
22
31
  mkdir -p /usr/local/etc/pupistry
@@ -83,3 +83,18 @@ build:
83
83
  proxy_uri:
84
84
 
85
85
 
86
+ # Enable the HieraCrypt feature
87
+ #
88
+ # Note - Once enabled, all your servers must have their definition added,
89
+ # otherwise they will not recieve any Hiera information as it will no longer
90
+ # be delivered in an unencrypted form.
91
+ #
92
+ # You will want to run `pupistry hieracrypt --generate` on each node to
93
+ # generate a file which needs to be saved into `hieracrypt/nodes/hostname`
94
+ # in your puppetcode repo (right alongside the `hieradata/` directory).
95
+ #
96
+ # If you later decide to disable hieracrypt, you should remove the entire
97
+ # `hieracrypt` directory to avoid confusion.
98
+ #
99
+ hieracrypt: false
100
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pupistry
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jethro Carr
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-08-15 00:00:00.000000000 Z
11
+ date: 2016-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -191,6 +191,7 @@ files:
191
191
  - lib/pupistry/bootstrap.rb
192
192
  - lib/pupistry/config.rb
193
193
  - lib/pupistry/gpg.rb
194
+ - lib/pupistry/hieracrypt.rb
194
195
  - lib/pupistry/packer.rb
195
196
  - lib/pupistry/storage_aws.rb
196
197
  - lib/pupistry/version.rb