diff --git a/tests/foreman/installer/test_install_foremanctl.py b/tests/foreman/installer/test_install_foremanctl.py index 0ff85a79e75..cc7fdc15b86 100644 --- a/tests/foreman/installer/test_install_foremanctl.py +++ b/tests/foreman/installer/test_install_foremanctl.py @@ -29,6 +29,7 @@ pytestmark = [pytest.mark.foremanctl, pytest.mark.upgrade] FOREMANCTL_CERTS_DIR = '/var/lib/foremanctl/certs/certs' +CAPSULE_CERTS_DIR = '/var/lib/foremanctl/certs/hosts' def common_sat_install_assertions(satellite): @@ -550,3 +551,144 @@ def test_positive_foremanctl_tuning_profile(module_sat_foremanctl_tuning): parameters_file = sat.load_remote_yaml_file(FOREMANCTL_PARAMETERS_FILE) assert 'tuning' not in parameters_file assert_postgresql_tuning(sat, 'default') + + +def assert_cert_validity_days(host, cert_paths, expected_days): + """Assert each certificate has the expected validity period in days.""" + for cert_path in cert_paths: + assert get_cert_validity_days(host, cert_path) == expected_days + + +def foremanctl_host_cert_paths(hostname): + """Return server and client certificate paths for a foremanctl host.""" + return [ + f'{FOREMANCTL_CERTS_DIR}/{hostname}.crt', + f'{FOREMANCTL_CERTS_DIR}/{hostname}-client.crt', + ] + + +def foremanctl_capsule_cert_paths(hostname): + """Return server and client certificate paths for a foremanctl capsule bundle.""" + return [ + f'{CAPSULE_CERTS_DIR}/{hostname}/certs/{hostname}.crt', + f'{CAPSULE_CERTS_DIR}/{hostname}/certs/{hostname}-client.crt', + ] + + +DEFAULT_CERT_DAYS = 7300 +RENEWED_CERT_DAYS = 730 + + +@pytest.mark.parametrize('module_sat_ready_rhel', ['default'], indirect=True) +@pytest.mark.rhel_ver_match('9') +def test_positive_foremanctl_certificate_bundle(module_sat_ready_rhel, rhel_contenthost): + """Verify foremanctl certificate-bundle generation and renewal for a capsule. + + :id: 9bd1d8d8-a22a-4917-9522-22b7165d224c + + :steps: + 1. Deploy Satellite with foremanctl using default certificates + 2. Generate a certificate bundle for the capsule host via + foremanctl certificate-bundle + 3. Verify the certificate bundle tarball is created at the expected path + 4. Verify Satellite and capsule server/client certificates have default + validity (7300 days) + 5. Renew Satellite certificates with foremanctl deploy --certificate-renew + --certificate-validity-days 730 + 6. Verify Satellite server/client cert validity is updated to 730 days and + capsule certificates remain at 7300 days + 7. Verify capsule server and client certificates are signed by the CA + 8. Capture SHA256 fingerprints of the CA, capsule server, and capsule + client certificates + 9. Renew capsule certificates with foremanctl certificate-bundle + --certificate-renew + 10. Verify capsule server/client cert validity is updated to 730 days + 11. Verify CA fingerprint is unchanged after capsule renewal + 12. Verify capsule server and client certificate fingerprints changed + 13. Verify renewed capsule certificates still chain to the same CA + + :expectedresults: + 1. foremanctl certificate-bundle succeeds and creates the tarball + 2. Initial validity periods are 7300 days for both Satellite and capsule + certificates + 3. Satellite renewal updates only Satellite server and client certificates + 4. Capsule certificates validate against the CA before capsule renewal + 5. Capsule bundle renewal updates only capsule server and client certificates + 6. CA identity is preserved across capsule renewal (fingerprint unchanged) + 7. Capsule server and client certificates are regenerated (fingerprints change) + 8. Renewed capsule certificates maintain chain integrity against the same CA + + :verifies: SAT-43475 + """ + sat = module_sat_ready_rhel + ca_cert = f'{FOREMANCTL_CERTS_DIR}/ca.crt' + capsule = rhel_contenthost + + foremanctl_cert_bundle_tarball = f'/var/lib/foremanctl/certs/bundles/{capsule.hostname}.tar.gz' + sat_certs = foremanctl_host_cert_paths(sat.hostname) + capsule_certs = foremanctl_capsule_cert_paths(capsule.hostname) + + # Generate bundle + deploy = sat.execute(f'foremanctl certificate-bundle {capsule.hostname}', timeout='10m') + assert deploy.status == 0, f'foremanctl certificate-bundle failed:\n{deploy.stderr}' + # verify tarball + result = sat.execute(f'tar tzf {foremanctl_cert_bundle_tarball}') + assert result.status == 0, ( + f'Certificate bundle tarball not found at {foremanctl_cert_bundle_tarball}: {result.stderr}' + ) + + # Phase 1: initial Satellite validity + assert_cert_validity_days(sat, sat_certs, DEFAULT_CERT_DAYS) + assert_cert_validity_days(sat, capsule_certs, DEFAULT_CERT_DAYS) + + # Phase 2: renew Satellite certs — capsule should be unchanged + result = sat.execute( + f'foremanctl deploy --certificate-renew --certificate-validity-days {RENEWED_CERT_DAYS}', + timeout='30m', + ) + assert result.status == 0, f'Satellite certificate renewal failed: {result.stderr}' + + assert_cert_validity_days(sat, sat_certs, RENEWED_CERT_DAYS) + assert_cert_validity_days(sat, capsule_certs, DEFAULT_CERT_DAYS) + + # Verify certificate chain — capsule server and client signed by CA + for cert in capsule_certs: + verify = sat.execute(f'openssl verify -CAfile {ca_cert} {cert}') + assert verify.status == 0, f'Chain validation failed for {cert}: {verify.stderr}' + + # Capture fingerprints to track capsule certificate identity across renewal + server_fp_before = get_cert_fingerprint(sat, capsule_certs[0]) + client_fp_before = get_cert_fingerprint(sat, capsule_certs[1]) + ca_fp_before = get_cert_fingerprint(sat, ca_cert) + + # Phase 3: renew capsule bundle — capsule certs should change + result = sat.execute( + f'foremanctl certificate-bundle --certificate-renew {capsule.hostname}', + timeout='5m', + ) + assert result.status == 0, f'Capsule certificate renewal failed: {result.stderr}' + + assert_cert_validity_days(sat, capsule_certs, RENEWED_CERT_DAYS) + + # Renewed certs must still chain to the same CA + for cert in capsule_certs: + verify = sat.execute(f'openssl verify -CAfile {ca_cert} {cert}') + assert verify.status == 0, ( + f'Chain validation failed after renewal for {cert}: {verify.stderr}' + ) + + # Capsule server/client fingerprints must differ + server_fp_after = get_cert_fingerprint(sat, capsule_certs[0]) + assert server_fp_after != server_fp_before, ( + 'Capsule server cert fingerprint unchanged — renewal did not regenerate it' + ) + client_fp_after = get_cert_fingerprint(sat, capsule_certs[1]) + assert client_fp_after != client_fp_before, ( + 'Capsule client cert fingerprint unchanged — renewal did not regenerate it' + ) + + # CA certificate fingerprint must be unchanged (CA not regenerated) + ca_fp_after = get_cert_fingerprint(sat, ca_cert) + assert ca_fp_after == ca_fp_before, ( + 'CA fingerprint changed after --certificate-renew; CA should not be regenerated' + )