Skip to content

Commit 179bae6

Browse files
adamlazik1adamruzicka
authored andcommitted
Refs #38478 - Introduce SSH CA certificate support
1 parent d8d1811 commit 179bae6

File tree

7 files changed

+61
-3
lines changed

7 files changed

+61
-3
lines changed

lib/smart_proxy_remote_execution_ssh.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ def public_key_file
2121
File.expand_path("#{private_key_file}.pub")
2222
end
2323

24+
def cert_file
25+
File.expand_path("#{private_key_file}-cert.pub")
26+
end
27+
28+
def ca_public_key_file
29+
path = Plugin.settings.ssh_user_ca_public_key_file
30+
File.expand_path(path) if present?(path)
31+
end
32+
2433
def validate_mode!
2534
Plugin.settings.mode = Plugin.settings.mode.to_sym
2635

@@ -50,14 +59,23 @@ def validate_ssh_settings!
5059
end
5160

5261
unless File.exist?(private_key_file)
53-
raise "SSH public key file #{private_key_file} doesn't exist.\n"\
62+
raise "SSH private key file #{private_key_file} doesn't exist.\n"\
5463
"You can generate one with `ssh-keygen -t rsa -b 4096 -f #{private_key_file} -N ''`"
5564
end
5665

5766
unless File.exist?(public_key_file)
5867
raise "SSH public key file #{public_key_file} doesn't exist"
5968
end
6069

70+
if present?(Plugin.settings.ssh_user_ca_public_key_file)
71+
{ ca_public_key_file: 'CA public key', cert_file: 'certificate' }.each do |file, label|
72+
file_path = public_send(file)
73+
unless file_path && File.exist?(file_path)
74+
raise "SSH #{label} file '#{file_path}' doesn't exist"
75+
end
76+
end
77+
end
78+
6179
validate_ssh_log_level!
6280
end
6381

@@ -100,6 +118,12 @@ def job_storage
100118
def with_mqtt?
101119
Proxy::RemoteExecution::Ssh::Plugin.settings.mode == :'pull-mqtt'
102120
end
121+
122+
private
123+
124+
def present?(value)
125+
value && !value.empty?
126+
end
103127
end
104128
end
105129
end

lib/smart_proxy_remote_execution_ssh/api.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ class Api < ::Sinatra::Base
1313
File.read(Ssh.public_key_file)
1414
end
1515

16+
get "/ca_pubkey" do
17+
if Ssh.ca_public_key_file
18+
File.read(Ssh.ca_public_key_file)
19+
end
20+
end
21+
1622
if Proxy::RemoteExecution::Ssh::Plugin.settings.cockpit_integration
1723
post "/session" do
1824
do_authorize_any

lib/smart_proxy_remote_execution_ssh/multiplexed_ssh_connection.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def initialize(options, logger:)
6060
@host_public_key = options.fetch(:host_public_key, nil)
6161
@verify_host = options.fetch(:verify_host, nil)
6262
@client_private_key_file = settings.ssh_identity_key_file
63+
@client_ca_known_hosts_file = settings.ssh_ca_known_hosts_file
64+
@client_cert_file = Proxy::RemoteExecution::Ssh.cert_file if File.exist?(Proxy::RemoteExecution::Ssh.cert_file)
6365

6466
@local_working_dir = options.fetch(:local_working_dir, settings.local_working_dir)
6567
@socket_working_dir = options.fetch(:socket_working_dir, settings.socket_working_dir)
@@ -154,9 +156,14 @@ def establish_ssh_options
154156
ssh_options << "-o User=#{@ssh_user}"
155157
ssh_options << "-o Port=#{@ssh_port}" if @ssh_port
156158
ssh_options << "-o IdentityFile=#{@client_private_key_file}" if @client_private_key_file
159+
ssh_options << "-o CertificateFile=#{@client_cert_file}" if @client_cert_file
157160
ssh_options << "-o IdentitiesOnly=yes"
158-
ssh_options << "-o StrictHostKeyChecking=accept-new"
159-
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}" if @host_public_key
161+
ssh_options << "-o StrictHostKeyChecking=#{@client_ca_known_hosts_file ? 'yes' : 'accept-new'}"
162+
if @host_public_key
163+
ssh_options << "-o UserKnownHostsFile=#{prepare_known_hosts}"
164+
elsif @client_ca_known_hosts_file
165+
ssh_options << "-o UserKnownHostsFile=#{@client_ca_known_hosts_file}"
166+
end
160167
ssh_options << "-o LogLevel=#{ssh_log_level(true)}"
161168
ssh_options << "-o ControlMaster=auto"
162169
ssh_options << "-o ControlPath=#{socket_file}"

