PAM module (sudo via passkey)
pam-ssh-agent-webauthn is a PAM module that authenticates users via a WebAuthn passkey forwarded over the SSH agent. Drop it into /etc/pam.d/sudo and your sudo prompts get answered by a passkey approval (touch / biometric / PIN, depending on the authenticator) instead of a password.
It lives in its own repo and is standalone — no runtime dependency on ShellWatch. It works with any SSH agent that exposes webauthn-sk-ecdsa-sha2-nistp256@openssh.com keys and produces WebAuthn-format signatures. ShellWatch is one such agent; OpenSSH’s own forwarded agent is another (if you’re holding webauthn-keys).
Why use it
Section titled “Why use it”Combined with the SSH agent proxy and ssh -A (agent forwarding), this turns sudo on a remote box into a touch-to-confirm operation:
- You SSH to a target host with
ssh -A user@host. - You run
sudo apt updateon the target. - The target’s PAM stack invokes
pam_ssh_webauthn.so, which talks to the forwarded SSH agent (your localshellwatch-agent), which talks to ShellWatch, which fires aPendingActionto your browser. - You tap your YubiKey. PAM verifies the assertion locally against
/etc/security/authorized_keys, returnsPAM_SUCCESS, andsudoproceeds.
The signature is end-to-end — produced by your authenticator, verified by PAM on the target. Neither ShellWatch nor the SSH agent forwarder can forge it; they can only deny or delay.
What it does (mechanically)
Section titled “What it does (mechanically)”When PAM invokes the module:
- Reads authorized public keys from a configured file (default:
/etc/security/authorized_keys). - Connects to the SSH agent at
$SSH_AUTH_SOCK. - Lists agent identities, filters for
webauthn-sk-ecdsa-sha2-nistp256@openssh.comkeys. - Matches agent keys against authorized keys by raw blob comparison.
- Generates a 32-byte random challenge.
- Sends
SSH_AGENTC_SIGN_REQUESTto the agent. - Receives the signature with WebAuthn fields (
origin,clientDataJSON,extensions). - Validates the challenge is base64url-encoded inside
clientDataJSON. - Constructs
signed_data = SHA256(application) || flags || counter || extensions || SHA256(clientDataJSON). - Verifies the ECDSA P-256 signature over
signed_data. - Returns
PAM_SUCCESSorPAM_AUTH_ERR.
It does not handle standard SSH key types (RSA, Ed25519, ECDSA) — use pam-ssh-agent for those. Standard FIDO2 SK keys (with application=ssh:) are also out of scope; this module is specifically for browser-registered (webauthn-...) credentials.
Requires Rust 1.70+ and PAM development headers.
# Linuxsudo apt install libpam0g-dev # Debian/Ubuntusudo dnf install pam-devel # Fedora/RHEL
# Buildgit clone https://github.com/rado0x54/pam-ssh-agent-webauthn.gitcd pam-ssh-agent-webauthncargo build --release
# Output:# target/release/libpam_ssh_webauthn.so (Linux)# target/release/libpam_ssh_webauthn.dylib (macOS — for testing)Pre-built binaries are also attached to each release — drop the .so straight into the modules directory and skip the build step.
For FIPS / OpenSSL-backed crypto:
cargo build --release --features native-cryptoInstall
Section titled “Install”1. Drop the shared library into the PAM modules directory
Section titled “1. Drop the shared library into the PAM modules directory”# Linux (Debian/Ubuntu, x86_64)sudo cp target/release/libpam_ssh_webauthn.so \ /usr/lib/x86_64-linux-gnu/security/pam_ssh_webauthn.so
# Fedora/RHELsudo cp target/release/libpam_ssh_webauthn.so \ /usr/lib64/security/pam_ssh_webauthn.so
# macOS (testing only)sudo cp target/release/libpam_ssh_webauthn.dylib \ /usr/lib/pam/pam_ssh_webauthn.so2. Authorise some passkey public keys
Section titled “2. Authorise some passkey public keys”Create /etc/security/authorized_keys with the WebAuthn public keys that should be allowed to authenticate:
webauthn-sk-ecdsa-sha2-nistp256@openssh.com AAAA... my-yubikeyGet the public-key blob from ShellWatch under Settings → Passkeys — each registered passkey shows its OpenSSH-format public key.
The blob must match the agent’s blob exactly, including the application/relying-party ID. A key registered on one ShellWatch deployment is not interchangeable with one registered on another (they have different relying-party IDs).
Other key types in the file are silently ignored.
3. Configure PAM
Section titled “3. Configure PAM”Add to /etc/pam.d/sudo before other auth lines (order matters — sufficient short-circuits on success):
auth sufficient pam_ssh_webauthn.soWith a custom key file path:
auth sufficient pam_ssh_webauthn.so file=/path/to/authorized_keysTo override the agent socket (instead of $SSH_AUTH_SOCK):
auth sufficient pam_ssh_webauthn.so socket=/path/to/agent.sock4. Preserve SSH_AUTH_SOCK for sudo
Section titled “4. Preserve SSH_AUTH_SOCK for sudo”sudo strips most environment variables by default. Add an explicit allowlist:
Defaults env_keep += "SSH_AUTH_SOCK"Test it without PAM
Section titled “Test it without PAM”The included authenticator example runs the full flow without involving PAM — useful for sanity-checking that the agent and authorized-keys file line up:
export SSH_AUTH_SOCK=/path/to/agent.sockcargo run --example authenticator -- /path/to/authorized_keysThe list_keys example dumps the agent’s exposed key blobs:
SSH_AUTH_SOCK=/path/to/agent.sock cargo run --example list_keysValidation checks (matching OpenSSH’s webauthn_check_prepare_hash)
Section titled “Validation checks (matching OpenSSH’s webauthn_check_prepare_hash)”- Algorithm:
webauthn-sk-ecdsa-sha2-nistp256@openssh.comorsk-ecdsa-sha2-nistp256@openssh.com. - User Presence (UP): flag bit
0x01must be set. - Attested Credential Data (AD): flag bit
0x40must NOT be set (this is an assertion, not a registration). - Extension Data (ED): flag bit
0x80must be consistent with extensions presence. - Origin: must not contain quote characters; must match between signature blob and
clientDataJSON. clientDataJSON.type: must be"webauthn.get".clientDataJSON.challenge: must base64url-decode to the challenge PAM sent.clientDataJSON.origin: must match the origin from the signature blob.- Trailing data: rejected.
Limits
Section titled “Limits”- No counter / replay protection. The WebAuthn counter is integrity-protected (covered by the signature) but not checked for monotonic increase. A captured signature still can’t be replayed (each PAM invocation generates a fresh challenge), but a cloned authenticator with a reset counter would not be detected. Stateless PAM modules can’t reasonably persist counters.
- No User Verification (UV) enforcement. UP is required, UV is not. Some authenticators don’t support UV; if you want UV, use server-side enforcement on
sshd(not relevant forsudo, but matters for SSH login). - Agent timeout: 60 s. Generous because the sign request requires user interaction.
- No
authorized_keys_command— file-only.
Troubleshooting
Section titled “Troubleshooting”SSH_AUTH_SOCK not set — sudo dropped the variable. Add the env_keep rule above.
No matching key — your authorized-keys blob doesn’t match the agent’s exact blob. Use cargo run --example list_keys to see what the agent actually presents and copy that whole blob (including the application field).
Auth times out — the sign request fired but nobody approved it within 60 s. Check that a browser tab is open in ShellWatch (or that Web Push is configured and you tapped the notification).
Silent failures — enable debug logging via PAM or syslog. The module logs to syslog facility AUTH as pam_ssh_webauthn.
Trust model
Section titled “Trust model”| Trusted | The authenticator hardware, the browser’s WebAuthn origin check, this PAM verifier. |
| Transport only | The SSH agent forwarder (ShellWatch, or any other forwarder) — can deny or delay, cannot forge. |
| Verified on target | ECDSA P-256 signature over a fresh challenge, checked against the public key in /etc/security/authorized_keys. |
This is what ShellWatch’s promise of “end-to-end, human-in-the-loop” looks like in practice — the PAM verifier on the target host independently checks the signature, so even a compromised broker can’t grant sudo.