chef-vault 1.0.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.
- data/.gitignore +1 -0
- data/README.rdoc +100 -0
- data/chef-vault.gemspec +17 -0
- data/lib/chef-vault.rb +43 -0
- data/lib/chef-vault/certificate.rb +27 -0
- data/lib/chef-vault/user.rb +27 -0
- data/lib/chef-vault/version.rb +4 -0
- data/lib/chef/knife/DecryptCert.rb +43 -0
- data/lib/chef/knife/DecryptPassword.rb +42 -0
- data/lib/chef/knife/EncryptCert.rb +165 -0
- data/lib/chef/knife/EncryptPassword.rb +161 -0
- metadata +72 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/README.rdoc
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
= Chef-Vault
|
3
|
+
|
4
|
+
= DESCRIPTION:
|
5
|
+
|
6
|
+
Gem that allows you to encrypt passwords & certificates using the public key of
|
7
|
+
a list of chef nodes. This allows only those chef nodes to decrypt the
|
8
|
+
password or certificate.
|
9
|
+
|
10
|
+
= INSTALLATION:
|
11
|
+
|
12
|
+
Be sure you are running the latest version Chef. Versions earlier than 0.10.0
|
13
|
+
don't support plugins:
|
14
|
+
|
15
|
+
gem install chef
|
16
|
+
|
17
|
+
This plugin is distributed as a Ruby Gem. To install it, run:
|
18
|
+
|
19
|
+
gem install chef-vault
|
20
|
+
|
21
|
+
Depending on your system's configuration, you may need to run this command with
|
22
|
+
root privileges.
|
23
|
+
|
24
|
+
= CONFIGURATION:
|
25
|
+
|
26
|
+
= KNIFE COMMANDS:
|
27
|
+
|
28
|
+
This plugin provides the following Knife subcommands.
|
29
|
+
Specific command options can be found by invoking the subcommand with a
|
30
|
+
<tt>--help</tt> flag
|
31
|
+
|
32
|
+
== knife encrypt password
|
33
|
+
|
34
|
+
Use this knife command to encrypt the username and password that you want to
|
35
|
+
protect.
|
36
|
+
|
37
|
+
knife encrypt password --search SEARCH --username USERNAME --password PASSWORD --admins ADMINS
|
38
|
+
|
39
|
+
== knife decrypt password
|
40
|
+
|
41
|
+
Use this knife command to dencrypt the password that is protected
|
42
|
+
|
43
|
+
knife decrypt password --username USERNAME
|
44
|
+
|
45
|
+
== knife encrypt cert
|
46
|
+
|
47
|
+
Use this knife command to encrypt the contents of a certificate that you want
|
48
|
+
to protect.
|
49
|
+
|
50
|
+
knife encrypt cert --search SEARCH --cert CERT --password PASSWORD --name NAME --admins ADMINS
|
51
|
+
|
52
|
+
== knife decrypt cert
|
53
|
+
|
54
|
+
Use this knife command to dencrypt the certificate that is protected
|
55
|
+
|
56
|
+
knife decrypt cert --name NAME
|
57
|
+
|
58
|
+
= USAGE IN RECIPES
|
59
|
+
|
60
|
+
To use this gem in a recipe to decrypt data you must first install the gem
|
61
|
+
via a chef_gem resource. Once the gem is installed require the gem and then
|
62
|
+
you can create a new instance of ChefVault.
|
63
|
+
|
64
|
+
== Example Code (password)
|
65
|
+
|
66
|
+
chef_gem "chef-vault"
|
67
|
+
|
68
|
+
require 'chef-vault'
|
69
|
+
|
70
|
+
vault = ChefVault.new("passwords")
|
71
|
+
user = vault.user("Administrator")
|
72
|
+
password = user.decrypt_password
|
73
|
+
|
74
|
+
== Example Code (certificate)
|
75
|
+
|
76
|
+
chef_gem "chef-vault"
|
77
|
+
|
78
|
+
require 'chef-vault'
|
79
|
+
|
80
|
+
vault = ChefVault.new("certs")
|
81
|
+
cert = vault.certificate("domain.com")
|
82
|
+
contents = cert.decrypt_contents
|
83
|
+
|
84
|
+
= LICENSE:
|
85
|
+
|
86
|
+
Author:: Kevin Moser (<kevin.moser@nordstrom.com>)
|
87
|
+
Copyright:: Copyright (c) 2013 Nordstrom, Inc.
|
88
|
+
License:: Apache License, Version 2.0
|
89
|
+
|
90
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
91
|
+
you may not use this file except in compliance with the License.
|
92
|
+
You may obtain a copy of the License at
|
93
|
+
|
94
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
95
|
+
|
96
|
+
Unless required by applicable law or agreed to in writing, software
|
97
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
98
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
99
|
+
See the License for the specific language governing permissions and
|
100
|
+
limitations under the License.
|
data/chef-vault.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "chef-vault/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "chef-vault"
|
7
|
+
s.version = ChefVault::VERSION
|
8
|
+
s.has_rdoc = true
|
9
|
+
s.authors = ["Kevin Moser"]
|
10
|
+
s.email = ["kevin.moser@nordstrom.com"]
|
11
|
+
s.summary = "Data encryption support for chef using data bags"
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.add_dependency "chef", ">= 0.10.10"
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
end
|
data/lib/chef-vault.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Kevin Moser (<kevin.moser@nordstrom.com>)
|
3
|
+
#
|
4
|
+
# Copyright:: 2013, Nordstrom, Inc.
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'chef'
|
20
|
+
require 'chef-vault/user'
|
21
|
+
require 'chef-vault/certificate'
|
22
|
+
require 'chef-vault/version'
|
23
|
+
|
24
|
+
class ChefVault
|
25
|
+
|
26
|
+
attr_accessor :data_bag
|
27
|
+
|
28
|
+
def initialize(data_bag)
|
29
|
+
@data_bag = data_bag
|
30
|
+
end
|
31
|
+
|
32
|
+
def version
|
33
|
+
VERSION
|
34
|
+
end
|
35
|
+
|
36
|
+
def user(username)
|
37
|
+
ChefVault::User.new(@data_bag, username)
|
38
|
+
end
|
39
|
+
|
40
|
+
def certificate(name)
|
41
|
+
ChefVault::Certificate.new(@data_bag, name)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ChefVault
|
2
|
+
class Certificate
|
3
|
+
attr_accessor :name
|
4
|
+
|
5
|
+
def initialize(data_bag, name)
|
6
|
+
@name = name
|
7
|
+
@data_bag = data_bag
|
8
|
+
end
|
9
|
+
|
10
|
+
def decrypt_contents
|
11
|
+
# use the private client_key file to create a decryptor
|
12
|
+
private_key = open(Chef::Config[:client_key]).read
|
13
|
+
private_key = OpenSSL::PKey::RSA.new(private_key)
|
14
|
+
keys = Chef::DataBagItem.load(@data_bag, "#{name}_keys")
|
15
|
+
|
16
|
+
unless keys[Chef::Config[:node_name]]
|
17
|
+
throw "#{name} is not encrypted for you! Rebuild the certificate data bag"
|
18
|
+
end
|
19
|
+
|
20
|
+
node_key = Base64.decode64(keys[Chef::Config[:node_name]])
|
21
|
+
shared_secret = private_key.private_decrypt(node_key)
|
22
|
+
certificate = Chef::EncryptedDataBagItem.load(@data_bag, @name, shared_secret)
|
23
|
+
|
24
|
+
certificate["contents"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ChefVault
|
2
|
+
class User
|
3
|
+
attr_accessor :username
|
4
|
+
|
5
|
+
def initialize(data_bag, username)
|
6
|
+
@username = username
|
7
|
+
@data_bag = data_bag
|
8
|
+
end
|
9
|
+
|
10
|
+
def decrypt_password
|
11
|
+
# use the private client_key file to create a decryptor
|
12
|
+
private_key = open(Chef::Config[:client_key]).read
|
13
|
+
private_key = OpenSSL::PKey::RSA.new(private_key)
|
14
|
+
keys = Chef::DataBagItem.load(@data_bag, "#{username}_keys")
|
15
|
+
|
16
|
+
unless keys[Chef::Config[:node_name]]
|
17
|
+
throw "Password for #{username} is not encrypted for you! Rebuild the password data bag"
|
18
|
+
end
|
19
|
+
|
20
|
+
node_key = Base64.decode64(keys[Chef::Config[:node_name]])
|
21
|
+
shared_secret = private_key.private_decrypt(node_key)
|
22
|
+
cred = Chef::EncryptedDataBagItem.load(@data_bag, @username, shared_secret)
|
23
|
+
|
24
|
+
cred["password"]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class DecryptCert < Chef::Knife
|
4
|
+
deps do
|
5
|
+
require 'chef/search/query'
|
6
|
+
require 'chef/shef/ext'
|
7
|
+
require 'json'
|
8
|
+
end
|
9
|
+
|
10
|
+
banner "knife decrypt cert --name NAME"
|
11
|
+
|
12
|
+
option :name,
|
13
|
+
:short => '-N NAME',
|
14
|
+
:long => '--name NAME',
|
15
|
+
:description => 'Certificate data bag name'
|
16
|
+
|
17
|
+
def run
|
18
|
+
unless config[:name]
|
19
|
+
puts("You must supply a certificate to decrypt")
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
Shef::Extensions.extend_context_object(self)
|
23
|
+
|
24
|
+
data_bag = "certs"
|
25
|
+
data_bag_path = "./data_bags/#{data_bag}"
|
26
|
+
|
27
|
+
name = config[:name].gsub(".", "_")
|
28
|
+
|
29
|
+
user_private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
|
30
|
+
key = JSON.parse(IO.read("#{data_bag_path}/#{name}_keys.json"))
|
31
|
+
unless key[Chef::Config[:node_name]]
|
32
|
+
puts("Can't find a key for #{Chef::Config[:node_name]}... You can't decrypt!")
|
33
|
+
exit 1
|
34
|
+
end
|
35
|
+
|
36
|
+
data_bag_shared_key = user_private_key.private_decrypt(Base64.decode64(key[Chef::Config[:node_name]]))
|
37
|
+
|
38
|
+
certificate = JSON.parse(open("#{data_bag_path}/#{name}.json").read())
|
39
|
+
certificate = Chef::EncryptedDataBagItem.new certificate, data_bag_shared_key
|
40
|
+
|
41
|
+
puts("certificate:\n#{certificate['contents']}")
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class DecryptPassword < Chef::Knife
|
4
|
+
deps do
|
5
|
+
require 'chef/search/query'
|
6
|
+
require 'chef/shef/ext'
|
7
|
+
require 'json'
|
8
|
+
end
|
9
|
+
|
10
|
+
banner "knife decrypt password --username USERNAME"
|
11
|
+
|
12
|
+
option :username,
|
13
|
+
:short => '-U USERNAME',
|
14
|
+
:long => '--username USERNAME',
|
15
|
+
:description => 'username of account to encrypt'
|
16
|
+
|
17
|
+
def run
|
18
|
+
unless config[:username]
|
19
|
+
puts("You must supply a username to decrypt")
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
Shef::Extensions.extend_context_object(self)
|
23
|
+
|
24
|
+
data_bag_path = "./data_bags/passwords"
|
25
|
+
|
26
|
+
username = config[:username]
|
27
|
+
|
28
|
+
user_private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
|
29
|
+
key = JSON.parse(IO.read("#{data_bag_path}/#{username}_keys.json"))
|
30
|
+
unless key[Chef::Config[:node_name]]
|
31
|
+
puts("Can't find a key for #{Chef::Config[:node_name]}... You can't decrypt!")
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
data_bag_shared_key = user_private_key.private_decrypt(Base64.decode64(key[Chef::Config[:node_name]]))
|
36
|
+
|
37
|
+
credential = JSON.parse(open("#{data_bag_path}/#{username}.json").read())
|
38
|
+
credential = Chef::EncryptedDataBagItem.new credential, data_bag_shared_key
|
39
|
+
|
40
|
+
puts("username: #{credential['username']}, password: #{credential['password']}")
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class EncryptCert < Chef::Knife
|
4
|
+
deps do
|
5
|
+
require 'chef/search/query'
|
6
|
+
require 'chef/shef/ext'
|
7
|
+
end
|
8
|
+
|
9
|
+
banner "knife encrypt cert --search SEARCH --cert CERT --password PASSWORD --name NAME --admins ADMINS"
|
10
|
+
|
11
|
+
option :search,
|
12
|
+
:short => '-S SEARCH',
|
13
|
+
:long => '--search SEARCH',
|
14
|
+
:description => 'node search for nodes to encrypt to'
|
15
|
+
|
16
|
+
option :cert,
|
17
|
+
:short => '-C CERT',
|
18
|
+
:long => '--cert CERT',
|
19
|
+
:description => 'cert with contents to encrypt'
|
20
|
+
|
21
|
+
option :admins,
|
22
|
+
:short => '-A ADMINS',
|
23
|
+
:long => '--admins ADMINS',
|
24
|
+
:description => 'administrators who can decrypt certificate'
|
25
|
+
|
26
|
+
option :password,
|
27
|
+
:short => '-P PASSWORD',
|
28
|
+
:long => '--password PASSWORD',
|
29
|
+
:description => 'optional pfx password'
|
30
|
+
|
31
|
+
option :name,
|
32
|
+
:short => '-N NAME',
|
33
|
+
:long => '--name NAME',
|
34
|
+
:description => 'optional data bag name'
|
35
|
+
|
36
|
+
def run
|
37
|
+
unless config[:search]
|
38
|
+
puts("You must supply either -S or --search")
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
unless config[:cert]
|
42
|
+
puts("You must supply either -C or --cert")
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
unless config[:admins]
|
46
|
+
puts("You must supply either -A or --admins")
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
Shef::Extensions.extend_context_object(self)
|
50
|
+
|
51
|
+
data_bag = "certs"
|
52
|
+
data_bag_path = "./data_bags/#{data_bag}"
|
53
|
+
|
54
|
+
node_search = config[:search]
|
55
|
+
admins = config[:admins]
|
56
|
+
file_to_encrypt = config[:cert]
|
57
|
+
contents = open(file_to_encrypt, "rb").read
|
58
|
+
name = config[:name] ? config[:name].gsub(".", "_") : File.basename(file_to_encrypt, ".*").gsub(".", "_")
|
59
|
+
|
60
|
+
current_dbi = Hash.new
|
61
|
+
current_dbi_keys = Hash.new
|
62
|
+
if File.exists?("#{data_bag_path}/#{name}_keys.json") && File.exists?("#{data_bag_path}/#{name}.json")
|
63
|
+
current_dbi_keys = JSON.parse(open("#{data_bag_path}/#{name}_keys.json").read())
|
64
|
+
current_dbi = JSON.parse(open("#{data_bag_path}/#{name}.json").read())
|
65
|
+
|
66
|
+
unless equal?(data_bag, name, "contents", contents)
|
67
|
+
puts("FATAL: Content in #{data_bag_path}/#{name}.json does not match content in file supplied!")
|
68
|
+
exit 1
|
69
|
+
end
|
70
|
+
else
|
71
|
+
puts("INFO: Existing data bag #{data_bag}/#{name} does not exist in #{data_bag_path}, continuing as fresh build...")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the public keys for all of the nodes to encrypt for. Skipping the nodes that are already in
|
75
|
+
# the data bag
|
76
|
+
keyfob = Hash.new
|
77
|
+
public_keys = search(:node, node_search).map(&:name).map do |client|
|
78
|
+
begin
|
79
|
+
if current_dbi_keys[client]
|
80
|
+
puts("INFO: Skipping #{client} as it is already in the data bag...")
|
81
|
+
else
|
82
|
+
puts("INFO: Adding #{client} to public_key array...")
|
83
|
+
cert_der = api.get("clients/#{client}")['certificate']
|
84
|
+
cert = OpenSSL::X509::Certificate.new cert_der
|
85
|
+
keyfob[client]=OpenSSL::PKey::RSA.new cert.public_key
|
86
|
+
end
|
87
|
+
rescue Exception => node_error
|
88
|
+
puts("WARNING: Caught exception: #{node_error.message} while processing #{client}, so skipping...")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get the public keys for the admin users, skipping users already in the data bag
|
93
|
+
public_keys << admins.split(",").map do |user|
|
94
|
+
begin
|
95
|
+
if current_dbi_keys[user]
|
96
|
+
puts("INFO: Skipping #{user} as it is already in the data bag")
|
97
|
+
else
|
98
|
+
puts("INFO: Adding #{user} to public_key array...")
|
99
|
+
public_key = api.get("users/#{user}")['public_key']
|
100
|
+
keyfob[user] = OpenSSL::PKey::RSA.new public_key
|
101
|
+
end
|
102
|
+
rescue Exception => user_error
|
103
|
+
puts("WARNING: Caught exception: #{user_error.message} while processing #{user}, so skipping...")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
if public_keys.length == 0
|
108
|
+
puts "A node search for #{node_search} returned no results"
|
109
|
+
exit 1
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get the current secret, is nil if current secret does not exist yet
|
113
|
+
current_secret = get_shared_secret(data_bag, name)
|
114
|
+
data_bag_shared_key = current_secret ? current_secret : OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
|
115
|
+
enc_db_key_dbi = current_dbi_keys.empty? ? Mash.new({id: "#{name}_keys"}) : current_dbi_keys
|
116
|
+
|
117
|
+
# Encrypt for every new node not already in the data bag
|
118
|
+
keyfob.each do |node,pkey|
|
119
|
+
puts("INFO: Encrypting for #{node}...")
|
120
|
+
enc_db_key_dbi[node] = Base64.encode64(pkey.public_encrypt(data_bag_shared_key))
|
121
|
+
end unless keyfob.empty?
|
122
|
+
|
123
|
+
# Delete existing keys data bag and rewrite the whole bag from memory
|
124
|
+
puts("INFO: Writing #{data_bag_path}/#{name}_keys.json...")
|
125
|
+
File.delete("#{data_bag_path}/#{name}_keys.json") if File.exists?("#{data_bag_path}/#{name}_keys.json")
|
126
|
+
File.open("#{data_bag_path}/#{name}_keys.json",'w').write(JSON.pretty_generate(enc_db_key_dbi))
|
127
|
+
|
128
|
+
# If the existing certificate bag does not exist, write it out with the correct certificate
|
129
|
+
# Otherwise leave the existing bag alone
|
130
|
+
if current_dbi.empty?
|
131
|
+
dbi_mash = Mash.new({id: name, contents: contents})
|
132
|
+
dbi_mash.merge!({password: config[:password]}) if config[:password]
|
133
|
+
dbi = Chef::DataBagItem.from_hash(dbi_mash)
|
134
|
+
edbi = Chef::EncryptedDataBagItem.encrypt_data_bag_item(dbi, data_bag_shared_key)
|
135
|
+
|
136
|
+
puts("INFO: Writing #{data_bag_path}/#{name}.json...")
|
137
|
+
open("#{data_bag_path}/#{name}.json",'w').write(JSON.pretty_generate(edbi))
|
138
|
+
end
|
139
|
+
|
140
|
+
puts("INFO: Successfully wrote #{data_bag_path}/#{name}.json & #{data_bag_path}/#{name}_keys.json!")
|
141
|
+
end
|
142
|
+
|
143
|
+
def equal?(db, dbi, key, value)
|
144
|
+
data_bag_path = "./data_bags/#{db}"
|
145
|
+
|
146
|
+
shared_secret = get_shared_secret(db, dbi)
|
147
|
+
dbi = JSON.parse(open("#{data_bag_path}/#{dbi}.json").read())
|
148
|
+
dbi = Chef::EncryptedDataBagItem.new dbi, shared_secret
|
149
|
+
|
150
|
+
dbi[key] == value
|
151
|
+
end
|
152
|
+
|
153
|
+
def get_shared_secret(db, dbi)
|
154
|
+
data_bag_path = "./data_bags/#{db}"
|
155
|
+
|
156
|
+
private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
|
157
|
+
key = File.exists?("#{data_bag_path}/#{dbi}_keys.json") ? JSON.parse(open("#{data_bag_path}/#{dbi}_keys.json").read()) : nil
|
158
|
+
|
159
|
+
begin
|
160
|
+
private_key.private_decrypt(Base64.decode64(key[Chef::Config[:node_name]]))
|
161
|
+
rescue
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
class EncryptPassword < Chef::Knife
|
4
|
+
deps do
|
5
|
+
require 'chef/search/query'
|
6
|
+
require 'chef/shef/ext'
|
7
|
+
end
|
8
|
+
|
9
|
+
banner "knife encrypt password --search SEARCH --username USERNAME --password PASSWORD --admins ADMINS"
|
10
|
+
|
11
|
+
option :search,
|
12
|
+
:short => '-S SEARCH',
|
13
|
+
:long => '--search SEARCH',
|
14
|
+
:description => 'node search for nodes to encrypt for'
|
15
|
+
|
16
|
+
option :username,
|
17
|
+
:short => '-U USERNAME',
|
18
|
+
:long => '--username USERNAME',
|
19
|
+
:description => 'username of account to encrypt'
|
20
|
+
|
21
|
+
option :password,
|
22
|
+
:short => '-P PASSWORD',
|
23
|
+
:long => '--password PASSWORD',
|
24
|
+
:description => 'password of account to encrypt'
|
25
|
+
|
26
|
+
option :admins,
|
27
|
+
:short => '-A ADMINS',
|
28
|
+
:long => '--admins ADMINS',
|
29
|
+
:description => 'administrators who can decrypt password'
|
30
|
+
|
31
|
+
def run
|
32
|
+
unless config[:search]
|
33
|
+
puts("You must supply either -S or --search")
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
unless config[:username]
|
37
|
+
puts("You must supply either -U or --username")
|
38
|
+
exit 1
|
39
|
+
end
|
40
|
+
unless config[:password]
|
41
|
+
puts("You must supply either -P or --password")
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
unless config[:admins]
|
45
|
+
puts("You must supply either -A or --admins")
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
Shef::Extensions.extend_context_object(self)
|
49
|
+
|
50
|
+
data_bag = "passwords"
|
51
|
+
data_bag_path = "./data_bags/#{data_bag}"
|
52
|
+
|
53
|
+
node_search = config[:search]
|
54
|
+
admins = config[:admins]
|
55
|
+
username = config[:username]
|
56
|
+
password = config[:password]
|
57
|
+
current_dbi = Hash.new
|
58
|
+
current_dbi_keys = Hash.new
|
59
|
+
if File.exists?("#{data_bag_path}/#{username}_keys.json") && File.exists?("#{data_bag_path}/#{username}.json")
|
60
|
+
current_dbi_keys = JSON.parse(open("#{data_bag_path}/#{username}_keys.json").read())
|
61
|
+
current_dbi = JSON.parse(open("#{data_bag_path}/#{username}.json").read())
|
62
|
+
|
63
|
+
unless equal?(data_bag, username, "password", password)
|
64
|
+
puts("FATAL: Password in #{data_bag_path}/#{username}.json does not match password supplied!")
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
else
|
68
|
+
puts("INFO: Existing data bag #{data_bag}/#{username} does not exist in #{data_bag_path}, continuing as fresh build...")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the public keys for all of the nodes to encrypt for. Skipping the nodes that are already in
|
72
|
+
# the data bag
|
73
|
+
keyfob = Hash.new
|
74
|
+
public_keys = search(:node, node_search).map(&:name).map do |client|
|
75
|
+
begin
|
76
|
+
if current_dbi_keys[client]
|
77
|
+
puts("INFO: Skipping #{client} as it is already in the data bag...")
|
78
|
+
else
|
79
|
+
puts("INFO: Adding #{client} to public_key array...")
|
80
|
+
cert_der = api.get("clients/#{client}")['certificate']
|
81
|
+
cert = OpenSSL::X509::Certificate.new cert_der
|
82
|
+
keyfob[client]=OpenSSL::PKey::RSA.new cert.public_key
|
83
|
+
end
|
84
|
+
rescue Exception => node_error
|
85
|
+
puts("WARNING: Caught exception: #{node_error.message} while processing #{client}, so skipping...")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get the public keys for the admin users, skipping users already in the data bag
|
90
|
+
public_keys << admins.split(",").map do |user|
|
91
|
+
begin
|
92
|
+
if current_dbi_keys[user]
|
93
|
+
puts("INFO: Skipping #{user} as it is already in the data bag")
|
94
|
+
else
|
95
|
+
puts("INFO: Adding #{user} to public_key array...")
|
96
|
+
public_key = api.get("users/#{user}")['public_key']
|
97
|
+
keyfob[user] = OpenSSL::PKey::RSA.new public_key
|
98
|
+
end
|
99
|
+
rescue Exception => user_error
|
100
|
+
puts("WARNING: Caught exception: #{user_error.message} while processing #{user}, so skipping...")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
if public_keys.length == 0
|
105
|
+
puts "A node search for #{node_search} returned no results"
|
106
|
+
exit 1
|
107
|
+
end
|
108
|
+
|
109
|
+
# Get the current secret, is nil if current secret does not exist yet
|
110
|
+
current_secret = get_shared_secret(data_bag, username)
|
111
|
+
data_bag_shared_key = current_secret ? current_secret : OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
|
112
|
+
enc_db_key_dbi = current_dbi_keys.empty? ? Mash.new({id: "#{username}_keys"}) : current_dbi_keys
|
113
|
+
|
114
|
+
# Encrypt for every new node not already in the data bag
|
115
|
+
keyfob.each do |node,pkey|
|
116
|
+
puts("INFO: Encrypting for #{node}...")
|
117
|
+
enc_db_key_dbi[node] = Base64.encode64(pkey.public_encrypt(data_bag_shared_key))
|
118
|
+
end unless keyfob.empty?
|
119
|
+
|
120
|
+
# Delete existing keys data bag and rewrite the whole bag from memory
|
121
|
+
puts("INFO: Writing #{data_bag_path}/#{username}_keys.json...")
|
122
|
+
File.delete("#{data_bag_path}/#{username}_keys.json") if File.exists?("#{data_bag_path}/#{username}_keys.json")
|
123
|
+
File.open("#{data_bag_path}/#{username}_keys.json",'w').write(JSON.pretty_generate(enc_db_key_dbi))
|
124
|
+
|
125
|
+
# If the existing password bag does not exist, write it out with the correct password
|
126
|
+
# Otherwise leave the existing bag alone
|
127
|
+
if current_dbi.empty?
|
128
|
+
dbi_mash = Mash.new({id: username, username: username, password: password})
|
129
|
+
dbi = Chef::DataBagItem.from_hash(dbi_mash)
|
130
|
+
edbi = Chef::EncryptedDataBagItem.encrypt_data_bag_item(dbi, data_bag_shared_key)
|
131
|
+
|
132
|
+
puts("INFO: Writing #{data_bag_path}/#{username}.json...")
|
133
|
+
open("#{data_bag_path}/#{username}.json",'w').write(JSON.pretty_generate(edbi))
|
134
|
+
end
|
135
|
+
|
136
|
+
puts("INFO: Successfully wrote #{data_bag_path}/#{username}.json & #{data_bag_path}/#{username}_keys.json!")
|
137
|
+
end
|
138
|
+
|
139
|
+
def equal?(db, dbi, key, value)
|
140
|
+
data_bag_path = "./data_bags/#{db}"
|
141
|
+
|
142
|
+
shared_secret = get_shared_secret(db, dbi)
|
143
|
+
dbi = JSON.parse(open("#{data_bag_path}/#{dbi}.json").read())
|
144
|
+
dbi = Chef::EncryptedDataBagItem.new dbi, shared_secret
|
145
|
+
|
146
|
+
dbi[key] == value
|
147
|
+
end
|
148
|
+
|
149
|
+
def get_shared_secret(db, dbi)
|
150
|
+
data_bag_path = "./data_bags/#{db}"
|
151
|
+
|
152
|
+
private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
|
153
|
+
key = File.exists?("#{data_bag_path}/#{dbi}_keys.json") ? JSON.parse(open("#{data_bag_path}/#{dbi}_keys.json").read()) : nil
|
154
|
+
|
155
|
+
begin
|
156
|
+
private_key.private_decrypt(Base64.decode64(key[Chef::Config[:node_name]]))
|
157
|
+
rescue
|
158
|
+
nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: chef-vault
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kevin Moser
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chef
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.10.10
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.10.10
|
30
|
+
description: Data encryption support for chef using data bags
|
31
|
+
email:
|
32
|
+
- kevin.moser@nordstrom.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- README.rdoc
|
39
|
+
- chef-vault.gemspec
|
40
|
+
- lib/chef-vault.rb
|
41
|
+
- lib/chef-vault/certificate.rb
|
42
|
+
- lib/chef-vault/user.rb
|
43
|
+
- lib/chef-vault/version.rb
|
44
|
+
- lib/chef/knife/DecryptCert.rb
|
45
|
+
- lib/chef/knife/DecryptPassword.rb
|
46
|
+
- lib/chef/knife/EncryptCert.rb
|
47
|
+
- lib/chef/knife/EncryptPassword.rb
|
48
|
+
homepage:
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.8.24
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Data encryption support for chef using data bags
|
72
|
+
test_files: []
|