lib/smart_proxy_remote_execution_ssh/plugin.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class Plugin < Proxy::Plugin
1212

1313
settings_file "remote_execution_ssh.yml"
1414
default_settings :ssh_identity_key_file => '~/.ssh/id_rsa_foreman_proxy',
15+
# :ssh_ca_known_hosts_file => nil,
16+
# :ssh_user_ca_public_key_file => nil,
1517
:ssh_user => 'root',
1618
:remote_working_dir => '/var/tmp',
1719
:local_working_dir => '/var/tmp',

settings.d/remote_execution_ssh.yml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
# Mode of operation, one of ssh, pull, pull-mqtt
1212
:mode: ssh
1313

14+
# Enables the use of SSH certificate for smart proxy authentication
15+
# The file should contain an SSH CA public key that the SSH public key of smart proxy is signed by
16+
# :ssh_user_ca_public_key_file:
17+
18+
# Enables the use of SSH host certificates for host authentication
19+
# The file should contain a list of trusted SSH CA authorities that the host certs can be signed by
20+
# Example file content: @cert-authority * <SSH CA public key>
21+
# :ssh_ca_known_hosts_file:
22+
1423
# Defines how often (in seconds) should the runner check
1524
# for new data leave empty to use the runner's default
1625
# :runner_refresh_interval: 1

test/api_test.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def setup
4141
end
4242
end
4343

44+
describe '/ca_pubkey' do
45+
it 'returns the content of the CA public key' do
46+
get '/ca_pubkey'
47+
_(last_response.body).must_equal '===ca-public-key==='
48+
end
49+
end
50+
4451
describe 'job storage' do
4552
let(:uuid) { SecureRandom.uuid }
4653
let(:execution_plan_uuid) { SecureRandom.uuid }

test/test_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
DATA_DIR = File.expand_path('../data', __FILE__)
1717
FAKE_PRIVATE_KEY_FILE = File.join(DATA_DIR, 'fake_id_rsa')
1818
FAKE_PUBLIC_KEY_FILE = "#{FAKE_PRIVATE_KEY_FILE}.pub"
19+
FAKE_CA_PUBLIC_KEY_FILE = File.join(DATA_DIR, 'fake_ca_cert.pub')
1920

2021
logdir = File.join(File.dirname(__FILE__), '..', 'logs')
2122
FileUtils.mkdir_p(logdir) unless File.exist?(logdir)
@@ -24,9 +25,11 @@ def prepare_fake_keys
2425
Proxy::RemoteExecution::Ssh::Plugin.settings.ssh_identity_key_file = FAKE_PRIVATE_KEY_FILE
2526
# Workaround for Proxy::RemoteExecution::Ssh::Plugin.settings.ssh_identity_key_file returning nil
2627
Proxy::RemoteExecution::Ssh::Plugin.settings.stubs(:ssh_identity_key_file).returns(FAKE_PRIVATE_KEY_FILE)
28+
Proxy::RemoteExecution::Ssh::Plugin.settings.stubs(:ssh_user_ca_public_key_file).returns(FAKE_CA_PUBLIC_KEY_FILE)
2729
FileUtils.mkdir_p(DATA_DIR) unless File.exist?(DATA_DIR)
2830
File.write(FAKE_PRIVATE_KEY_FILE, '===private-key===')
2931
File.write(FAKE_PUBLIC_KEY_FILE, '===public-key===')
32+
File.write(FAKE_CA_PUBLIC_KEY_FILE, '===ca-public-key===')
3033
end
3134

3235
class Minitest::Test

0 commit comments

Comments
 (0)