Skip to content

Commit

Permalink
Fixes #37601 - Add Foreman CA refresh template
Browse files Browse the repository at this point in the history
In this PR I am introducing a way to refresh CA certificate for Foreman
server. It will have the following parts:
 [X] Downloadable template to run directly on a server
 [ ] REX script template to be used with SSH REX provider
 [ ] REX Ansible template to be used with Ansible REX provider

All the ways would refresh `katello-server-ca.pem` file and refresh
CA root store accordingly.

Also added the certs to the ENC, so every ENC consumer would be able
to use them to refresh Foreman's CA on host.
  • Loading branch information
ShimShtein committed Jul 9, 2024
1 parent 58e8f94 commit 332b295
Show file tree
Hide file tree
Showing 21 changed files with 139 additions and 44 deletions.
13 changes: 12 additions & 1 deletion app/controllers/unattended_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class UnattendedController < ApplicationController
# Maximum size of built/failed request body accepted to prevent DoS (in bytes)
MAX_BUILT_BODY = 65535

ANONYMOUS_TEMPLATE_KIND_NAME = 'anonymous'

def built
return unless verify_found_host
return head(:method_not_allowed) unless allowed_to_install?
Expand Down Expand Up @@ -60,6 +62,7 @@ def host_template
return head(:not_found) unless kind.present?

return if render_ipxe_template
return if render_anonymous_template(kind, params[:id])

return unless verify_found_host
return head(:method_not_allowed) unless allowed_to_install?
Expand All @@ -86,6 +89,14 @@ def render_error(message, options)
end
end

def render_anonymous_template(kind, name)
return false unless kind == ANONYMOUS_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]
Expand Down Expand Up @@ -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'))

Expand Down
16 changes: 16 additions & 0 deletions app/models/host_info_providers/static_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/models/template_kind.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def self.default_template_labels
"registration" => N_("Registration template"),
"kexec" => N_("Discovery Kexec"),
"Bootdisk" => N_("Boot disk"),
"anonymous" => N_("Templates accessible anonymously"),
}
end

Expand All @@ -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"),
"anonymous" => N_("Templates from this category can be accessed anonymously using the /unattended endpoint."),
}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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?

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<%#
kind: anonymous
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') -%>
Original file line number Diff line number Diff line change
@@ -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 -%>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions test/controllers/unattended_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class UnattendedControllerTest < ActionController::TestCase
assert_response :success
end

test "should get a foreman CA refresh for a host" do
get :host_template, params: { kind: 'anonymous', id: 'foreman_ca_refresh' }
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"
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/template_kinds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ host_init_config:
registration:
name: registration
description: description for registration template

anonymous:
name: anonymous
description: description for anonymous template
8 changes: 8 additions & 0 deletions test/fixtures/templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,11 @@ host_init_config:
operatingsystems: centos5_3, redhat
locked: true
type: ProvisioningTemplate

foreman_ca_refresh:
name: foreman_ca_refresh
template: 'echo "Refreshing certificates"'
template_kind: anonymous
operatingsystems: centos5_3, redhat
locked: true
type: ProvisioningTemplate
32 changes: 32 additions & 0 deletions test/models/host_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 332b295

Please sign in to comment.