From e9f737f2827f4b40f9b64825cad72ad36911378c Mon Sep 17 00:00:00 2001 From: Lukas Zapletal Date: Mon, 24 Aug 2026 09:28:07 +0200 Subject: [PATCH] pkg/rhsm: use Katello CA as default on Satellite hosts On Satellite-registered hosts, content is served through the Satellite content proxy whose TLS certificate is signed by the Katello CA. The default redhat-uep.pem only contains the Red Hat CDN CA and cannot validate these certificates, causing TLS failures when fetching packages from RHSM-backed repositories. When /etc/rhsm/ca/katello-server-ca.pem exists (installed by Satellite registration), use it as the default CA. This single-point fix works across all download backends (curl, librepo, dnf) since they all read the default CA from the same Subscriptions class. A repo's explicit sslcacert still takes precedence. A hybrid approach combining the RHSM CA and the system CA bundle was rejected because the three download backends configure CA certs differently (librepo has no LRO_SSLCAPATH, dnf does not expose sslcapath), making a reliable cross-backend setup fragile. Mirrors osbuild PR #2537. SAT-37848 Co-Authored-By: Claude Opus 4.8 --- pkg/rhsm/secrets.go | 21 ++++++++++++++++++++- pkg/rhsm/secrets_test.go | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/rhsm/secrets.go b/pkg/rhsm/secrets.go index 5542618c1f..fa8730ce09 100644 --- a/pkg/rhsm/secrets.go +++ b/pkg/rhsm/secrets.go @@ -42,6 +42,25 @@ type ConsumerSecrets struct { ConsumerCert string } +// defaultCACert picks the CA certificate to use when a repo does not set +// sslcacert explicitly. On Satellite-registered hosts content is served through +// the Satellite content proxy whose TLS certificate is signed by the Katello CA; +// the default redhat-uep.pem only holds the Red Hat CDN CA and cannot validate +// it. When katello-server-ca.pem is present (installed by Satellite +// registration) it contains the CA chain needed for the content proxy, so prefer +// it; otherwise fall back to redhat-uep.pem. A repo's explicit sslcacert still +// takes precedence over this default (see GetSecretsForBaseurl). +// katelloCACert is a variable so tests can point it at a temp file; production +// code never reassigns it. +var katelloCACert = "/etc/rhsm/ca/katello-server-ca.pem" + +func defaultCACert() string { + if _, err := os.Stat(katelloCACert); err == nil { + return katelloCACert + } + return "/etc/rhsm/ca/redhat-uep.pem" +} + func getRHSMSecrets() (*RHSMSecrets, error) { // search /etc first to allow container users to override the entitlements globs := []string{ @@ -58,7 +77,7 @@ func getRHSMSecrets() (*RHSMSecrets, error) { cert := strings.TrimSuffix(key, "-key.pem") + ".pem" if _, err := os.Stat(cert); err == nil { return &RHSMSecrets{ - SSLCACert: "/etc/rhsm/ca/redhat-uep.pem", + SSLCACert: defaultCACert(), SSLClientKey: key, SSLClientCert: cert, }, nil diff --git a/pkg/rhsm/secrets_test.go b/pkg/rhsm/secrets_test.go index 3be7f7a345..3f6ca0b3d8 100644 --- a/pkg/rhsm/secrets_test.go +++ b/pkg/rhsm/secrets_test.go @@ -1,6 +1,8 @@ package rhsm import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -132,6 +134,23 @@ func TestGetSecretsForBaseurlFallback(t *testing.T) { assert.Equal(t, "/etc/pki/entitlement/fallback-key.pem", secrets.SSLClientKey) } +// defaultCACert prefers the Katello CA when it exists (Satellite-registered +// host) and otherwise falls back to redhat-uep.pem. +func TestDefaultCACert(t *testing.T) { + orig := katelloCACert + t.Cleanup(func() { katelloCACert = orig }) + + // Katello CA absent: fall back to the Red Hat CDN CA. + katelloCACert = filepath.Join(t.TempDir(), "does-not-exist.pem") + assert.Equal(t, "/etc/rhsm/ca/redhat-uep.pem", defaultCACert()) + + // Katello CA present: prefer it. + present := filepath.Join(t.TempDir(), "katello-server-ca.pem") + require.NoError(t, os.WriteFile(present, []byte("ca"), 0600)) + katelloCACert = present + assert.Equal(t, present, defaultCACert()) +} + // mirrors the cases in osbuild's test_util_rhsm.py::TestUrlMatching. func TestBaseurlToRegex(t *testing.T) { for _, tc := range []struct {