Skip to content

Instantly share code, notes, and snippets.

@djcode
Last active January 22, 2021 20:26
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 djcode/437140921106334f943860517b820dcb to your computer and use it in GitHub Desktop.
Save djcode/437140921106334f943860517b820dcb to your computer and use it in GitHub Desktop.
A simple vault backup script

vault_backup.rb

Introduction

A ruby script written in an afternoon to backup/export key/values from HashiCorp's Vault. It uses the Vault ruby client libraries, so make sure they are installed beforehand with 'gem install vault'.

Be careful with the data exported using this tool. Vault is designed to be secure, and this tool does bypass some (all) of that security. Use at your own risk and be aware of the consequences.

This script can export the data in an encrypted fashion, but should by no means be considered secure.

Features

  • Export to a customizable YAML file
  • Import to a different vault from an exported YAML file
  • Password-protect values (not keys) with AES 256 encryption

Documentation

Before you can use vault_backup.rb, you must ensure the vault is unsealed and you've set your vault address and token environment variables. For example:

export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=94356e20-0ca0-1163-8396-f0d4f3f56bc7

Backup mode

vault_backup.rb backup --file=FILE [--password=PASSWORD] [--root=secret/]

Mandatory arguments:

-f FILE, --file=FILE the file location to backup/export to.

Optional arguments:

-r PATH, --root=PATH the root Vault location to backup/export. This must end with a forward slash (/). Defaults to 'secret/'.

-p PASSWORD, --password=PASSWORD the password to encrypt the exported data with. This only encrypts secrets, not locations, so exported YAML files can still be manipulated before being imported.

Restore mode

vault_backup.rb restore --file=FILE [--password=PASSWORD] [--root=secret/]

Mandatory arguments:

-f FILE, --file=FILE the file location to import from.

Optional arguments:

-p PASSWORD, --password=PASSWORD the password to decrypt the imported data with. Only required if importing from and encrypted yaml file.

Still to do

  • Better error handling
  • Make classes instead of global variables
  • Validate vault responses
#!/usr/bin/env ruby
unless Gem::Specification.find_all_by_name('vault').any?
puts 'Install the vault gem first "gem install vault"'
exit 1
end
require 'optparse'
require 'vault'
require 'yaml'
$vault_data = []
$encryption = false
options = {}
OptionParser.new do |opts|
opts.banner = 'Usage: vault_backup.rb <backup|restore> <-f file> [-r <rootpath>] [-p <password>]'
opts.on('-r ROOT', '--root=ROOT', 'Root path in vault to backup from (Default: secret/)') do |v|
options[:root] = v || 'secret/'
end
opts.on('-p PASSWORD', '--password=PASSWORD', 'Password to encrypt/decrypt values') do |v|
options[:password] = v || nil
end
opts.on('-f FILE', '--file=FILE', 'File to backup to or restore from') do |v|
options[:file] = v
end
end.parse!
if %w('backup restore').include?(ARGV[0].downcase)
puts 'Missing command: [backup|restore]'
exit 1
else
command = 'backup'
command = 'restore' if ARGV[0].casecmp('restore').zero?
end
unless options[:file]
puts 'Must specify a file with -f FILE or --file=FILE'
exit 1
end
options[:root] = 'secret/' unless options[:root]
if options[:password]
require 'digest'
require 'openssl'
$encryption = true
$iv = "\x00\xD3\xE1\xE0\xB1\x00\x9D\xAC\xEC\x8A\xFA\x1A\xDA\xB1\x01\xEC"
$key = Digest::SHA256.digest options[:password]
end
def encrypt_value(data)
cipher = OpenSSL::Cipher::AES256.new :CBC
cipher.encrypt
cipher.iv = $iv
cipher.key = $key
cipher.update(YAML.dump(data)) + cipher.final
end
def decrypt_value(data)
decipher = OpenSSL::Cipher::AES256.new :CBC
decipher.decrypt
decipher.iv = $iv
decipher.key = $key
begin
YAML.load(decipher.update(data) + decipher.final)
rescue OpenSSL::Cipher::CipherError
puts 'The password is incorrect.'
exit 1
end
end
def backup(path)
if path.end_with?('/')
backup_list(path).each do |newpath|
backup(path + newpath)
end
else
backup_read(path)
end
end
def backup_read(path)
Vault.with_retries(Vault::HTTPError) do
data = if $encryption
puts "Encrypting: #{path}"
encrypt_value(Vault.logical.read(path).data)
else
puts " Saving: #{path}"
Vault.logical.read(path).data
end
$vault_data << {'path' => path, 'data' => data}
end
end
def backup_list(path)
Vault.with_retries(Vault::HTTPError) do
Vault.logical.list(path)
end
end
def restore(file)
contents = YAML.load(File.read(file))
if contents[0]['data'].is_a?(Hash) == $encryption
puts 'Password specified on unencrypted file'
puts 'or encrypted file with no password specified'
exit 1
end
if $encryption
contents.each do |row|
$vault_data << { 'path' => row['path'], 'data' => decrypt_value(row['data']) }
end
else
$vault_data = contents
end
$vault_data.each do |row|
puts " Restoring: #{row['path']}"
Vault.with_retries(Vault::HTTPError) do
Vault.logical.write(row['path'],row['data'])
end
end
end
if command == 'backup'
backup(options[:root])
open(options[:file], 'w') do |f|
f.puts $vault_data.to_yaml
end
end
if command == 'restore'
restore(options[:file])
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment