diff --git a/app/lib/proxy_api/remote_execution_ssh.rb b/app/lib/proxy_api/remote_execution_ssh.rb index 8b755d27b..a68330af3 100644 --- a/app/lib/proxy_api/remote_execution_ssh.rb +++ b/app/lib/proxy_api/remote_execution_ssh.rb @@ -11,6 +11,12 @@ def pubkey raise ProxyException.new(url, e, N_('Unable to fetch public key')) end + def ca_pubkey + get('ca_pubkey')&.strip + rescue => e + raise ProxyException.new(url, e, N_('Unable to fetch CA public key')) + end + def drop_from_known_hosts(hostname) delete('known_hosts/' + hostname) rescue => e diff --git a/app/models/concerns/foreman_remote_execution/host_extensions.rb b/app/models/concerns/foreman_remote_execution/host_extensions.rb index 35fb32e28..ebbd7fa27 100644 --- a/app/models/concerns/foreman_remote_execution/host_extensions.rb +++ b/app/models/concerns/foreman_remote_execution/host_extensions.rb @@ -108,7 +108,12 @@ def remote_execution_proxies(provider, authorized = true) end def remote_execution_ssh_keys - remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey }.compact.uniq + # only include public keys from SSH proxies that don't have SSH cert verification configured + remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.pubkey if proxy.ca_pubkey.blank? }.compact.uniq + end + + def remote_execution_ssh_ca_keys + remote_execution_proxies(%w(SSH Script), false).values.flatten.uniq.map { |proxy| proxy.ca_pubkey }.compact.uniq end def drop_execution_interface_cache @@ -139,10 +144,13 @@ def infrastructure_host? def extend_host_params_hash(params) keys = remote_execution_ssh_keys + ca_keys = remote_execution_ssh_ca_keys source = 'global' - if keys.present? - value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact } - params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source} + {keys: keys, ca_keys: ca_keys}.each do |key_set_name, key_set| + if key_set.present? + value, safe_value = params.fetch("remote_execution_ssh_#{key_set_name}", {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact } + params["remote_execution_ssh_#{key_set_name}"] = {:value => value + key_set, :safe_value => safe_value + key_set, :source => source} + end end [:remote_execution_ssh_user, :remote_execution_effective_user_method, :remote_execution_connect_by_ip].each do |key| diff --git a/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb b/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb index 163b21b36..9d2ac8091 100644 --- a/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb +++ b/app/models/concerns/foreman_remote_execution/smart_proxy_extensions.rb @@ -11,6 +11,10 @@ def pubkey self[:pubkey] || update_pubkey end + def ca_pubkey + self[:ca_pubkey] || update_ca_pubkey + end + def update_pubkey return unless has_feature?(%w(SSH Script)) @@ -19,6 +23,15 @@ def update_pubkey key end + def update_ca_pubkey + return unless has_feature?(%w(SSH Script)) + + # smart proxy is not required to have a CA pubkey, in which case an empty string is returned + key = ::ProxyAPI::RemoteExecutionSSH.new(:url => url).ca_pubkey&.presence + self.update_attribute(:ca_pubkey, key) + key + end + def drop_host_from_known_hosts(host) ::ProxyAPI::RemoteExecutionSSH.new(:url => url).drop_from_known_hosts(host) end @@ -26,6 +39,7 @@ def drop_host_from_known_hosts(host) def refresh errors = super update_pubkey + update_ca_pubkey errors end end diff --git a/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl b/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl new file mode 100644 index 000000000..fd13aae3a --- /dev/null +++ b/app/views/api/v2/smart_proxies/ca_pubkey.json.rabl @@ -0,0 +1 @@ +attribute :ca_pubkey => :remote_execution_ca_pubkey diff --git a/db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb b/db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb new file mode 100644 index 000000000..f5a189770 --- /dev/null +++ b/db/migrate/20250606125543_add_ca_pub_key_to_smart_proxy.rb @@ -0,0 +1,5 @@ +class AddCAPubKeyToSmartProxy < ActiveRecord::Migration[7.0] + def change + add_column :smart_proxies, :ca_pubkey, :text + end +end diff --git a/lib/foreman_remote_execution/plugin.rb b/lib/foreman_remote_execution/plugin.rb index 69a5241a4..3dace7a7f 100644 --- a/lib/foreman_remote_execution/plugin.rb +++ b/lib/foreman_remote_execution/plugin.rb @@ -213,6 +213,7 @@ extend_template_helpers ForemanRemoteExecution::RendererMethods extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/pubkey' + extend_rabl_template 'api/v2/smart_proxies/main', 'api/v2/smart_proxies/ca_pubkey' extend_rabl_template 'api/v2/interfaces/main', 'api/v2/interfaces/execution_flag' extend_rabl_template 'api/v2/subnets/show', 'api/v2/subnets/remote_execution_proxies' extend_rabl_template 'api/v2/hosts/main', 'api/v2/host/main' diff --git a/test/unit/concerns/host_extensions_test.rb b/test/unit/concerns/host_extensions_test.rb index ae4bfe950..c493d0604 100644 --- a/test/unit/concerns/host_extensions_test.rb +++ b/test/unit/concerns/host_extensions_test.rb @@ -12,6 +12,7 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase before do SmartProxy.any_instance.stubs(:pubkey).returns(sshkey) + SmartProxy.any_instance.stubs(:ca_pubkey).returns(nil) Setting[:remote_execution_ssh_user] = 'root' Setting[:remote_execution_effective_user_method] = 'sudo' end @@ -61,6 +62,48 @@ class ForemanRemoteExecutionHostExtensionsTest < ActiveSupport::TestCase end end + describe 'has ssh CA key configured' do + let(:host) { FactoryBot.create(:host, :with_execution) } + let(:sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQ foo@example.com' } + let(:ca_sshkey) { 'ssh-rsa AAAAB3NzaC1yc2EAAAABJE bar@example.com' } + + before do + SmartProxy.any_instance.stubs(:pubkey).returns(sshkey) + SmartProxy.any_instance.stubs(:ca_pubkey).returns(ca_sshkey) + Setting[:remote_execution_ssh_user] = 'root' + Setting[:remote_execution_effective_user_method] = 'sudo' + end + + it 'has CA ssh keys in the parameters' do + assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey + end + + it 'excludes ssh keys from proxies that have SSH CA key configured' do + assert_empty host.remote_execution_ssh_keys + end + + it 'merges ssh CA keys from host parameters and proxies' do + key = 'ssh-rsa not-even-a-key something@somewhere.com' + host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => [key]) + assert_includes host.host_param('remote_execution_ssh_ca_keys'), key + assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey + end + + it 'has ssh CA keys in the parameters even when no user specified' do + FactoryBot.create(:smart_proxy, :ssh) + host.interfaces.first.subnet.remote_execution_proxies.clear + User.current = nil + assert_includes host.remote_execution_ssh_ca_keys, ca_sshkey + end + + it 'merges ssh CA key as a string from host parameters and proxies' do + key = 'ssh-rsa not-even-a-key something@somewhere.com' + host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_ssh_ca_keys', :value => key) + assert_includes host.host_param('remote_execution_ssh_ca_keys'), key + assert_includes host.host_param('remote_execution_ssh_ca_keys'), ca_sshkey + end + end + context 'host has multiple nics' do let(:host) { FactoryBot.build(:host, :with_execution) }