chef-vault 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|