SSH Logins With A Key And A Code, And An Exempt Group For Automation
SSH logins that need a key AND a code from an authenticator app: secrets held by root, the code in place of a password, and the account Ansible logs in with exempt so a playbook still connects. The drop-in is checked with the whole sshd configuration first, because read too late it stopped sshd from starting. 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-10-08 · 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-totp?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
Ansible role 1.0.0, live-tested on IaC Bazaar: [SSH Logins With A Key And A Code, And An Exempt Group For Automation](https://www.iac-bazaar.com/catalog/ansible-ssh-totp?utm_source=syndication&utm_medium=readme&utm_campaign=artifact)
```yaml
# SSH Logins With A Key And A Code, And An Exempt Group For Automation: https://www.iac-bazaar.com/catalog/ansible-ssh-totp (download from your IaC Bazaar account)
```Preview:
Documentation
ssh-totp
SSH logins that need a key AND a six-digit code from an authenticator app, for every account outside an exempt group: the secrets held by root, the code in place of a password rather than beside it, the account Ansible logs in with exempt by itself so a playbook can still connect, and the sshd drop-in checked with the whole configuration before it lands. Original role for EL 10, live-tested with podman on Rocky Linux 10.
No download, and no version to pin. EL 10 packages what this policy needs, so
the role installs google-authenticator, openssh-server by name and takes what the distribution ships. What the
role owns is the policy and the proof that the host obeys it.
What it changes. /etc/ssh/sshd_config.d/00-iacbazaar-totp.conf (root
0600): AuthenticationMethods publickey,keyboard-interactive:pam, and a Match Group iacbazaar-totp-exempt block that needs the key alone. In
/etc/pam.d/sshd, the auth phase asks pam_google_authenticator for the code
and no longer includes password-auth; the account, password and session
phases are the package's. iacbazaar-totp-enrol USER (in /usr/local/sbin,
root only) makes an account's secret in /var/lib/iacbazaar-totp and prints
the otpauth:// line an authenticator app reads, and five one-time scratch
codes.
Everyone outside the group needs a code from the next login on. An account
with no secret is refused after its key is accepted (measured), so enrol each
account first, or put it in ssh_totp_exempt_users until it has enrolled. The
account this play logs in as is added by itself - read from SUDO_USER, or the
account itself when it is root - and the role refuses to touch sshd unless that
account is in the group. With become_method: su it cannot tell who logged
in, so name the account.
root cannot pass a code. Under PermitRootLogin prohibit-password (the
ssh-hardening role's looser setting) sshd hands PAM a dummy password for root,
and the code is refused ("Dummy password supplied by PAM", measured), so root
logs in with a key only when it is exempt, and not at all otherwise.
The number in the file name keeps sshd running. sshd takes the first value
it reads. The ssh-hardening role's 01- file turns keyboard-interactive off; a
drop-in numbered 40- was read after it, AuthenticationMethods then named a
method that was off, and sshd refused to start (measured). This role's file is
00-, and each candidate is checked in place: the checker puts it under its
real name beside the drop-ins already there and runs sshd -t over the whole
tree, so the same file named 40- is refused ("Disabled method
keyboard-interactive") before it lands. The Match block at the end of the
file does not reach the files read after it (measured: sshd -T for a
non-member still shows PasswordAuthentication no from the 01- file).
Wrong codes delay the next login from the same address. sshd on EL 10
penalises a source address after failed logins (PerSourcePenalties): after a
few wrong codes from one address, the next connection from it - an exempt
account's included - was dropped ("drop connection ... penalty: failed
authentication", measured). People behind one NAT or bastion share that address
with your automation. sshd -T on EL 10 shows the defaults: five seconds per
failed login, connections refused once that reaches fifteen, at most ten
minutes.
The secrets are root's. Each secret is root 0400 in
/var/lib/iacbazaar-totp, read by the PAM module as root (user=root): a user
cannot read or replace their own, and a home directory that is not mounted yet
locks nobody out. A code that was used is refused (measured), and each secret
is written with google-authenticator's rate limit of three tries per 30 seconds
(ssh_totp_rate_limit).
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-lynis
Lynis from EPEL on a systemd timer, with the units the package keeps only in its docs, your skipped tests in custom.prf, and a minimum hardening index below which the audit unit fails, so a falling score shows as a failed unit. The live test runs the audit, reads the index and the tests that ran, and finds the timer enabled. Original role, live-tested on Rocky Linux 10.
ansible-suricata
Suricata from EPEL watching the default-route interface as the suricata user, ET Open fetched at install and refreshed daily, plus a canary rule that proves the sensor is alive. A root-run check left root-owned logs and a daemon recording nothing; the role prevents it. The live test raises the canary alert while a second lookup is logged, not alerted. Original role, live-tested on Rocky Linux 10.
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-389-ds
389 Directory Server from AppStream. A fresh instance listens on every interface and lets anonymous clients read the tree; this role binds both listeners to loopback and limits anonymous clients to the rootDSE. The live test adds a user who binds, sees a wrong password and an anonymous search refused, and reads the LDAPS certificate. Original role, live-tested on Rocky Linux 10.
ansible-krb5-kdc
An MIT Kerberos KDC: the realm created once with a stash, kadmind behind an ACL, a default policy that locks an account after five wrong passwords, every listener on loopback. The KDC starts on a broken config without a word, so the live test proves each setting by behaviour: the ticket lifetime, the refusals, the lockout. 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.