Skip to content

SSH CA cert feature SAT-28038#20453

Merged
lhellebr merged 9 commits intoSatelliteQE:masterfrom
lhellebr:ssh_ca_cert
Feb 12, 2026
Merged

SSH CA cert feature SAT-28038#20453
lhellebr merged 9 commits intoSatelliteQE:masterfrom
lhellebr:ssh_ca_cert

Conversation

@lhellebr
Copy link
Contributor

@lhellebr lhellebr commented Dec 9, 2025

No description provided.

@lhellebr lhellebr requested a review from adamlazik1 December 9, 2025 14:41
@lhellebr lhellebr requested a review from a team as a code owner December 9, 2025 14:41
@lhellebr lhellebr added the No-CherryPick PR doesnt need CherryPick to previous branches label Dec 9, 2025
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • In register_host, lce and cv are treated as objects with .id attributes but are initialized from cli_factory helpers (which usually return dict-like data); consider consistently using either dict keys or attributes (e.g. lce['id'] / cv['id']) to avoid type/attribute mismatches.
  • The three test_positive_ssh_ca_* tests share substantial setup and assertion logic (CA creation, host registration, invocation, and log checks); consider extracting common helpers/fixtures to reduce duplication and make intent clearer.
  • When constructing InstallerCommand for foreman_proxy_plugin_remote_execution_script_ssh_host_ca_public_key, you wrap the certificate value in extra quotes (f'"{host_ca_cert}"'); consider passing the raw value and letting InstallerCommand handle quoting to avoid subtle shell-escaping issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `register_host`, `lce` and `cv` are treated as objects with `.id` attributes but are initialized from `cli_factory` helpers (which usually return dict-like data); consider consistently using either dict keys or attributes (e.g. `lce['id']` / `cv['id']`) to avoid type/attribute mismatches.
- The three `test_positive_ssh_ca_*` tests share substantial setup and assertion logic (CA creation, host registration, invocation, and log checks); consider extracting common helpers/fixtures to reduce duplication and make intent clearer.
- When constructing `InstallerCommand` for `foreman_proxy_plugin_remote_execution_script_ssh_host_ca_public_key`, you wrap the certificate value in extra quotes (`f'"{host_ca_cert}"'`); consider passing the raw value and letting `InstallerCommand` handle quoting to avoid subtle shell-escaping issues.

## Individual Comments

### Comment 1
<location> `tests/foreman/destructive/test_remoteexecution.py:249-148` </location>
<code_context>
+    )
+
+
+def log_compare(satellite, host):
+    return host.execute(
+        f'[ $(( $(cat /root/saved_sshd_log) + 1 )) -eq $(journalctl -u sshd | grep {satellite.ip} | grep " CA " | wc -l) ]'
+    ).status
+
+
</code_context>

<issue_to_address>
**suggestion:** Clarify and/or assert more explicitly what `log_compare` is checking to avoid fragile log-count based assertions

This helper assumes `journalctl | grep {satellite.ip} | grep " CA " | wc -l` increases by exactly 1 between `log_save` and the job run, which is brittle: concurrent SSH activity in shared environments can match the same pattern, and log rotation/truncation can change counts independently of this test.

Please consider either matching a more specific pattern (e.g., including a unique test-run identifier) or parsing `journalctl` output and asserting that at least one new relevant line appears, rather than enforcing an exact count change. If you keep this approach, add a brief comment showing the expected `sshd` log line so the dependency is explicit.

Suggested implementation:

```python
def log_save(satellite, host):
    # We track sshd log entries for CA-based publickey auth from the satellite IP.
    # Expected log line (simplified example):
    #   "Accepted publickey for <user> from <satellite_ip> ... ssh2: RSA-CERT ID ... CA ..."
    host.execute(
        f'journalctl -u sshd | grep "{satellite.ip}" | grep "Accepted publickey" | grep " CA " | wc -l > /root/saved_sshd_log'
    )

```

