Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/kamal/configuration/docs/ssh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ ssh:

# Key data
#
# An array of strings, with each element of the array being
# a raw private key in PEM format.
key_data: [ "-----BEGIN OPENSSH PRIVATE KEY-----" ]
# An array of strings, with each element of the array being a secret name.
key_data:
- SSH_PRIVATE_KEY

# Config
#
Expand Down
15 changes: 13 additions & 2 deletions lib/kamal/configuration/ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ class Kamal::Configuration::Ssh

include Kamal::Configuration::Validation

attr_reader :ssh_config
attr_reader :ssh_config, :secrets

def initialize(config:)
@ssh_config = config.raw_config.ssh || {}
@secrets = config.secrets
validate! ssh_config
end

Expand Down Expand Up @@ -35,7 +36,17 @@ def keys
end

def key_data
ssh_config["key_data"]
key_data = ssh_config["key_data"]
return unless key_data

key_data.map do |k|
if secrets.key?(k)
secrets[k]
else
warn "Inline key_data usage is deprecated and will be removed in Kamal 3. Please store your key_data in a secret."
k
end
end
end

def config
Expand Down
18 changes: 14 additions & 4 deletions lib/kamal/secrets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ def initialize(destination: nil, secrets_path:)
end

def [](key)
# Fetching secrets may ask the user for input, so ensure only one thread does that
@mutex.synchronize do
secrets.fetch(key)
end
synchronized_fetch(key)
rescue KeyError
if secrets_files.present?
raise Kamal::ConfigurationError, "Secret '#{key}' not found in #{secrets_files.join(", ")}"
Expand All @@ -30,6 +27,12 @@ def secrets_files
@secrets_files ||= secrets_filenames.select { |f| File.exist?(f) }
end

def key?(key)
synchronized_fetch(key).present?
rescue KeyError
false
end

private
def secrets
@secrets ||= secrets_files.inject({}) do |secrets, secrets_file|
Expand All @@ -40,4 +43,11 @@ def secrets
def secrets_filenames
[ "#{@secrets_path}-common", "#{@secrets_path}#{(".#{@destination}" if @destination)}" ]
end

def synchronized_fetch(key)
# Fetching secrets may ask the user for input, so ensure only one thread does that
@mutex.synchronize do
secrets.fetch(key)
end
end
end
19 changes: 19 additions & 0 deletions test/configuration/ssh_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,23 @@ class ConfigurationSshTest < ActiveSupport::TestCase
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "proxy" => "[email protected]" }) })
assert_equal "[email protected]", config.ssh.options[:proxy].jump_proxies
end

test "ssh key_data with plain value array" do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "key_data" => [ "-----BEGIN OPENSSH PRIVATE KEY-----" ] }) })
assert_equal [ "-----BEGIN OPENSSH PRIVATE KEY-----" ], config.ssh.options[:key_data]
end

test "ssh key_data with array containing one secret string" do
with_test_secrets("secrets" => "SSH_PRIVATE_KEY=secret_ssh_key") do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "key_data" => [ "SSH_PRIVATE_KEY" ] }) })
assert_equal [ "secret_ssh_key" ], config.ssh.options[:key_data]
end
end

test "ssh key_data with array containing multiple secret strings" do
with_test_secrets("secrets" => "SSH_PRIVATE_KEY=secret_ssh_key\nSECOND_KEY=second_secret_ssh_key") do
config = Kamal::Configuration.new(@deploy.tap { |c| c.merge!(ssh: { "key_data" => [ "SSH_PRIVATE_KEY", "SECOND_KEY" ] }) })
assert_equal [ "secret_ssh_key", "second_secret_ssh_key" ], config.ssh.options[:key_data]
end
end
end
13 changes: 13 additions & 0 deletions test/secrets_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ class SecretsTest < ActiveSupport::TestCase
end
end

test "synchronized_fetch" do
with_test_secrets("secrets" => "SECRET=ABC") do
assert_equal "ABC", Kamal::Secrets.new(secrets_path: ".kamal/secrets").send(:synchronized_fetch, "SECRET")
end
end

test "key?" do
with_test_secrets("secrets" => "SECRET1=ABC") do
assert Kamal::Secrets.new(secrets_path: ".kamal/secrets").key?("SECRET1")
assert_not Kamal::Secrets.new(secrets_path: ".kamal/secrets").key?("SECRET2")
end
end

test "command interpolation" do
with_test_secrets("secrets" => "SECRET=$(echo ABC)") do
assert_equal "ABC", Kamal::Secrets.new(secrets_path: ".kamal/secrets")["SECRET"]
Expand Down
Loading