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 +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
|