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.

ansibleSecurity & Secrets

Verification

Live-tested

Really 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 · attribution

Paste 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.

README.md, GitLab, Gitea
[![IaC Bazaar: live-tested](https://www.iac-bazaar.com/api/artifacts/ansible-ssh-totp/badge)](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:IaC Bazaar: live-tested

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

Live-tested

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.

View module
Live-tested

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.

View module
Live-tested

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.

View module
Live-tested

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.

View module
Live-tested

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.

View module
Live-tested

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.

View module