Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #37601 - Add Foreman CA refresh template #10208

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

PUBLIC_TEMPLATE_KIND_NAME = 'public'

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_public_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_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]
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"),
"public" => N_("Templates accessible publicly"),
}
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"),
"public" => N_("Templates from this category can be accessed publicly 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: 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') -%>
Original file line number Diff line number Diff line change
@@ -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 -%>
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually do this today? I don't see why we need to.

When we use custom certificates (like when Let's Encrypt is used as a CA) it should already be in the certificate store.

Perhaps this should also check if $CA_TRUST_ANCHORS/katello-server-ca.pem exists.

Copy link
Member

@ares ares Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the katello-rhsm-consumer script does it for some reason today

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
10 changes: 10 additions & 0 deletions test/controllers/unattended_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
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

public:
name: public
description: description for public template
16 changes: 16 additions & 0 deletions test/fixtures/templates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions test/models/host_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
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 @@ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
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
Loading
Loading