sanctum 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/sanctum +3 -0
- data/bin/setup +6 -0
- data/lib/sanctum.rb +1 -0
- data/lib/sanctum/cli.rb +109 -0
- data/lib/sanctum/colorize_string.rb +44 -0
- data/lib/sanctum/command.rb +16 -0
- data/lib/sanctum/command/base.rb +30 -0
- data/lib/sanctum/command/check.rb +29 -0
- data/lib/sanctum/command/config.rb +34 -0
- data/lib/sanctum/command/create.rb +62 -0
- data/lib/sanctum/command/diff_helper.rb +70 -0
- data/lib/sanctum/command/edit.rb +63 -0
- data/lib/sanctum/command/editor_helper.rb +58 -0
- data/lib/sanctum/command/paths_helper.rb +64 -0
- data/lib/sanctum/command/pull.rb +81 -0
- data/lib/sanctum/command/push.rb +98 -0
- data/lib/sanctum/command/sanctum.example.yaml +33 -0
- data/lib/sanctum/command/view.rb +21 -0
- data/lib/sanctum/get_config.rb +1 -0
- data/lib/sanctum/get_config/config_file.rb +46 -0
- data/lib/sanctum/get_config/config_merge.rb +60 -0
- data/lib/sanctum/get_config/env.rb +18 -0
- data/lib/sanctum/get_config/hash_merger.rb +12 -0
- data/lib/sanctum/get_config/options.rb +41 -0
- data/lib/sanctum/vault_client.rb +31 -0
- data/lib/sanctum/vault_secrets.rb +46 -0
- data/lib/sanctum/vault_transit.rb +60 -0
- data/lib/sanctum/version.rb +3 -0
- data/sanctum.gemspec +32 -0
- metadata +180 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Sanctum
|
2
|
+
module Command
|
3
|
+
class Check < Base
|
4
|
+
|
5
|
+
def run
|
6
|
+
targets.each do |target|
|
7
|
+
# Recursively get local files for each prefix specified in sanctum.yaml
|
8
|
+
local_paths = get_local_paths(File.join(File.dirname(config_file), target[:path]))
|
9
|
+
# Read each file
|
10
|
+
local_secrets = read_local_files(local_paths)
|
11
|
+
# Decrypt each secret
|
12
|
+
local_secrets = VaultTransit.decrypt(vault_client, local_secrets, transit_key)
|
13
|
+
|
14
|
+
# Recursively get vault secrets for each prefix specified in sanctum.yaml
|
15
|
+
secrets_list = VaultSecrets.new(vault_client, target[:prefix]).get
|
16
|
+
|
17
|
+
# Only one entry in this hash (which will be the target).
|
18
|
+
tree = secrets_list.values.first
|
19
|
+
# Build local paths based on prefix and paths specified in sanctum.yaml
|
20
|
+
vault_secrets = build_path(tree, [target[:path]])
|
21
|
+
# Join the path array to create a path
|
22
|
+
vault_secrets = join_path(vault_secrets, config_file)
|
23
|
+
compare_secrets(vault_secrets, local_secrets, target[:name])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Sanctum
|
4
|
+
module Command
|
5
|
+
# Intentionally not extending Base
|
6
|
+
# This command creates an example config
|
7
|
+
class Config
|
8
|
+
include Colorizer
|
9
|
+
attr_reader :config_path, :example_file
|
10
|
+
|
11
|
+
def initialize(options={}, _args=[])
|
12
|
+
options = {working_dir: Dir.pwd}.merge(options)
|
13
|
+
|
14
|
+
relative_path = File.expand_path File.dirname(__FILE__)
|
15
|
+
@config_path = "#{options[:working_dir]}/sanctum.yaml"
|
16
|
+
@example_file = "#{relative_path}/sanctum.example.yaml"
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
raise yellow("config file already exists") if config_exist?
|
21
|
+
create_config_file
|
22
|
+
end
|
23
|
+
|
24
|
+
def config_exist?
|
25
|
+
File.file?(config_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_config_file
|
29
|
+
FileUtils.cp(example_file, config_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
require 'pathname'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Sanctum
|
8
|
+
module Command
|
9
|
+
class Create < Base
|
10
|
+
|
11
|
+
def run(&block)
|
12
|
+
if args.one?
|
13
|
+
path = args[0]
|
14
|
+
validate_path(path)
|
15
|
+
create_file(path, &block)
|
16
|
+
else
|
17
|
+
raise ArgumentError, red('Please pass only one path argument')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def create_file(path)
|
23
|
+
# Calling vault_client will help prevent a race condition where the token is expired
|
24
|
+
# and contents fail to encrypt
|
25
|
+
vault_client
|
26
|
+
tmp_file = Tempfile.new(File.basename(path))
|
27
|
+
|
28
|
+
begin
|
29
|
+
if block_given?
|
30
|
+
yield tmp_file
|
31
|
+
else
|
32
|
+
editor = ENV.fetch('EDITOR', 'vi')
|
33
|
+
raise red("Error with editor") unless system(editor, tmp_file.path)
|
34
|
+
end
|
35
|
+
|
36
|
+
contents = File.read(tmp_file.path)
|
37
|
+
data_hash = {"#{tmp_file.path}" => validate(contents)}
|
38
|
+
write_encrypted_data(vault_client, data_hash, transit_key)
|
39
|
+
tmp_file.close
|
40
|
+
|
41
|
+
FileUtils.cp(tmp_file.path, path)
|
42
|
+
rescue Exception => e
|
43
|
+
# If write_encrypted_data failed, data would fail to write to disk
|
44
|
+
# It would be sad to lose that data, at least this would print the contents to the console.
|
45
|
+
puts red("Contents may have failed to write\nError: #{e}")
|
46
|
+
puts yellow("Contents: \n#{contents}")
|
47
|
+
ensure
|
48
|
+
tmp_file.close
|
49
|
+
secure_erase(tmp_file.path, tmp_file.length)
|
50
|
+
tmp_file.unlink
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_path(path)
|
55
|
+
path = Pathname.new(path)
|
56
|
+
raise yellow("File exists, use edit command") if path.exist?
|
57
|
+
path.dirname.mkpath unless path.dirname.exist?
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'hashdiff'
|
2
|
+
|
3
|
+
module Sanctum
|
4
|
+
module Command
|
5
|
+
module DiffHelper
|
6
|
+
|
7
|
+
def hash_diff(first_hash, second_hash)
|
8
|
+
differences = HashDiff.best_diff(first_hash, second_hash, delimiter: " => ", array_path: true)
|
9
|
+
|
10
|
+
differences.each do |diff|
|
11
|
+
if diff[0] == "+"
|
12
|
+
puts green("#{diff[0].to_s + diff[1].join(" => ").to_s} => #{diff[2]}")
|
13
|
+
else
|
14
|
+
puts red("#{diff[0].to_s + diff[1].join(" => ").to_s} => #{diff[2]}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
differences
|
18
|
+
end
|
19
|
+
|
20
|
+
def compare_secrets(vault_secrets, local_secrets, name, direction="both")
|
21
|
+
if vault_secrets == local_secrets
|
22
|
+
warn yellow("Target #{name}: contains no differences")
|
23
|
+
else
|
24
|
+
case direction
|
25
|
+
when "pull"
|
26
|
+
puts yellow("Target #{name}: differences pulling from vault")
|
27
|
+
hash_diff(local_secrets, vault_secrets)
|
28
|
+
when "push"
|
29
|
+
puts yellow("Target #{name}: differences pushing to vault")
|
30
|
+
hash_diff(vault_secrets, local_secrets)
|
31
|
+
when "both"
|
32
|
+
puts yellow("Target #{name}: differences pulling from vault")
|
33
|
+
hash_diff(local_secrets, vault_secrets)
|
34
|
+
|
35
|
+
puts yellow("Target #{name}: differences pushing to vault")
|
36
|
+
hash_diff(vault_secrets, local_secrets)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def confirmed_with_user?
|
42
|
+
puts yellow("\nWould you like to continue?: ")
|
43
|
+
question = STDIN.gets.chomp.upcase
|
44
|
+
|
45
|
+
if ["Y", "YES"].include? question
|
46
|
+
puts yellow("Overwriting differences")
|
47
|
+
true
|
48
|
+
else
|
49
|
+
warn yellow("Skipping....\n")
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Array is a unique list of paths built from the differences of hash1 and hash2.
|
55
|
+
# See diff_paths variable in push or pull command
|
56
|
+
# Hash will be all local, or vault secrets.
|
57
|
+
# We then build a new hash that contains only the k,v needed to sync (to or from vault)
|
58
|
+
def only_changes(array, hash)
|
59
|
+
tmp_hash = Hash.new
|
60
|
+
array.each do |a|
|
61
|
+
hash.each do |k, v|
|
62
|
+
tmp_hash[k] = v if a == k
|
63
|
+
end
|
64
|
+
end
|
65
|
+
tmp_hash
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'yaml'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Sanctum
|
7
|
+
module Command
|
8
|
+
class Edit < Base
|
9
|
+
|
10
|
+
def run(&block)
|
11
|
+
if args.one?
|
12
|
+
path = args[0]
|
13
|
+
edit_file(path, &block)
|
14
|
+
else
|
15
|
+
raise ArgumentError, red('Please pass only one path argument')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def edit_file(path)
|
21
|
+
tmp_file = Tempfile.new(File.basename(path))
|
22
|
+
|
23
|
+
begin
|
24
|
+
encrypted_data_hash = {path => File.read(path)}
|
25
|
+
decrypted_data_hash = decrypt_data(vault_client, encrypted_data_hash, transit_key)
|
26
|
+
decrypted_data_hash.each_value do |v|
|
27
|
+
v = v.to_yaml
|
28
|
+
File.write(tmp_file.path, v)
|
29
|
+
end
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
yield tmp_file
|
33
|
+
else
|
34
|
+
previous_contents = File.read(tmp_file.path)
|
35
|
+
editor = ENV.fetch('EDITOR', 'vi')
|
36
|
+
raise red("Error with editor") unless system(editor, tmp_file.path )
|
37
|
+
end
|
38
|
+
contents = File.read(tmp_file.path)
|
39
|
+
|
40
|
+
# Only write contents if something changed
|
41
|
+
unless contents == previous_contents
|
42
|
+
data_hash = {"#{tmp_file.path}" => validate(contents)}
|
43
|
+
write_encrypted_data(vault_client, data_hash, transit_key)
|
44
|
+
tmp_file.close
|
45
|
+
|
46
|
+
FileUtils.cp(tmp_file.path, path)
|
47
|
+
end
|
48
|
+
|
49
|
+
rescue Exception => e
|
50
|
+
# If write_encrypted_data failed, data would fail to write to disk
|
51
|
+
# It would be sad to lose that data, at least this would print the contents to the console.
|
52
|
+
puts red("Contents may have failed to write\nError: #{e}")
|
53
|
+
puts yellow("Contents: \n#{contents}")
|
54
|
+
ensure
|
55
|
+
tmp_file.close
|
56
|
+
secure_erase(tmp_file.path, tmp_file.length)
|
57
|
+
tmp_file.unlink
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Sanctum
|
6
|
+
module Command
|
7
|
+
module EditorHelper
|
8
|
+
include Colorizer
|
9
|
+
|
10
|
+
def decrypt_data(vault_client, data, transit_key)
|
11
|
+
VaultTransit.decrypt(vault_client, data, transit_key)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write_encrypted_data(vault_client, data, transit_key)
|
15
|
+
VaultTransit.write_to_file(vault_client, data, transit_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate(contents)
|
19
|
+
validate_json(contents) || validate_yaml(contents) || raise
|
20
|
+
rescue
|
21
|
+
fail red("Invalid Contents")
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate_json(json)
|
25
|
+
JSON.parse(json)
|
26
|
+
rescue JSON::ParserError
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_yaml(yaml)
|
31
|
+
YAML.load(yaml)
|
32
|
+
rescue YAML::SyntaxError
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_random_data(file, file_len)
|
37
|
+
max_chunk_len = [file_len, (1024 * 1024 * 2)].max
|
38
|
+
|
39
|
+
3.times do
|
40
|
+
random_data = SecureRandom.random_bytes(max_chunk_len)
|
41
|
+
File.write(file, random_data, 0, mode: 'wb')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def secure_erase(file, file_len)
|
46
|
+
if file_len >= 1
|
47
|
+
begin
|
48
|
+
#Try to use shred if available on system
|
49
|
+
raise red("Failed system shred") unless system("shred", file)
|
50
|
+
rescue
|
51
|
+
write_random_data(file, file_len)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'find'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Sanctum
|
5
|
+
module Command
|
6
|
+
module PathsHelper
|
7
|
+
|
8
|
+
#Helper methods for building, reading and joining paths
|
9
|
+
private
|
10
|
+
def build_path_helper(hash, path = [])
|
11
|
+
if hash.values.any? { |k| !k.is_a?(Hash) }
|
12
|
+
[path, hash]
|
13
|
+
else
|
14
|
+
hash.flat_map do |(key,value)|
|
15
|
+
build_path_helper(value, path+[key])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
public
|
21
|
+
def build_path(hash, path = [])
|
22
|
+
build_path_helper(hash, path).each_slice(2).to_h
|
23
|
+
end
|
24
|
+
|
25
|
+
def join_path(hash, config_file)
|
26
|
+
config_file = Pathname.new(config_file)
|
27
|
+
tmp_hash = Hash.new
|
28
|
+
|
29
|
+
hash.each do |p, v|
|
30
|
+
p = config_file.dirname + Pathname.new(p.join("/"))
|
31
|
+
tmp_hash["#{p}"] = v
|
32
|
+
end
|
33
|
+
tmp_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_local_files(paths)
|
37
|
+
tmp_hash = Hash.new
|
38
|
+
paths.each do |k,v|
|
39
|
+
if File.file?(k)
|
40
|
+
v = File.read(k)
|
41
|
+
tmp_hash["#{k}"] = v
|
42
|
+
end
|
43
|
+
end
|
44
|
+
tmp_hash
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_local_paths(paths)
|
48
|
+
tmp_array = Array.new
|
49
|
+
Find.find(paths) do |path|
|
50
|
+
if FileTest.file?(path)
|
51
|
+
tmp_array << path
|
52
|
+
if File.basename(path).start_with?(?.)
|
53
|
+
Find.prune
|
54
|
+
else
|
55
|
+
next
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
tmp_array
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Sanctum
|
4
|
+
module Command
|
5
|
+
class Pull < Base
|
6
|
+
|
7
|
+
def run
|
8
|
+
targets.each do |target|
|
9
|
+
# Use command line if force: true
|
10
|
+
if options[:cli][:force]
|
11
|
+
force = options[:cli][:force]
|
12
|
+
else
|
13
|
+
force = target.fetch(:force) {options[:sanctum][:force]}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Recursively get vault secrets for each prefix specified in sanctum.yaml
|
17
|
+
secrets_list = VaultSecrets.new(vault_client, target[:prefix]).get
|
18
|
+
secrets_list.each do |k,v|
|
19
|
+
|
20
|
+
vault_secrets = build_vault_secrets(v, [target[:path]])
|
21
|
+
local_secrets = build_local_secrets(vault_secrets)
|
22
|
+
|
23
|
+
#Compare secrets, if there are no differences continue to next target
|
24
|
+
differences = compare_secrets(vault_secrets, local_secrets, target[:name], "pull")
|
25
|
+
next if differences.nil?
|
26
|
+
|
27
|
+
#Get uniq array of HashDiff returned paths
|
28
|
+
diff_paths = differences.map{|x| x[1][0]}.uniq
|
29
|
+
|
30
|
+
#Only sync the differences
|
31
|
+
vault_secrets = only_changes(diff_paths, vault_secrets)
|
32
|
+
|
33
|
+
if force
|
34
|
+
# Write files to disk and encrypt with transit
|
35
|
+
warn red("#{target[:name]}: Forcefully writing differences to disk(pull)")
|
36
|
+
VaultTransit.write_to_file(vault_client, vault_secrets, transit_key)
|
37
|
+
else
|
38
|
+
#Confirm with user, and write to local file if approved
|
39
|
+
next unless confirmed_with_user?
|
40
|
+
VaultTransit.write_to_file(vault_client, vault_secrets, transit_key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_paths(paths)
|
48
|
+
paths.each do |k,v|
|
49
|
+
k = Pathname.new(k)
|
50
|
+
unless k.dirname.exist?
|
51
|
+
k.dirname.mkpath
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_vault_secrets(tree, path)
|
57
|
+
# Build local paths based on vault_prefix(tree) and paths specified in sanctum.yaml
|
58
|
+
vault_secrets = build_path(tree, path)
|
59
|
+
|
60
|
+
# Join the path array to create a path
|
61
|
+
vault_secrets = join_path(vault_secrets, config_file)
|
62
|
+
|
63
|
+
# Ensure local paths exist, relative to sanctum.yaml if they don't create them
|
64
|
+
create_paths(vault_secrets)
|
65
|
+
vault_secrets
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_local_secrets(vault_secrets)
|
69
|
+
|
70
|
+
# read_local_files uses vault_secrets paths to create a new hash with local paths and values.
|
71
|
+
# This means that we will only compare secrets/paths that exist in both vault and locally.
|
72
|
+
# We will not for example, see differences if a file exists locally but not in vault.
|
73
|
+
local_secrets = read_local_files(vault_secrets)
|
74
|
+
# Decrypt local_secrets
|
75
|
+
local_secrets = VaultTransit.decrypt(vault_client, local_secrets, transit_key)
|
76
|
+
local_secrets
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|