Skip to content

Instantly share code, notes, and snippets.

@hh
Last active December 13, 2015 17:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hh/4949041 to your computer and use it in GitHub Desktop.
Save hh/4949041 to your computer and use it in GitHub Desktop.
Encrypting databags dynamically based on search.
{
"id":"afilename",
"contents":"M69z...ENCAFILENAMECONTENTSXchyhg==\n"
}
{
"id":"afilename_keys",
"NodeX":"Gt/7ENCDATABAGKEY\n",
"NodeY":"eC4.ENCDATABAGKEY=\n"
}

Encrypting dynamically to a set of nodes in chef is possible using data bags.

I created a plugin that creates a symmetric key, and two data bag item .json files based on name of the file you feed to it. ie where filebasename is filebasebasename.ext

One file it creates is filebasename_keys.json, which has encrypts the symmetric key to each node returned from the search using the nodes 'client api' public key.

The other is filebasename.json, which is an EncryptedDataBag encrypted using the symmetric shared key.

The symmetric key used for encryption of the file contents is never stored on disk and is only stored encrypted to the nodes returned from the search.

With knife_encrypted_plugin.rb in your .chef/plugins/knife directory and two nodes tagged with 'encrypt_to_me'

# knife encrypt 'tags:encrypt_to_me' afilename.secretfileextension

This would result in afilename.json and afilename_keys.json.

You would then upload them to a databag on the chef server for searching by nodes:

# knife data bag create databag_name
# knife data bag from file databag_name ./afilename.json
# knife data bag from file databag_name ./afilename_keys.json

To access the unencrypted contents (only from the nodes returned from search and that have the secret encrypted to their public key in afilename_keys), put the following in a recipe:

# Load the _keys data bag item, and extract the datbag key as encrypted the node
public_encrypted_secret = Base64.decode64(
  Chef::DataBagItem.load('databag_name','afilename_keys')[node.name])

# use the private client_key file to create a decryptor
pkey = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())

# the private client_key is then used to decrypt data encrypted with the public_key
encrypted_data_bag_secret = pkey.private_decrypt public_encrypted_secret

# afilename works like any other encrypted data bag
# we just distributed the encrypted_data_bag_secret in a new way
afilename_contents = Chef::EncryptedDataBagItem.load(
  'databag_name','afilename',encrypted_data_bag_secret)['contents']
require 'chef/knife'
module KnifePlugins
class Encrypt < Chef::Knife
deps do
require 'chef/search/query'
require 'chef/shef/ext'
end
banner "knife encrypt SEARCH FILE"
def run
Shef::Extensions.extend_context_object(self)
node_search = name_args[0]
file_to_encrypt = name_args[1]
# base our data bag item names on the name of the file before the first '.'
dbi_name = File.basename file_to_encrypt.split('.').first
# grab the contents of the file
contents = open(file_to_encrypt).read
# keyfob is created by using the provided node search
keyfob = Hash.new
public_keys = search(:node,node_search).map(&:name).map do |client|
# to retrieve the nodes client certificate
cert_der = api.get("clients/#{client}")['certificate']
# and using at a map to each clients public_key
cert = OpenSSL::X509::Certificate.new cert_der
keyfob[client]=OpenSSL::PKey::RSA.new cert.public_key
end
if public_keys.length == 0
puts "A node search for #{node_search} returned no results"
exit 1
end
if File.exists?("#{dbi_name}_keys.json") || File.exists?("#{dbi_name}.json")
puts "Output files #{dbi_name}_keys.json and #{dbi_name}.json exist!"
puts "Delete them and try again"
exit 1
end
# we create a shared key to create an encrypted data bag with the contents of the file
data_bag_shared_key = OpenSSL::PKey::RSA.new(245).to_pem.lines.to_a[1..-2].join
# we create a *_keys normal data bag item
enc_db_key_dbi = Mash.new({id: dbi_name + "_keys"})
# and we make an entry for each node encrypting the data_bag_shared_key to it
keyfob.each do |node,pkey|
enc_db_key_dbi[node] = Base64.encode64(pkey.public_encrypt(data_bag_shared_key))
end
# we write out the *_keys.json for placement in your git repository
# which is nice, because you can now review what nodes you are encrypting to
File.open("#{dbi_name}_keys.json",'w').write(enc_db_key_dbi.to_json)
# The encrypted data bag item itself, with only a contents key/value works like
# any other, however we don't distribute the key via normal means
dbi=Chef::DataBagItem.from_hash(Mash.new({id: dbi_name, contents: contents}))
dbi_json = Chef::EncryptedDataBagItem.encrypt_data_bag_item(dbi, data_bag_shared_key).to_json
ui.output "#{dbi_json}"
end
end
end
@seth
Copy link

seth commented Feb 14, 2013

Instead of searching for all nodes to extract node names, you can just list the nodes. Far more efficient:

api.get("nodes").keys

Take care that on OHC api.get("clients/CLIENT") will return a hash, but not so in OSC where you will get an inflated object.

Since you are using the data_bag_shared_key as the key to a symmetric cipher, I think you can just generate a secure random key. So you might find useful:

require 'securerandom'
SecureRandom.base64(128)
SecureRandom.hex(128)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment