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 +4 -4
- data/README.md +50 -20
- data/exe/pupistry +27 -1
- data/lib/pupistry.rb +1 -0
- data/lib/pupistry/agent.rb +1 -0
- data/lib/pupistry/artifact.rb +44 -10
- data/lib/pupistry/config.rb +11 -0
- data/lib/pupistry/hieracrypt.rb +412 -0
- data/lib/pupistry/version.rb +1 -1
- data/resources/bootstrap/freebsd-10.erb +11 -2
- data/settings.example.yaml +15 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c94c39eb6f50371e99762aee5b7546f60f9897d
|
4
|
+
data.tar.gz: 3d23604c96dd8e1a1991e71347ef2481428bf00e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/exe/pupistry
CHANGED
@@ -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
|
|
data/lib/pupistry.rb
CHANGED
data/lib/pupistry/agent.rb
CHANGED
data/lib/pupistry/artifact.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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"
|
data/lib/pupistry/config.rb
CHANGED
@@ -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
|
data/lib/pupistry/version.rb
CHANGED
@@ -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
|
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
|
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
|
data/settings.example.yaml
CHANGED
@@ -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.
|
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:
|
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
|