From 1fd7a96543c8c4a990f31107857e23ca4974911f Mon Sep 17 00:00:00 2001 From: alazik Date: Mon, 16 Jun 2025 09:56:50 +0200 Subject: [PATCH 1/3] Fixes #38499 - Introduce SSH cert support --- .../snippet/remote_execution_ssh_keys.erb | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb index 0818e50c4b5..8be8f7e202d 100644 --- a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb +++ b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb @@ -10,6 +10,9 @@ description: | remote_execution_ssh_keys: public keys to be put in ~/.ssh/authorized_keys + remote_execution_ssh_ca_keys: public ssh CA keys to be put in + /etc/ssh/foreman-user-ca.pub + remote_execution_ssh_user: user for which remote_execution_ssh_keys will be authorized @@ -33,7 +36,7 @@ if [ -z "$PKG_MANAGER" ]; then <%= indent(2) { snippet 'pkg_manager' } -%> fi -<% if !host_param('remote_execution_ssh_keys').blank? %> +<% if host_param('remote_execution_ssh_keys').present? || host_param('remote_execution_ssh_ca_keys').present? %> <% ssh_user = host_param('remote_execution_ssh_user') || 'root' %> user_exists=false @@ -46,6 +49,7 @@ fi <% end -%> if $user_exists; then +<% if host_param('remote_execution_ssh_keys').present? -%> <% ssh_path = "~#{ssh_user}/.ssh" %> mkdir -p <%= ssh_path %> @@ -63,6 +67,23 @@ EOF # Restore SELinux context with restorecon, if it's available: command -v restorecon && restorecon -RvF <%= ssh_path %> || true +<% end -%> + +<% user_ca_keys = host_param('remote_execution_ssh_ca_keys') %> +<% if user_ca_keys.present? -%> + mkdir -p /etc/ssh/sshd_config.d + +<% user_ca_path = '/etc/ssh/foreman-user-ca.pub' %> +<%= save_to_file(user_ca_path, user_ca_keys.is_a?(String) ? user_ca_keys : user_ca_keys.join("\n")) %> + + chmod 0644 <%= user_ca_path %> + +<%= save_to_file('/etc/ssh/sshd_config.d/60-foreman-user-ca.conf', "TrustedUserCAKeys #{user_ca_path}") %> + + command -v restorecon && restorecon -RvF /etc/ssh || true + + systemctl try-restart <%= @host.operatingsystem&.family == 'Debian' ? 'ssh' : 'sshd' %> +<% end -%> <% if ssh_user != 'root' && host_param('remote_execution_effective_user_method') == 'sudo' -%> if [ ! -x "$(command -v sudo)" ]; then From be08409d7c9306a04782c3f36dab225531245ef2 Mon Sep 17 00:00:00 2001 From: alazik Date: Mon, 16 Jun 2025 09:56:50 +0200 Subject: [PATCH 2/3] Fixes #38499 - Introduce SSH cert support --- .../snippet/remote_execution_ssh_keys.erb | 18 ++++++++++++++++++ .../snippet/restart_sshd.erb | 15 +++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 app/views/unattended/provisioning_templates/snippet/restart_sshd.erb diff --git a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb index 8be8f7e202d..083efa531ec 100644 --- a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb +++ b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb @@ -85,6 +85,24 @@ EOF systemctl try-restart <%= @host.operatingsystem&.family == 'Debian' ? 'ssh' : 'sshd' %> <% end -%> +<% user_ca_keys = host_param('remote_execution_ssh_ca_keys') %> +<% if user_ca_keys.present? -%> + mkdir -p /etc/ssh/sshd_config.d + +<% user_ca_path = '/etc/ssh/foreman-user-ca.pub' %> +<%= save_to_file(user_ca_path, user_ca_keys.is_a?(String) ? user_ca_keys : user_ca_keys.join("\n")) %> + + chmod 0644 <%= user_ca_path %> + +<%= save_to_file('/etc/ssh/sshd_config.d/60-foreman-user-ca.conf', "TrustedUserCAKeys #{user_ca_path}") %> + + command -v restorecon && restorecon -RvF /etc/ssh || true + + # restart the sshd service + $(command -v cloud-init && cloud-init status --wait) >/dev/null 2>&1 || true + systemctl restart <%= @host.operatingsystem.family == 'Debian' ? 'ssh' : 'sshd' %> +<% end -%> + <% if ssh_user != 'root' && host_param('remote_execution_effective_user_method') == 'sudo' -%> if [ ! -x "$(command -v sudo)" ]; then $PKG_MANAGER_INSTALL sudo diff --git a/app/views/unattended/provisioning_templates/snippet/restart_sshd.erb b/app/views/unattended/provisioning_templates/snippet/restart_sshd.erb new file mode 100644 index 00000000000..460fe7fe2a1 --- /dev/null +++ b/app/views/unattended/provisioning_templates/snippet/restart_sshd.erb @@ -0,0 +1,15 @@ +<%# +kind: snippet +name: restart_sshd +model: ProvisioningTemplate +snippet: true +description: | + This snippet restarts the sshd service + + Parameters: + + remote_execution_ssh_ca_keys: public ssh CA keys +-%> + +systemctl try-restart <%= @host.operatingsystem&.family == 'Debian' ? 'ssh' : 'sshd' %> +echo "Restarted sshd to apply CA keys" > /tmp/ssh_ca_keys_applied From 7bfdde49722b463136d59635f59a5d65e8f91baf Mon Sep 17 00:00:00 2001 From: alazik Date: Mon, 16 Jun 2025 09:56:50 +0200 Subject: [PATCH 3/3] Refs #38499 - create append_to_file macro --- .../foreman/renderer/configuration.rb | 2 ++ .../foreman/renderer/scope/macros/base.rb | 27 +++++++++++++++++++ .../foreman/renderer/scope/macros/helpers.rb | 9 +++++++ .../snippet/remote_execution_ssh_keys.erb | 4 +-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/app/services/foreman/renderer/configuration.rb b/app/services/foreman/renderer/configuration.rb index 42f08623a9a..f22b22d16fc 100644 --- a/app/services/foreman/renderer/configuration.rb +++ b/app/services/foreman/renderer/configuration.rb @@ -14,6 +14,7 @@ class Configuration :template_name, :dns_lookup, :pxe_kernel_options, + :append_to_file, :save_to_file, :subnet_param, :subnet_has_param?, :global_setting, @@ -57,6 +58,7 @@ class Configuration :foreman_server_ca_cert, :format_time, :shell_escape, + :expand_path, :join_with_line_break, :current_date, :current_time, diff --git a/app/services/foreman/renderer/scope/macros/base.rb b/app/services/foreman/renderer/scope/macros/base.rb index 4175d817723..a7c1ded558c 100644 --- a/app/services/foreman/renderer/scope/macros/base.rb +++ b/app/services/foreman/renderer/scope/macros/base.rb @@ -129,6 +129,33 @@ def save_to_file(filename, content, verbatim: false) end end + apipie :method, 'Generates a shell command to append the given text into a file' do + desc "This is useful if some multiline string needs to be appended to some file on the hard disk. This + is typically used in provisioning or job templates, e.g. when ssh keys need to be appended to the + authorized_keys file. The content must end with a line end, if not an extra trailing line end is + appended automatically unless the content is empty. Note that, the file name or path is printed as + it is without any escaping even if it contains any whitespace or special charecters. In order to + escape the special charecters, process the file name using the shell_escape function." + required :filename, String, desc: 'The file path to append the content to' + required :content, String, desc: 'Content to be appended' + keyword :verbatim, [true, false], desc: 'Controls whether the file should be put on disk as-is or if variables should be replaced by shell before the file is written out', default: false + returns String, desc: 'String representing the shell command' + example "append_to_file('/etc/motd', \"hello\\nworld\\n\") # => 'cat << EOF-0e4f089a >> /etc/motd\\nhello\\nworld\\nEOF-0e4f089a'" + example "append_to_file(shell_escape('/tmp/a file with spaces'), nil) # => 'touch /tmp/a\ file\ with\ spaces'" + end + def append_to_file(filename, content, verbatim: false) + delimiter = 'EOF-' + Digest::SHA512.hexdigest(filename)[0..7] + if content.empty? + "touch #{filename}" + elsif verbatim + content = Base64.encode64(content) + "cat << #{delimiter} | base64 -d >> #{filename}\n#{content}#{delimiter}" + else + content += "\n" unless content.end_with?("\n") + "cat << #{delimiter} >> #{filename}\n#{content}#{delimiter}" + end + end + apipie :method, desc: 'Takes a block of code, runs it and prefixes the resulting text by given number of spaces' do desc "This is useful when rendering output is a whitespace sensitive format, such as YAML." required :count, Integer, desc: 'The number of spaces' diff --git a/app/services/foreman/renderer/scope/macros/helpers.rb b/app/services/foreman/renderer/scope/macros/helpers.rb index 837541b9295..09a57273393 100644 --- a/app/services/foreman/renderer/scope/macros/helpers.rb +++ b/app/services/foreman/renderer/scope/macros/helpers.rb @@ -100,6 +100,15 @@ def shell_escape(string) Shellwords.shellescape(string) end + apipie :method, "Expand file path to be able to safely process it using shell_escape" do + required :string, String, desc: 'File path to expand' + returns String + example "expand_path('~root/.ssh/authorized_keys') #=> '/root/.ssh/authorized_keys'" + end + def expand_path(path) + File.expand_path(path) + end + apipie :method, 'Returns current date' do keyword :format, String, desc: 'Format string to format date according to the directives in this string', default: '%F' returns String diff --git a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb index 083efa531ec..055f35b39bf 100644 --- a/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb +++ b/app/views/unattended/provisioning_templates/snippet/remote_execution_ssh_keys.erb @@ -54,9 +54,7 @@ if $user_exists; then mkdir -p <%= ssh_path %> - cat << EOF >> <%= ssh_path %>/authorized_keys -<%= host_param('remote_execution_ssh_keys').is_a?(String) ? host_param('remote_execution_ssh_keys') : host_param('remote_execution_ssh_keys').join("\n") %> -EOF +<%= append_to_file(shell_escape(expand_path("#{ssh_path}/authorized_keys")), host_param('remote_execution_ssh_keys').is_a?(String) ? host_param('remote_execution_ssh_keys') : host_param('remote_execution_ssh_keys').join("\n")) %> chmod 0700 <%= ssh_path %> chmod 0600 <%= ssh_path %>/authorized_keys