OpenSSH Hardened Where sshd Reads It First
An sshd drop-in numbered 01, so it is read before the 50-redhat.conf that asks for X11 forwarding: root login off, passwords off, MaxAuthTries 4, idle timeouts. The live test proves the policy with the daemon rather than the file: a password login refused, a key login accepted, root refused, and the banner delivered before authentication. Original role, live-tested on Rocky Linux 10.
Verification
Live-testedReally deployed to a container sandbox, proven idempotent (a second run changes nothing), verified against the role’s assertions, then torn down.
Conformance
- Static validation (yamllint · ansible-lint)
Provenance
- SHA-256 checksum
- Signature (pending)
Functional
- Live-tested - applied, verified, destroyed
Last verified 2026-09-26 · podman 4.9.3 · ansible 2.21.4 · how we verify
Cite it in your README
badge · attributionPaste this beside the module in the repository that uses it. The badge is rendered from this artifact's verification record, so it reads live-tested because the record says so, and the link lands on this page.
[](https://www.iac-bazaar.com/catalog/ansible-ssh-hardening?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
Ansible role 1.0.0, live-tested on IaC Bazaar: [OpenSSH Hardened Where sshd Reads It First](https://www.iac-bazaar.com/catalog/ansible-ssh-hardening?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
```yaml
# OpenSSH Hardened Where sshd Reads It First: https://www.iac-bazaar.com/catalog/ansible-ssh-hardening (download from your IaC Bazaar account)
```Preview:
Documentation
ssh-hardening
An SSH policy that takes effect, proven by a refused login. The role writes
one drop-in numbered to be read before the distribution's own, checks the
configuration with sshd -t before anything is reloaded, and enables the unit EL
ships. The live test reads sshd -T for the effective policy and then attempts
three real logins: a password, a key, and root.
No download, and no version to pin. EL 10 packages OpenSSH, so the role installs it by name and manages the unit the distribution ships. What the role owns is the policy: who may log in, with what, for how long, and what is written down about it.
The number in the filename is the whole trick. sshd takes the FIRST
occurrence of a directive it reads, and EL ships
/etc/ssh/sshd_config.d/50-redhat.conf with X11Forwarding yes. A hardening
drop-in numbered above 50 is read second and silently loses; this one is
01-iacbazaar-ssh.conf. The live test asserts both halves together: that
forwarding is off in sshd -T, AND that the 50 file still says yes, so a future
rename could not pass quietly.
The algorithms are deliberately not here. EL's
40-redhat-crypto-policies.conf includes the system crypto policy's own
opensshserver.config, and sshd -T shows its ciphers, MACs and key exchange
list (post-quantum mlkem768x25519-sha256 among them on EL 10). Because this
role's file is read FIRST, a Ciphers line in it would override the entire
system policy from one drop-in. It sets none, and the live test asserts the
policy include is still in place. Change the algorithms with the crypto-policy
role, where the change applies to every service at once.
PasswordAuthentication no is not enough on its own. With PAM in the stack,
a password can still arrive through keyboard-interactive, so the role sets
KbdInteractiveAuthentication to match and the live test proves it by trying a
password login and reading the refusal.
A banner file is not a banner. Banner is what makes sshd read
/etc/issue.net; nothing else on EL reads that file. The live test asserts the
text arrives at a client that has not authenticated.
License
Commercial - IaC Bazaar EULA. (c) IaC Bazaar.
Usage code & full reference need an account
The complete copy-paste usage, the full input/output reference, and operational notes are free with an account - shown here and bundled in the download. Sign in and this section fills in.
- Variables
- Test
Related modules
ansible-aide
AIDE from AppStream on EL 10: a watched tree, a baseline database built once, and the oneshot unit and timer the package does not ship. The live test passes on an unchanged host, FAILS when a file appears inside a watched path and names it, ignores one inside an excluded path, and passes again once the change is gone. Original role, live-tested on Rocky Linux 10.
ansible-authelia
Authelia from the upstream release (sha256-verified) as a hardened system service on loopback with file users and sqlite; its secrets are generated once on the host and reach the service as AUTHELIA_*_FILE variables. The live test hashes a password with Authelia's hasher, logs in, sees a wrong password refused and an anonymous visitor sent to the portal. Original role, live-tested on Rocky 10.
ansible-bandit
bandit on EL 10 from PyPI into a venv of its own with a link in the PATH; the live test runs pip check, then scans a module that passes a string to subprocess.call with shell=True (B602, High, exit 1) and a clean module (exit 0). Pinned. Original role, live-tested on Rocky Linux 10.
ansible-boundary
boundary on EL 10 from releases.hashicorp.com; SHA256SUMS is signature-checked against HashiCorp's key before get_url trusts it, the live test re-checks both, then runs authenticate against a dead address ('connection refused') and database init with a throwaway config (parsed, root key loaded, then the database refused). Pinned. Original role, live-tested on Rocky Linux 10.
ansible-dockle
dockle on EL 10 from the GitHub release; get_url refuses it unless its SHA-256 is in the vendor's checksums file, and the live test re-checks it, then scans a one-layer image whose config names no user, expecting CIS-DI-0001 in the output, exit code 1 with --exit-level warn, and the warning counted in the JSON form. Pinned. Original role, live-tested on Rocky Linux 10.
ansible-gitleaks
gitleaks on EL 10 from the GitHub release, refused by Ansible's get_url unless its SHA-256 is the one in the project's checksums file, and re-checked with sha256sum -c by the live test, which then plants a file holding an AWS access key that is not one and runs gitleaks detect over it, expecting 'leaks found: 1'. Original role, live-tested on Rocky Linux 10.