pupistry 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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