diff --git a/lib/smart_proxy_remote_execution_ssh.rb b/lib/smart_proxy_remote_execution_ssh.rb index c6649c5..b7ee3ba 100644 --- a/lib/smart_proxy_remote_execution_ssh.rb +++ b/lib/smart_proxy_remote_execution_ssh.rb @@ -21,6 +21,15 @@ def public_key_file File.expand_path("#{private_key_file}.pub") end + def cert_file + File.expand_path("#{private_key_file}-cert.pub") + end + + def ca_public_key_file + path = Plugin.settings.ssh_user_ca_public_key_file + File.expand_path(path) if present?(path) + end + def validate_mode! Plugin.settings.mode = Plugin.settings.mode.to_sym @@ -64,7 +73,7 @@ def validate_ssh_settings! end unless File.exist?(private_key_file) - raise "SSH public key file #{private_key_file} doesn't exist.\n"\ + raise "SSH private key file #{private_key_file} doesn't exist.\n"\ "You can generate one with `ssh-keygen -t rsa -b 4096 -f #{private_key_file} -N ''`" end @@ -72,6 +81,15 @@ def validate_ssh_settings! raise "SSH public key file #{public_key_file} doesn't exist" end + if present?(Plugin.settings.ssh_user_ca_public_key_file) + { ca_public_key_file: 'CA public key', cert_file: 'certificate' }.each do |file, label| + file_path = public_send(file) + unless file_path && File.exist?(file_path) + raise "SSH #{label} file '#{file_path}' doesn't exist" + end + end + end + validate_ssh_log_level! end @@ -114,6 +132,12 @@ def job_storage def with_mqtt? Proxy::RemoteExecution::Ssh::Plugin.settings.mode == :'pull-mqtt' end + + private + + def present?(value) + value && !value.empty? + end end end end diff --git a/lib/smart_proxy_remote_execution_ssh/api.rb b/lib/smart_proxy_remote_execution_ssh/api.rb index eac3b20..4b857af 100644 --- a/lib/smart_proxy_remote_execution_ssh/api.rb +++ b/lib/smart_proxy_remote_execution_ssh/api.rb @@ -13,6 +13,12 @@ class Api < ::Sinatra::Base File.read(Ssh.public_key_file) end + get "/ca_pubkey" do + if Ssh.ca_public_key_file + File.read(Ssh.ca_public_key_file) + end + end + if Proxy::RemoteExecution::Ssh::Plugin.settings.cockpit_integration post "/session" do do_authorize_any diff --git a/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb b/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb index 68f1927..8f6b8f3 100644 --- a/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb +++ b/lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb @@ -60,6 +60,8 @@ def initialize(options, logger:) @host_public_key = options.fetch(:host_public_key, nil) @verify_host = options.fetch(:verify_host, nil) @client_private_key_file = settings.ssh_identity_key_file + @client_ca_known_hosts_file = settings.ssh_ca_known_hosts_file + @client_cert_file = Proxy::RemoteExecution::Ssh.cert_file @local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir) @socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir) @@ -154,9 +156,14 @@ def establish_ssh_options ssh_options << "-o User=#{@ssh_user}" ssh_options << "-o Port=#{@ssh_port}" if @ssh_port ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file + ssh_options << "-o CertificateFile=#{@client_cert_file}" if @client_cert_file ssh_options << "-o IdentitiesOnly=yes" - ssh_options << "-o StrictHostKeyChecking=accept-new" - ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key + ssh_options << "-o StrictHostKeyChecking=#{@client_ca_known_hosts_file ? 'yes' : 'accept-new'}" + if @host_public_key + ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" + elsif @client_ca_known_hosts_file + ssh_options << "-o UserKnownHostsFile=#{@client_ca_known_hosts_file}" + end ssh_options << "-o LogLevel=#{ssh_log_level(true)}" ssh_options << "-o ControlMaster=auto" ssh_options << "-o ControlPath=#{socket_file}" diff --git a/lib/smart_proxy_remote_execution_ssh/plugin.rb b/lib/smart_proxy_remote_execution_ssh/plugin.rb index 21ab35c..9c5347a 100644 --- a/lib/smart_proxy_remote_execution_ssh/plugin.rb +++ b/lib/smart_proxy_remote_execution_ssh/plugin.rb @@ -12,6 +12,8 @@ class Plugin < Proxy::Plugin settings_file "remote_execution_ssh.yml" default_settings :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy', + # :ssh_ca_known_hosts_file => nil, + # :ssh_user_ca_public_key_file => nil, :ssh_user => 'root', :remote_working_dir => '/var/tmp', :local_working_dir => '/var/tmp', diff --git a/test/api_test.rb b/test/api_test.rb index 832804f..e5dc2d5 100644 --- a/test/api_test.rb +++ b/test/api_test.rb @@ -41,6 +41,13 @@ def setup end end + describe '/ca_pubkey' do + it 'returns the content of the CA public key' do + get '/ca_pubkey' + _(last_response.body).must_equal '===ca-public-key===' + end + end + describe 'job storage' do let(:uuid) { SecureRandom.uuid } let(:execution_plan_uuid) { SecureRandom.uuid } diff --git a/test/test_helper.rb b/test/test_helper.rb index 3a631e1..5966a59 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -16,6 +16,7 @@ DATA_DIR = File.expand_path('../data', __FILE__) FAKE_PRIVATE_KEY_FILE = File.join(DATA_DIR, 'fake_id_rsa') FAKE_PUBLIC_KEY_FILE = "#{FAKE_PRIVATE_KEY_FILE}.pub" +FAKE_CA_PUBLIC_KEY_FILE = File.join(DATA_DIR, 'fake_ca_cert.pub') logdir = File.join(File.dirname(__FILE__), '..', 'logs') FileUtils.mkdir_p(logdir) unless File.exist?(logdir) @@ -24,9 +25,11 @@ def prepare_fake_keys Proxy::RemoteExecution::Ssh::Plugin.settings.ssh_identity_key_file = FAKE_PRIVATE_KEY_FILE # Workaround for Proxy::RemoteExecution::Ssh::Plugin.settings.ssh_identity_key_file returning nil Proxy::RemoteExecution::Ssh::Plugin.settings.stubs(:ssh_identity_key_file).returns(FAKE_PRIVATE_KEY_FILE) + Proxy::RemoteExecution::Ssh::Plugin.settings.stubs(:ssh_user_ca_public_key_file).returns(FAKE_CA_PUBLIC_KEY_FILE) FileUtils.mkdir_p(DATA_DIR) unless File.exist?(DATA_DIR) File.write(FAKE_PRIVATE_KEY_FILE, '===private-key===') File.write(FAKE_PUBLIC_KEY_FILE, '===public-key===') + File.write(FAKE_CA_PUBLIC_KEY_FILE, '===ca-public-key===') end class Minitest::Test