Skip to content
Draft
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
26 changes: 25 additions & 1 deletion lib/smart_proxy_remote_execution_ssh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +24 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we require this strict naming convention for cert file
similarly as we do for public key file, or should we make this an editable
value in settings, with this format being the possible default?


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

Expand Down Expand Up @@ -64,14 +73,23 @@ 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

unless File.exist?(public_key_file)
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

Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/smart_proxy_remote_execution_ssh/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you treat these inconsistently? There is also Proxy::RemoteExecution::Ssh.ca_public_key_file but you don't use it. And for the others, there is no such method.

Copy link
Contributor Author

@adamlazik1 adamlazik1 Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It relates to my question to the comment above: #126 (comment)

cert_file is, for now, a non-editable value whose name depends on the private key file, so it is not in settings. Personally, I do not see any problem to making the cert file name an editable value, I just wanted to confirm first whether there are any objections against it. For example, public key file name is also a non-editable value and depends on the private key 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)
Expand Down Expand Up @@ -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}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a known_hosts file with cert authorities listed, should we change StrictHostKeyChecking to yes? To me, it doesn't make much sense to have it otherwise.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO yes. Otherwise, we get "if you trust this cert, connect and if you don't, connect anyway".

Copy link
Contributor

@adamruzicka adamruzicka Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For host keys, those should be all the combinations that can happen:

Proxy Host Result
No record for host plain SSH key TOFU - success + save key
Record for host plain SSH key, matching record success
Record for host plain SSH key, not matching record failure
CA cert plain SSH key Fallback to rows 1-3 failure
CA cert trusted certificate success
CA cert untrusted certificate failure
No record for host, no CA cert untrusted certificate Fallback to row 1
Record for host, no CA cert untrusted certificate Fallback to rows 2-3

Could we agree that this table describes the expected behaviour?

One could argue that proxy expecting a cert and host not providing any (row 4) should fail rather than falling back to traditional pubkey auth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too would say that row 4 could fail, but there is currently option to only have one host CA per smart proxy so it would be setting behavior for all hosts that use that proxy for REX. That is probably the only point that I see why it should fallback to plain SSH key instead of fail.

Copy link
Contributor Author

@adamlazik1 adamlazik1 Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until we decide on one or the other, I will treat the expected behavior of row 4 to be failure. I updated the StrictHostKeyChecking param accordingly. I will also make the same edits to ansible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So row 4 should indeed result in a failure, added two more rows to the table describing one other case that we previously missed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm that rows 7 and 8 also behave as intended, so the behavior of the whole feature should work as expected.

end
ssh_options << "-o LogLevel=#{ssh_log_level(true)}"
ssh_options << "-o ControlMaster=auto"
ssh_options << "-o ControlPath=#{socket_file}"
Expand Down
2 changes: 2 additions & 0 deletions lib/smart_proxy_remote_execution_ssh/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions test/api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading