diff --git a/app/controllers/unattended_controller.rb b/app/controllers/unattended_controller.rb index 08e7605f6ed..c039c07c8ce 100644 --- a/app/controllers/unattended_controller.rb +++ b/app/controllers/unattended_controller.rb @@ -19,6 +19,8 @@ class UnattendedController < ApplicationController # Maximum size of built/failed request body accepted to prevent DoS (in bytes) MAX_BUILT_BODY = 65535 + PUBLIC_TEMPLATE_KIND_NAME = 'public' + def built return unless verify_found_host return head(:method_not_allowed) unless allowed_to_install? @@ -60,6 +62,7 @@ def host_template return head(:not_found) unless kind.present? return if render_ipxe_template + return if render_public_template(kind, params[:id]) return unless verify_found_host return head(:method_not_allowed) unless allowed_to_install? @@ -86,6 +89,14 @@ def render_error(message, options) end end + def render_public_template(kind, name) + return false unless kind == PUBLIC_TEMPLATE_KIND_NAME + + template = ProvisioningTemplate.joins(:template_kind).find_by(name: name, template_kinds: { name: kind }) + + render_template(template: template, type: kind) + end + def render_intermediate_template ipxe_template_kind = TemplateKind.find_by(name: 'iPXE') name = Setting[:intermediate_ipxe_script] @@ -159,7 +170,7 @@ def load_host_details @host = Foreman::UnattendedInstallation::HostFinder.new(query_params: query_params).search end - def verify_found_host + def verify_found_host(needs_token = true) host_verifier = Foreman::UnattendedInstallation::HostVerifier.new(@host, request_ip: request.remote_ip, for_host_template: (action_name == 'host_template')) diff --git a/app/models/host_info_providers/static_info.rb b/app/models/host_info_providers/static_info.rb index 350b723959f..e48c1f3bd2a 100644 --- a/app/models/host_info_providers/static_info.rb +++ b/app/models/host_info_providers/static_info.rb @@ -14,6 +14,7 @@ def host_info add_taxonomy_params param add_domain_params param add_login_params param + add_certificate_params param # Parse ERB values contained in the parameters param = ParameterSafeRender.new(self).render(param) @@ -56,6 +57,21 @@ def add_network_params(param) param['foreman_interfaces'] = host.interfaces.map(&:to_export) end + def add_certificate_params(param) + param['server_ca'] = read_cert(Setting[:server_ca_file]) + param['ssl_ca'] = read_cert(Setting[:ssl_ca_file]) + end + + def read_cert(path) + return nil unless path + + File.read(path) + rescue StandardError => e + Foreman::Logging.logger('app').warn("Failed to read CA file: #{e}") + + nil + end + def all_subnets host.interfaces.map { |i| [i.subnet, i.subnet6] }.flatten.compact end diff --git a/app/models/template_kind.rb b/app/models/template_kind.rb index f5b3a140f3e..19d6c9bdc3c 100644 --- a/app/models/template_kind.rb +++ b/app/models/template_kind.rb @@ -27,6 +27,7 @@ def self.default_template_labels "registration" => N_("Registration template"), "kexec" => N_("Discovery Kexec"), "Bootdisk" => N_("Boot disk"), + "public" => N_("Templates accessible publicly"), } end @@ -44,6 +45,7 @@ def self.default_template_descriptions "POAP" => N_("Provisioning for switches running NX-OS."), "cloud-init" => N_("Template for cloud-init unattended endpoint."), "host_init_config" => N_("Contains the instructions in form of a bash script for the initial host configuration, after the host is registered in Foreman"), + "public" => N_("Templates from this category can be accessed publicly using the /unattended endpoint."), } end diff --git a/app/services/foreman/unattended_installation/host_verifier.rb b/app/services/foreman/unattended_installation/host_verifier.rb index b85457038c4..166e81c853a 100644 --- a/app/services/foreman/unattended_installation/host_verifier.rb +++ b/app/services/foreman/unattended_installation/host_verifier.rb @@ -3,12 +3,13 @@ module UnattendedInstallation class HostVerifier attr_reader :errors, :host, :request_ip, :for_host_template, :controller_name - def initialize(host, request_ip:, for_host_template:) + def initialize(host, request_ip:, for_host_template:, needs_token: true) @host = host @errors = [] @for_host_template = for_host_template @request_ip = request_ip @controller_name = 'unattended' + @needs_token = needs_token end def valid? @@ -25,6 +26,7 @@ def valid? # In case the token expires during installation # Only relevant when the verifier is being used with `for_host_template` def valid_host_token? + return true unless @needs_token return true unless for_host_template return true unless @host&.token_expired? diff --git a/app/views/unattended/provisioning_templates/registration/foreman_ca_refresh.erb b/app/views/unattended/provisioning_templates/registration/foreman_ca_refresh.erb new file mode 100644 index 00000000000..ae1b41afd47 --- /dev/null +++ b/app/views/unattended/provisioning_templates/registration/foreman_ca_refresh.erb @@ -0,0 +1,17 @@ +<%# +kind: public +name: foreman_ca_refresh +model: ProvisioningTemplate +oses: +- AlmaLinux +- CentOS +- CentOS_Stream +- Fedora +- RedHat +- Rocky +description: | + This template is used to refresh foreman CA certificates on Katello-registered hosts +-%> +#!/bin/sh + +<%= snippet('ca_registration') -%> diff --git a/app/views/unattended/provisioning_templates/registration/foreman_raw_ca.erb b/app/views/unattended/provisioning_templates/registration/foreman_raw_ca.erb new file mode 100644 index 00000000000..74d535b2061 --- /dev/null +++ b/app/views/unattended/provisioning_templates/registration/foreman_raw_ca.erb @@ -0,0 +1,15 @@ +<%# +kind: public +name: foreman_raw_ca +model: ProvisioningTemplate +oses: +- AlmaLinux +- CentOS +- CentOS_Stream +- Fedora +- RedHat +- Rocky +description: | + This template exposes the CA certificate to be consumed directly from a URL. +-%> +<%= foreman_server_ca_cert -%> diff --git a/app/views/unattended/provisioning_templates/snippet/ca_registration.erb b/app/views/unattended/provisioning_templates/snippet/ca_registration.erb new file mode 100644 index 00000000000..e391b16d530 --- /dev/null +++ b/app/views/unattended/provisioning_templates/snippet/ca_registration.erb @@ -0,0 +1,34 @@ +<%# +kind: snippet +name: ca_registration +model: ProvisioningTemplate +snippet: true +description: | + This template is used for updating Foreman's CA on hosts that are registered by Katello. + It replaces the CA used by subscription-manager and adds the CA to trusted anchors. +-%> + +<% if plugin_present?('katello') -%> + # Define the path to the Katello server CA certificate + KATELLO_SERVER_CA_CERT=/etc/rhsm/ca/katello-server-ca.pem + + # If katello ca cert file exists on host, update it and make sure it's in trust anchors + if [ -f "$KATELLO_SERVER_CA_CERT" ]; then + <%= save_to_file('"$KATELLO_SERVER_CA_CERT"', foreman_server_ca_cert) -%> + + if [ -f /etc/debian_version ]; then + CA_TRUST_ANCHORS=/usr/local/share/ca-certificates/ + else + CA_TRUST_ANCHORS=/etc/pki/ca-trust/source/anchors + fi + + # Add the Katello CA certificate to the system-wide CA certificate store + cp $KATELLO_SERVER_CA_CERT $CA_TRUST_ANCHORS + + if [ -f /etc/debian_version ]; then + update-ca-certificates + else + update-ca-trust + fi + fi +<% end -%> diff --git a/app/views/unattended/provisioning_templates/snippet/subscription_manager_setup.erb b/app/views/unattended/provisioning_templates/snippet/subscription_manager_setup.erb index 92f73b5aa05..182f234e714 100644 --- a/app/views/unattended/provisioning_templates/snippet/subscription_manager_setup.erb +++ b/app/views/unattended/provisioning_templates/snippet/subscription_manager_setup.erb @@ -24,17 +24,6 @@ RHSM_CFG=/etc/rhsm/rhsm.conf <% end -%> <% if plugin_present?('katello') -%> - # Define the path to the Katello server CA certificate - KATELLO_SERVER_CA_CERT=/etc/rhsm/ca/katello-server-ca.pem - - # If SSL_CA_CERT is not set, create a temporary file for it - if [ -z "$SSL_CA_CERT" ]; then - SSL_CA_CERT=$(mktemp) - cat << EOF > "$SSL_CA_CERT" -<%= foreman_server_ca_cert %> -EOF - fi - <% if @subman_setup_scenario == 'registration' -%> # rhn-client-tools conflicts with subscription-manager package # since rhn tools replaces subscription-manager, we need to explicitly @@ -59,10 +48,15 @@ EOF <% end -%> <% end -%> + # Define the path to the Katello server CA certificate + KATELLO_SERVER_CA_CERT=/etc/rhsm/ca/katello-server-ca.pem + # Prepare the SSL certificate mkdir -p /etc/rhsm/ca - cp -f $SSL_CA_CERT $KATELLO_SERVER_CA_CERT + touch $KATELLO_SERVER_CA_CERT chmod 644 $KATELLO_SERVER_CA_CERT + + <%= snippet('ca_registration') -%> <% end -%> # Prepare subscription-manager @@ -133,25 +127,5 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi -<% if @subman_setup_scenario == 'provisioning' && plugin_present?('katello') -%> - if [ -f /etc/debian_version ]; then - CA_TRUST_ANCHORS=/usr/local/share/ca-certificates/ - else - CA_TRUST_ANCHORS=/etc/pki/ca-trust/source/anchors - fi - - # Add the Katello CA certificate to the system-wide CA certificate store - if [ -d $CA_TRUST_ANCHORS ]; then - if [ -f /etc/debian_version ]; then - cp $KATELLO_SERVER_CA_CERT $CA_TRUST_ANCHORS - update-ca-certificates - else - update-ca-trust enable - cp $KATELLO_SERVER_CA_CERT $CA_TRUST_ANCHORS - update-ca-trust - fi - fi -<% end -%> - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true diff --git a/test/controllers/unattended_controller_test.rb b/test/controllers/unattended_controller_test.rb index 077e8e9731c..baf5be27808 100644 --- a/test/controllers/unattended_controller_test.rb +++ b/test/controllers/unattended_controller_test.rb @@ -58,6 +58,16 @@ class UnattendedControllerTest < ActionController::TestCase assert_response :success end + test "should get a foreman CA refresh for a host" do + get :host_template, params: { kind: 'public', id: 'foreman_ca_refresh' } + assert_response :success + end + + test "should get raw foreman CA certificate" do + get :host_template, params: { kind: 'public', id: 'foreman_raw_ca' } + assert_response :success + end + test "should get a kickstart when IPv6 mapped IPv4 address is used" do @request.env["HTTP_X_FORWARDED_FOR"] = "::ffff:" + @rh_host.ip @request.env["REMOTE_ADDR"] = "127.0.0.1" diff --git a/test/fixtures/template_kinds.yml b/test/fixtures/template_kinds.yml index 27d1e73ae92..82da1981aa9 100644 --- a/test/fixtures/template_kinds.yml +++ b/test/fixtures/template_kinds.yml @@ -34,3 +34,7 @@ host_init_config: registration: name: registration description: description for registration template + +public: + name: public + description: description for public template diff --git a/test/fixtures/templates.yml b/test/fixtures/templates.yml index 96103a95b46..ec52c8a004a 100644 --- a/test/fixtures/templates.yml +++ b/test/fixtures/templates.yml @@ -187,3 +187,19 @@ host_init_config: operatingsystems: centos5_3, redhat locked: true type: ProvisioningTemplate + +foreman_ca_refresh: + name: foreman_ca_refresh + template: 'echo "Refreshing certificates"' + template_kind: public + operatingsystems: centos5_3, redhat + locked: true + type: ProvisioningTemplate + +foreman_raw_ca: + name: foreman_raw_ca + template: 'echo "Raw CA certificate"' + template_kind: public + operatingsystems: centos5_3, redhat + locked: true + type: ProvisioningTemplate diff --git a/test/models/host_test.rb b/test/models/host_test.rb index 5485276e4a1..784f82a9a5f 100644 --- a/test/models/host_test.rb +++ b/test/models/host_test.rb @@ -2325,6 +2325,38 @@ def to_managed! assert enc['parameters']['foreman_interfaces'].any? { |s| s['ip6'] == host.ip6 } end + test "#info ENC YAML exposes CA certificates" do + cert_path = Rails.root.join('test/static_fixtures/certificates/example.com.crt') + cert_2_path = Rails.root.join('test/static_fixtures/certificates/example2.com.crt') + cert_file_content = File.read(cert_path) + cert_2_file_content = File.read(cert_2_path) + + Setting[:server_ca_file] = cert_path + Setting[:ssl_ca_file] = cert_2_path + + host = FactoryBot.build(:host, :managed) + + enc = host.info + assert_kind_of Hash, enc + assert_equal cert_file_content, enc['parameters']['server_ca'] + assert_equal cert_2_file_content, enc['parameters']['ssl_ca'] + end + + test "#info ENC YAML works with wrong ca file paths" do + cert_path = Rails.root.join('test/static_fixtures/certificates/example.com.crt') + cert_2_path = Rails.root.join('test/static_fixtures/certificates/example2.com.crt') + + Setting[:server_ca_file] = cert_path + 'zzz' + Setting[:ssl_ca_file] = cert_2_path + 'zzz' + + host = FactoryBot.build(:host, :managed) + + enc = host.info + assert_kind_of Hash, enc + assert_nil enc['parameters']['server_ca'] + assert_nil enc['parameters']['ssl_ca'] + end + describe 'cloning' do test 'relationships are copied' do host = FactoryBot.create(:host, :with_parameter) diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/cloud-init/CloudInit_default.host4dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/cloud-init/CloudInit_default.host4dhcp.snap.txt index 19d0796a72b..4c886c2ddac 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/cloud-init/CloudInit_default.host4dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/cloud-init/CloudInit_default.host4dhcp.snap.txt @@ -121,7 +121,6 @@ runcmd: sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/finish/Kickstart_default_finish.host4dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/finish/Kickstart_default_finish.host4dhcp.snap.txt index 89e5d2fa38a..7592e5ea370 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/finish/Kickstart_default_finish.host4dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/finish/Kickstart_default_finish.host4dhcp.snap.txt @@ -106,7 +106,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4and6dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4and6dhcp.snap.txt index b64463f4a7c..7aeada5f473 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4and6dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4and6dhcp.snap.txt @@ -169,7 +169,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4dhcp.snap.txt index 262e69a1db2..2c3e75bc066 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4dhcp.snap.txt @@ -169,7 +169,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4static.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4static.snap.txt index 96226b67853..59a8e17e6e5 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4static.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host4static.snap.txt @@ -169,7 +169,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6dhcp.snap.txt index f26f0cdae93..e339f509d19 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6dhcp.snap.txt @@ -169,7 +169,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6static.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6static.snap.txt index d5be03c69b7..66dde21cc0d 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6static.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.host6static.snap.txt @@ -169,7 +169,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky8_dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky8_dhcp.snap.txt index a46702451a9..a076abbc8d4 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky8_dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky8_dhcp.snap.txt @@ -167,7 +167,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky9_dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky9_dhcp.snap.txt index 4e5fc5f8b3b..5e3698a72aa 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky9_dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/provision/Kickstart_default.rocky9_dhcp.snap.txt @@ -166,7 +166,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure, diff --git a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/user_data/Kickstart_default_user_data.host4dhcp.snap.txt b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/user_data/Kickstart_default_user_data.host4dhcp.snap.txt index 89e7708c986..dfc6a356553 100644 --- a/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/user_data/Kickstart_default_user_data.host4dhcp.snap.txt +++ b/test/unit/foreman/renderer/snapshots/ProvisioningTemplate/user_data/Kickstart_default_user_data.host4dhcp.snap.txt @@ -120,7 +120,6 @@ else sed -i "/baseurl/a $full_refresh_config" $RHSM_CFG fi - # Restart yggdrasild if installed and running systemctl try-restart yggdrasil >/dev/null 2>&1 || true # Avoid timeout accessing unreachable repo on air gapped infrastructure,