In our current continuous delivery pipeline, we have to distribute a number of secure keys to various servers for access to different resources. It’s common to use encrypted data bags in Chef to store protected values such as passwords or, in my case, SSH keys. The typical process for doing this is:

1. Create the secure key (which is basically a string of characters)
2. Create a data bag item JSON file and copy the key text into it
3. Use knife command to upload and encrypt the data bag item (“knife data bag from file”)

This became slightly problematic for me because an RSA key file has line-breaks and other characters that Knife’s JSON parser doesn’t like. To make this easier, I wrote a simple Ruby Knife script to do combine steps 2 and 3. This is an interesting exercise for a couple reasons. First, it simplifies the process so I don’t have to create a semi-pointless JSON wrapper for the text of another file. More importantly, it shows how we can use the standard Chef API calls in a knife script, similar to the code we write in a recipe to access data bag items.

databag_encrypt_file.krb

#!/usr/bin/knife exec 
# Knife exec script to put the contents of a file into a data bag, then encrypt it.
#
########### USAGE ############
this_file = File.basename(__FILE__)
usage = <<-EOS

#{this_file}: Encrypts and stores the contents of a file into a data bag item. This 
is typically used to encrypt and store the contents of a PEM file.

usage: 
  knife exec #{this_file} {filename} {databag} {databag_item} {secret_key_file}

example:
  knife exec #{this_file} foo.pem foo_bag foo_item my_secret.pem

Use 'knife data bag show foo_bag foo_item --secret-file my_secret.pem' to verify.
EOS
############ USAGE ############

filename = ARGV[2]
data_bag_name = ARGV[3]
data_bag_item_name = ARGV[4]
encryption_key_file = ARGV[5]

abort usage if (encryption_key_file.nil? || (encryption_key_file == ""))

# See if the data bag exists yet
begin 
	data_bag = data_bag(data_bag_name)
	puts "Data bag #{data_bag_name} already exists."
rescue 
    puts "Creating new data bag #{data_bag_name}"
	bag = Chef::DataBag.new
	bag.name(data_bag_name)
	bag.create
end

puts "Storing contents of #{filename} in item #{data_bag_item_name}"

content = File.read(filename)

# Set up the un-encrypted contents of the data bag
bag_item = Chef::DataBagItem.new
bag_item.data_bag(data_bag_name)
bag_item[:comment] = "Data bag automatically generated from file #{filename} by databag_encrypt_file.krb"
bag_item[:filename] = File.basename(filename)
bag_item[:content] = content
bag_item[:id] = data_bag_item_name

puts "Encrypting with key #{encryption_key_file}"

# Now, encrypt the data bag contents into a new data bag
bag_hash = bag_item.to_hash
secret = Chef::EncryptedDataBagItem.load_secret(encryption_key_file)
enc_hash = Chef::EncryptedDataBagItem.encrypt_data_bag_item(bag_hash, secret)
ebag_item = Chef::DataBagItem.from_hash(enc_hash)
ebag_item.data_bag(data_bag_name)
ebag_item.save

puts "Success. Use command to verify contents:"
puts "  knife data bag show #{data_bag_name} #{data_bag_item_name} --secret-file #{encryption_key_file}"

# Need this, or knife exec attempts to execute your parameters as new scripts
exit 0

This allows me to use a simple command to upload my PEM file:

knife exec databag_encrypt_file.krb my_key.pem dev_keys key1 ~/keys/databag_secret.key

I can verify that the key was uploaded properly with the following command:

knife data bag show dev_keys key1 --secret-file ~/keys/databag_secret.key

The key file is now available for all recipes using standard Chef::DataBagItem API calls. Of course, it requires that the “databag_secret.key” file be available for both Knife and Chef-Client nodes. I call that the “root key distribution” problem and that’s another topic entirely.

3 thoughts to “Chef Knife script for encrypting a file into a data bag

  • Bob Brown

    Hi,

    Looks like this technique breaks down after chef-client/knife version > 12.6 and doesn’t work as you get a Validation error.

    Here’s what I’m seeing during the chef-client run:
    Option data_bag’s value {“encrypted_data”=>”cXBV+HI8YECORTP7jvhjqkE8oK9CI+5F0Vyvp0HDlArFu4wZbEPZAaPA8FIl\n+B5O\n”, “iv”=>”+NJXmfR+vhubtG1c+arPpg==\n”, “version”=>1, “cipher”=>”aes-256-cbc”} does not match regular expression /^[\-[:alnum:]_]+$/

    I see a similar error if I attempt to read a data bag or on creation of a new one.

    Have you updated this knife script to account for the change that occurred with version 12.7?

    Thanks,
    Bob

    Reply
    • Rich Mills

      Thanks for pointing that out, Bob. Yes, this was written a few years ago for an older version of Chef. Let me ask around and see if anyone internally has an updated version of the script.

      Reply
  • Dzmitry

    on 2023 I faced with “Chef::Exceptions::ValidationFailed: Property data_bag’s value”
    on 58 line “ebag_item = Chef::DataBagItem.from_hash(enc_hash)”
    I resolved it: https://www.appsloveworld.com/ruby/100/310/how-to-create-edit-encrypted-data-bag-item-from-a-chef-recipe

    secret = Chef::EncryptedDataBagItem.load_secret(Chef::Config[:encrypted_data_bag_secret])
    data = { “id” => “mysecret”, “secret” => “stuff” }
    encrypted_data_hash = Chef::EncryptedDataBagItem.encrypt_data_bag_item(data, secret)

    databag_item = Chef::DataBagItem.new
    databag_item.data_bag(“secrets”)
    databag_item.raw_data = encrypted_data_hash
    databag_item.save

    Reply

Leave a comment

Your email address will not be published. Required fields are marked *

X