SSH hardening
SSH administration uses two independent authentication steps: a valid SSH public key and a time-based one-time password. Password authentication stays disabled, and root cannot log in directly. The resulting path is deliberately different from “password plus OTP”:
trusted private network
|
v
SSH public key
|
v
keyboard-interactive / PAM
|
v
TOTPA planted authorized_keys entry is therefore insufficient by itself. The TOTP step is evaluated through PAM only after the public-key step succeeds.
OpenSSH and PAM boundary
The effective OpenSSH policy keeps these properties together:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
UsePAM yesThe PAM SSH authentication path uses pam_google_authenticator.so directly rather than delegating authentication to Rocky's password-auth stack. That distinction matters because password-auth includes Unix-password modules; retaining it in the SSH authentication path would blur the intended boundary between public-key authentication and TOTP.
A small OpenSSH drop-in enables keyboard-interactive authentication and requires the ordered publickey,keyboard-interactive method chain. The local drop-in sorts before Rocky's vendor SSH drop-in so the intended global value is established before the vendor default that disables challenge-response authentication.
Short-lived connection reuse
Each new SSH transport normally performs its own authentication. A workflow that invokes several ssh and scp processes in sequence would therefore repeat the public-key-plus-TOTP exchange for every connection.
OpenSSH connection multiplexing allows those client processes to open channels over one already-authenticated master connection for a short persistence window. TOTP still protects creation of that master connection. When it expires or is explicitly closed, the next SSH transport must authenticate again.
This is client-side connection reuse, not a server-side authentication exception. The OpenSSH and PAM policy remains unchanged.
SELinux and mutable TOTP state
SELinux remains enforcing. The Google Authenticator PAM module does more than read a seed: replay protection and rate-limiting state can require the secret file to be updated. During testing, the token itself was accepted, but authentication still failed because the module attempted to create a temporary sibling file in the user's home directory and SELinux denied the write.
The permanent fix was not to weaken SELinux. The TOTP state was moved into a private per-user directory with a persistent auth_home_t file-context rule. Both the directory and its contents then receive the type intended for authentication material, including temporary files created during an update. The same per-user enrollment is also consumed by NoMachine's separate PAM stack, so one labeled TOTP state location serves both private administration services without merging their first-factor policies. Future rebuilds enroll directly into that final labeled location; moving an existing default secret is only a migration path for older deployments.
This failure mode is useful because the logs separate cryptographic success from policy failure: Accepted google_authenticator proved the code was valid, while the following tempfile Permission denied and SELinux AVC identified the actual boundary that blocked login.
Lockout-safe rollout
PAM and SSH changes are applied incrementally. The existing SSH session stays open throughout the change, sshd -t must succeed before reload, and the effective configuration is checked with sshd -T. Only then is sshd reloaded.
A second independent terminal must complete a fresh public-key-plus-TOTP login before the original session is closed. This preserves a recovery path for syntax mistakes, PAM errors, SELinux labeling problems, or an authenticator mismatch without temporarily re-enabling password or root SSH access.
Client trust after address changes
SSH host trust is client-side state. When the server moves between LAN or VPN addresses, stale entries are removed from the connecting client's known_hosts, while the server's own known_hosts continues to describe systems that the server connects to.
A new address is accepted only after comparing the client's presented ED25519 host-key fingerprint with the fingerprint obtained through an already-trusted server session. The network address can change without silently changing the server identity being trusted.
This authentication layer complements the WireGuard administration path: WireGuard reduces network exposure, while SSH public-key plus TOTP reduces the consequence of a single compromised SSH credential. NoMachine uses the same TOTP enrollment behind a different first factor: the user's system password.