```python
def log_compare(satellite, host):
    """Check that at least one new CA-based sshd log entry appears for the satellite since log_save().

    Compares the current count of sshd log lines matching:
        satellite.ip + "Accepted publickey" + " CA "
    with the count stored by log_save(). Returns the exit status of the check command
    (0 for success, non-zero for failure).
    """
    return host.execute(
        f'[ $(journalctl -u sshd | grep "{satellite.ip}" | grep "Accepted publickey" | grep " CA " | wc -l) -gt $(cat /root/saved_sshd_log) ]'
    ).status


from robottelo.config import get_credentials

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@lhellebr
Copy link
Contributor Author

lhellebr commented Jan 2, 2026

Ready but:

  1. feature not merged
  2. can't run PRT or even local tests due to 1)

Feel free to review but do not merge yet.

@lhellebr lhellebr marked this pull request as ready for review January 2, 2026 16:26
@lhellebr
Copy link
Contributor Author

lhellebr commented Jan 2, 2026

TODO: provisioning coverage (possibly in a separate PR) once I get access to provisioning infra

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The test_execution helper returns the result of satellite.cli.JobInvocation.info, which in other tests is treated as a dict (result['success']), but in test_positive_ssh_ca_sat_only you assert result.status == 0; this inconsistency is likely a bug and should be aligned to a single, correct return type/usage.
  • There is a lot of duplicated shell command and path logic (e.g., repeated ssh-keygen invocations, restorecon/chown/chgrp sequences, and path string munging with split('/')) across fixtures and tests; consider extracting small reusable helpers to reduce duplication and make the setup steps easier to read and maintain.
  • Functions like log_compare return a bare .status integer and the tests then compare that to 0/!=0, which makes the intent a bit opaque; wrapping this in a boolean-returning helper (e.g., used_ca_authentication(host, sat) -> bool) would make the assertions more self-explanatory.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `test_execution` helper returns the result of `satellite.cli.JobInvocation.info`, which in other tests is treated as a dict (`result['success']`), but in `test_positive_ssh_ca_sat_only` you assert `result.status == 0`; this inconsistency is likely a bug and should be aligned to a single, correct return type/usage.
- There is a lot of duplicated shell command and path logic (e.g., repeated `ssh-keygen` invocations, `restorecon/chown/chgrp` sequences, and path string munging with `split('/')`) across fixtures and tests; consider extracting small reusable helpers to reduce duplication and make the setup steps easier to read and maintain.
- Functions like `log_compare` return a bare `.status` integer and the tests then compare that to 0/!=0, which makes the intent a bit opaque; wrapping this in a boolean-returning helper (e.g., `used_ca_authentication(host, sat) -> bool`) would make the assertions more self-explanatory.

## Individual Comments

### Comment 1
<location> `tests/foreman/destructive/test_remoteexecution.py:281` </location>
<code_context>
+    register_host(sat, host)
+    result = test_execution(sat, host)
+    # assert the run actually happened and it was authenticated using cert
+    assert result.status == 0
+    logger.debug(result)
+    assert log_compare(sat, host) == 0
</code_context>

<issue_to_address>
**issue (bug_risk):** JobInvocation result is treated as an object with `.status` but other tests use it as a dict, which is likely to break this test

Here `test_execution` returns `satellite.cli.JobInvocation.info(...)`, which is treated elsewhere in this file as a dict (`result['success'] == '1'`). Using `result.status` is inconsistent and may raise an `AttributeError` or behave incorrectly. Please align with the other tests and assert on the appropriate dict fields (e.g. `result['success']` and/or `result['overall']`) instead of `.status`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

register_host(sat, host)
result = test_execution(sat, host)
# assert the run actually happened and it was authenticated using cert
assert result.status == 0
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): JobInvocation result is treated as an object with .status but other tests use it as a dict, which is likely to break this test

Here test_execution returns satellite.cli.JobInvocation.info(...), which is treated elsewhere in this file as a dict (result['success'] == '1'). Using result.status is inconsistent and may raise an AttributeError or behave incorrectly. Please align with the other tests and assert on the appropriate dict fields (e.g. result['success'] and/or result['overall']) instead of .status.

@lhellebr
Copy link
Contributor Author

trigger: test-robottelo
pytest: tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_sat_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_host_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_host_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_and_host_ssh_ansible_cockpit

@Satellite-QE
Copy link
Collaborator

PRT Result

Build Number: 14058
Build Status: SUCCESS
PRT Comment: pytest tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_sat_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_host_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_host_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_and_host_ssh_ansible_cockpit --external-logging
Test Result : ================= 5 passed, 52 warnings in 6898.70s (1:54:58) ==================

@Satellite-QE Satellite-QE added the PRT-Passed Indicates that latest PRT run is passed for the PR label Jan 19, 2026
Copy link
Contributor

@rmynar rmynar left a comment

Choose a reason for hiding this comment

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

I had to stare at the fixtures for a while to understand them so a brief description would be nice.
Overall ACK, just some non-blocking comment in code

cert_name = f'{key_name}-cert.pub'
assert (
rhel_contenthost.execute(
f'cd {host_ssh_path} && if ! [ -f ssh_host_ed25519_key ]; then ssh-keygen -t ed25519 -f {key_name} -N ""; fi'
Copy link
Contributor

Choose a reason for hiding this comment

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

f'cd {host_ssh_path} && if ! [ -f {key_name} ]; then ssh-keygen -t ed25519 -f {key_name} -N ""; fi'

@Satellite-QE Satellite-QE removed the PRT-Passed Indicates that latest PRT run is passed for the PR label Jan 26, 2026
@lhellebr lhellebr requested a review from rmynar January 26, 2026 10:39
Copy link
Contributor

@adamlazik1 adamlazik1 left a comment

Choose a reason for hiding this comment

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

I apologize for not taking a look earlier, it somehow slipped my mind. Left some thoughts.

@pytest.mark.no_containers
@pytest.mark.rhel_ver_match([settings.content_host.default_rhel_version])
def test_positive_ssh_ca_sat_only(ca_sat, rhel_contenthost):
"""Setup Satellite's SSH key's cert, register host and run REX on that host
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"""Setup Satellite's SSH key's cert, register host and run REX on that host
"""Setup Satellite's SSH cert, register host and run REX on that host

Applies to other tests as well.


:id: 353a21bf-f379-440a-9dc6-e17bf6414713

:expectedresults: Verify the job has been run successfully against the host, Sat's cert hasn't been added to host's authorized_keys and CA verification has been used instead
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
:expectedresults: Verify the job has been run successfully against the host, Sat's cert hasn't been added to host's authorized_keys and CA verification has been used instead
:expectedresults: Verify the job has been run successfully against the host, Sat's public key hasn't been added to host's authorized_keys and CA verification has been used instead

Comment on lines 247 to 253
def log_save(satellite, host):
"""Save a number of lines mentioning CA was used in sshd log,
for later use
"""
host.execute(
f'journalctl -u sshd | grep {satellite.ip_addr} | grep CA | wc -l > /root/saved_sshd_log'
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest more straightforward approach as opposed to saving a value to a file:

Suggested change
def log_save(satellite, host):
"""Save a number of lines mentioning CA was used in sshd log,
for later use
"""
host.execute(
f'journalctl -u sshd | grep {satellite.ip_addr} | grep CA | wc -l > /root/saved_sshd_log'
)
def log_count(satellite, host):
"""Return number of lines mentioning CA was used in sshd log."""
result = host.execute(
f'journalctl -u sshd | grep {satellite.ip_addr} | grep CA | wc -l'
)
return int(result.stdout.strip())

Then instead of log_compare I would simply do:

saved_count = log_count(sat, host)
assert log_count(sat, host) == saved_count + 1

In my eyes it makes the tests a bit more readable. Also probably a little more resource efficient since we aren't creating, writing to, and reading a file.

Copy link
Contributor

@pondrejk pondrejk Feb 4, 2026

Choose a reason for hiding this comment

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

also, isn't there a risk that tests will overwrite this file for each other if run concurrently? I suggest trying out pytest session with -n4 or something. If you decide to use the files, I suggest to introduce some test-specific naming to it. Adam's suggestion is also good but I'm afraid both suggestions don't fully cover the concurrency concern, because still there is a chance the log count will be for example +2 if other test did something meanwhile. Isn't there another way to verify success apart from checking logs? Checking task details maybe?

Copy link
Contributor

@adamlazik1 adamlazik1 Feb 5, 2026

Choose a reason for hiding this comment

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

Sadly I think the sshd logs should be verified. Otherwise there is a risk that the job passes using plain public key authentication. That shouldn't happen in the current implementation but if we don't test it then regressions can arise in the future. But wouldn't concurrent tests be run on different hosts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course I'll reimplement this to not use files, that was a leftover from testing I missed.

I agree with Adam, it's necessary to look at the logs.

host_ca_file_local = f'/tmp/{gen_string("alpha")}'
host.get(host_path, host_ca_file_local)
satellite.put(host_ca_file_local, satellite_path)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd move the fixtures above the first test in the module

return f'/var/lib/foreman-proxy/ssh/{ca_contenthost[1].split("/")[-1]}'


def register_host(satellite, host, cockpit=False):
Copy link
Contributor

Choose a reason for hiding this comment

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

bit puzzled about the purpose of this fixture. Is it required by your scenarios to create a separate organization for each registration? If not, then you could use module_org fixture. Also not sure about the cockpit condition, if we run this against the same ca_contenthost multiple times, it is possible that the cockpit repos will be enabled on the machine from the previous runs so cockpit=False is not a 100% guarantee. If separate orgs and strict separation of cockpit/non-cockpit hosts aren't a strong requirement I guess we could just do satellite.register_host_custom_repo with module_org in every tests and setup cockpit repos in the one test that actually requires that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note this is not a fixture, it needs to run as part of test.

Note these are destructive tests so each of them should get their own Satellite, so no interference.

It's probably possible to setup cockpit everywhere, but that would do the setup steps (including repo sync) everywhere, even if it won't be used. The code will be a line shorter but it will be expensive, I would keep the condition.

And ok, I will save the org creation step and use function_org instead.

@lhellebr
Copy link
Contributor Author

trigger: test-robottelo
pytest: tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_sat_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_host_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_host_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_and_host_ssh_ansible_cockpit

@Satellite-QE
Copy link
Collaborator

PRT Result

Build Number: 14246
Build Status: SUCCESS
PRT Comment: pytest tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_sat_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_host_only tests/foreman/destructive/test_remoteexecution.py::test_negative_ssh_ca_host_wrong_cert tests/foreman/destructive/test_remoteexecution.py::test_positive_ssh_ca_sat_and_host_ssh_ansible_cockpit --external-logging
Test Result : ================= 5 passed, 52 warnings in 6943.88s (1:55:43) ==================

@Satellite-QE Satellite-QE added the PRT-Passed Indicates that latest PRT run is passed for the PR label Feb 10, 2026
@lhellebr lhellebr requested a review from pondrejk February 11, 2026 11:02
@lhellebr lhellebr requested a review from adamlazik1 February 11, 2026 11:02
@Satellite-QE Satellite-QE removed the PRT-Passed Indicates that latest PRT run is passed for the PR label Feb 11, 2026
Comment on lines +247 to +249
"""Return number of lines mentioning CA was used in sshd log,
for later use
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick, non-blocking

Suggested change
"""Return number of lines mentioning CA was used in sshd log,
for later use
"""
"""Return number of lines mentioning CA was used in sshd log"""

@lhellebr lhellebr merged commit 7bb320c into SatelliteQE:master Feb 12, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

No-CherryPick PR doesnt need CherryPick to previous branches

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants