Skip to content
Open
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
142 changes: 142 additions & 0 deletions tests/foreman/installer/test_install_foremanctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This still does not validate the certificates inside the bundle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

that only verifies the bundle has a file called {hostname}.crt, not the content of the file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We already test the presence of the files in https://github.com/theforeman/foremanctl/blob/master/tests/certificate_bundle_test.py, no need to repeat that (IMHO)

The "did the bundle change (or not change) properly depending on user input" is a valuable test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, this is already verified in the unit tests, so we can skip it.


# 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'
)