Protecting SSH and Console Logins with pam_authelia
On this page
- Debian and Ubuntu
- Arch Linux
- Alpine Linux
- Other Linux distributions (generic tarball)
- Building from source
1FA: password only2FA: password from PAM stack, then second factor1FA+2FA: password then second factor- Second factor method selection
- 1FA only
- 1FA+2FA (recommended default)
- 2FA only (local password + Authelia second factor)
- Device Authorization flow
- Custom CA (self-signed Authelia)
Authentication failedwith no useful logsssh: unable to authenticate, attempted methods [none keyboard-interactive], no supported methods remaindevice authorization response status=401in the debug logclaim "authelia.pam.username" missing from userinfo responseauthelia identity "..." does not match pam username "..."id token verification failed: ...userinfo request failed--oauth2-scope must include openid/--oauth2-scope must include authelia.pamresponse status=429in the debug log- Device Authorization flow fails with
device authorization token expired
Introduction
pam_authelia is a PAM module that lets anything authenticating through PAM (most commonly OpenSSH, but also login, su, sudo, and any other PAM consumer) delegate credential verification and two-factor challenges to an Authelia server over its existing HTTP API. Nothing on the server side needs to change: pam_authelia calls the same /api/firstfactor, /api/user/info, /api/secondfactor/totp, /api/secondfactor/duo, /api/oidc/device-authorization, and /api/oidc/token endpoints that the Authelia web portal uses.
The module ships as two artifacts that cooperate over a stdin/stdout pipe protocol:
pam_authelia.so: a small C shim loaded into the PAM consumer’s process (e.g.sshd). It handles the PAM conversation function (pam_conv) for prompting the user and securely wiping credentials from memory, and delegates everything else to the Go helper.pam_authelia: a Go helper binary that handles every HTTPS request to Authelia, parses responses, orchestrates the 2FA flow, and renders QR codes for the OAuth2 Device Authorization grant.
The split exists because a CGO-based single-binary PAM module would pull the Go runtime into every sshd preauth child, and because a clean process boundary simplifies fork safety and credential zeroisation. Operators do not need to understand the protocol to use the module.
This guide walks through installing pam_authelia, wiring sshd through it, and configuring each supported flow.
Assumptions
This guide makes the following assumptions:
- Authelia is already set up, running, and reachable over HTTPS from the host where you plan to install pam_authelia. If you are using a self-signed certificate the CA certificate must be available on that host (see the
ca-certoption). - The user you plan to authenticate already exists in Authelia’s authentication backend and has TOTP, Duo, and/or a Device Authorization OIDC client enrolled, depending on which 2FA flow you plan to use.
- You have
rooton the host running the PAM consumer and can edit/etc/pam.d/*and/etc/ssh/sshd_configand reloadsshd.
How it works
The flow for a single SSH login looks like this:
sshd ──▶ PAM ──▶ pam_authelia.so ──fork+exec──▶ pam_authelia (Go) ──HTTPS──▶ Authelia
▲ │ │
│ pam_conv prompts ──┘ │
└─────────────────────────────── SUCCESS / FAILURE ──┘sshdaccepts the TCP connection and hands the authentication over to PAM per its/etc/pam.d/sshdstack.- PAM loads
pam_authelia.so, which forks the Go helper (pam_authelia) with the PAM module options passed as CLI flags. - Username and password are sent to the Go helper over a pipe. For
auth-level=2FAthe password is taken fromPAM_AUTHTOK(set by a precedingpam_unixentry) so the user is never prompted by pam_authelia for it. - The Go helper calls Authelia (
/api/firstfactor, then optionally/api/user/infoand one of the/api/secondfactor/*or/api/oidc/*endpoints) and writes prompt/info/success/failure commands back to the C shim, which surfaces them to the SSH client throughpam_conv. - For the Device Authorization flow the helper additionally runs OIDC discovery against the configured Authelia URL and calls
/userinfowith the issued access token, to verify the approved identity matches the Linux username. See Device Authorization identity binding for the full check list. - On success the shim returns
PAM_SUCCESStosshd; on failure it returnsPAM_AUTH_ERR.
The Go helper never writes credentials to logs, zeroes them from memory after use, and enforces HTTPS-only communication with Authelia.
Installation
pam_authelia is released from github.com/authelia/pam as .deb, glibc tarball, and musl tarball artifacts for amd64, arm, and arm64. Checksums, SBOMs, and GPG signatures are published alongside every release.
Regardless of which channel you use, two files get installed:
| File | Destination |
|---|---|
pam_authelia | /usr/bin/pam_authelia |
pam_authelia.so | /lib/security/pam_authelia.so (distro-dependent) |
The PAM module directory differs between distributions (Debian uses /lib/x86_64-linux-gnu/security/, Alpine uses /lib/security/, Arch uses /usr/lib/security/), so if you are installing manually you may need to locate the directory containing pam_unix.so and install pam_authelia.so alongside it. The packaged installation methods below handle this automatically.
Debian and Ubuntu
The preferred method is to install from the Authelia APT repository, which publishes signed packages for both Authelia itself and pam_authelia. If you have already added the repository to your system you can install with a single command:
sudo apt update && sudo apt install pam_autheliaIf you have not added the repository yet, follow the APT Repository setup steps first and then run the command above. The repository is signed with Authelia’s release key and handles upgrades automatically, so you do not need to download .deb files manually.
Alternatively, you can download a specific .deb release directly from github.com/authelia/pam and install it with apt install ./<file>.deb; useful for pinning a particular version or for hosts that cannot reach the APT repository.
Arch Linux
Three community packages are maintained in the Arch Linux AUR, covering every preference:
| Package | What it installs | When to pick it |
|---|---|---|
pam_authelia | Builds from the latest tagged release tarball on your build host | Preferred for reproducible builds |
pam_authelia-bin | Installs the prebuilt upstream binary artifact as-is | Fastest install, no local toolchain needed |
pam_authelia-git | Tracks the master branch of github.com/authelia/pam | Following unreleased changes or testing patches |
Install whichever suits you using your AUR helper of choice, for example with paru:
paru -S pam_authelia-binor with yay:
yay -S pam_authelia-binAll three packages install the same file layout, so the PAM configuration examples later in this guide apply unchanged.
Alpine Linux
Download the -musl tarball from the github.com/authelia/pam releases page and extract the two files into place:
curl -LO https://github.com/authelia/pam/releases/latest/download/pam_authelia-v0.1.0-linux-amd64-musl.tar.gz
tar -xzf pam_authelia-v0.1.0-linux-amd64-musl.tar.gz
sudo install -m 0755 pam_authelia /usr/bin/pam_authelia
sudo install -m 0644 pam_authelia.so /lib/security/pam_authelia.soOther Linux distributions (generic tarball)
For glibc distributions without a native package, use the glibc tarball instead of -musl:
curl -LO https://github.com/authelia/pam/releases/latest/download/pam_authelia-v0.1.0-linux-amd64.tar.gz
tar -xzf pam_authelia-v0.1.0-linux-amd64.tar.gz
sudo install -m 0755 pam_authelia /usr/bin/pam_authelia
sudo install -m 0644 pam_authelia.so "$(dirname "$(find /lib /usr/lib -name pam_unix.so -print -quit)")/pam_authelia.so"The find | dirname dance picks up whichever PAM module directory your distribution uses by locating the well-known pam_unix.so file.
Building from source
If none of the packaged install channels above fit your platform, both artifacts can be built directly from the github.com/authelia/pam repository. You will need:
- Go
1.26or newer gcc(or any C11-capable compiler that understands-fstack-protector-strongand-D_FORTIFY_SOURCE=3)makelibpamdevelopment headers:libpam0g-devon Debian/Ubuntu,linux-pam-devon Alpine,pamis included in the base system on Arch
Clone the repository:
git clone https://github.com/authelia/pam.git
cd pamBuild the Go helper binary. The flags match Authelia’s own release build: -trimpath strips local paths from the binary, -ldflags '-s -w' strips the symbol table and DWARF debug information for a smaller binary. CGO_ENABLED=0 is deliberate; the Go helper does not link against libc and is safe to build as a static binary:
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w' -o pam_authelia ./cmd/pam_autheliaBuild the C shim. The shim/Makefile handles the hardening flags for you (-fstack-protector-strong, -D_FORTIFY_SOURCE=3, full RELRO, -z now, -fPIC, -fno-plt on Linux) and detects .so vs .dylib based on the host platform:
make -C shimInstall both artifacts. The pam_authelia.so destination depends on your distribution; use find to locate the directory that already contains pam_unix.so:
sudo install -m 0755 pam_authelia /usr/bin/pam_authelia
sudo install -m 0644 shim/pam_authelia.so \
"$(dirname "$(find /lib /usr/lib -name pam_unix.so -print -quit)")/pam_authelia.so"Confirm both files are in place and have the expected modes before adding pam_authelia.so to your PAM stack:
ls -l /usr/bin/pam_authelia
ls -l "$(dirname "$(find /lib /usr/lib -name pam_unix.so -print -quit)")/pam_authelia.so"SSH server prerequisites
pam_authelia uses the PAM keyboard-interactive conversation to prompt for passwords and 2FA codes, so sshd must be configured to use PAM and to permit keyboard-interactive authentication. The minimum /etc/ssh/sshd_config looks like this:
UsePAM yes
KbdInteractiveAuthentication yes
PasswordAuthentication no
AuthenticationMethods keyboard-interactiveIf you plan to use the OAuth2 Device Authorization flow you may want to consider LoginGraceTime. The default of 2 minutes is usually enough for users to scan the QR code and approve on their phone, but if you see logins timing out while users are still mid-approval you can raise it:
LoginGraceTime 5mReload sshd after editing the file:
sudo systemctl reload sshdPAM module options
The following options can be supplied to pam_authelia.so in any PAM stack file (commonly /etc/pam.d/sshd). Every option is a key=value pair except for the boolean debug flag which takes no value. Option names are case-sensitive and use kebab-case.
The required badge on each option below uses one of three values, matching the convention used elsewhere in the Authelia documentation:
yes: the option must be set; the module will refuse to authenticate without it.no: optional; the shown default applies when the option is omitted.situational: required only under specific configurations (for exampleoauth2-client-idis required whenmethod-prioritycontainsdevice_authorization, otherwise it is ignored).
Options
url
The URL of the Authelia server. Must use the https:// scheme. This is the base URL the Go helper uses for every API call, for example POSTing to /api/firstfactor.
Example:
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA+2FAauth-level
The authentication level to enforce. Must be one of 1FA, 2FA, or 1FA+2FA (case-sensitive). Each level is described in full under Authentication flows:
1FA: password only, validated against Authelia’s first-factor endpoint.2FA: the password is read fromPAM_AUTHTOK(set by a preceding module such aspam_unix.so), Authelia is queried silently for 1FA, and the user is prompted for the second factor.1FA+2FA: the user is prompted for a password, and upon success is prompted for the second factor.
cookie-name
The name of the session cookie Authelia issues on successful 1FA. Must match the server-side session.cookies[].name value in your Authelia configuration. Only change this if your Authelia deployment has a non-default session cookie name.
ca-cert
Path to a custom CA certificate (PEM-encoded) used to verify Authelia’s TLS certificate. Defaults to the system trust store. Use this when Authelia is served behind a private CA:
auth required pam_authelia.so url=https://auth.internal ca-cert=/etc/ssl/certs/internal-ca.pemThe file must be readable by the user sshd drops privileges to during authentication (typically root for the PAM preauth child).
timeout
Upper bound in seconds on the entire PAM exchange, including time spent waiting for user input and for Authelia responses. When the timeout fires the C shim kills the Go helper and returns PAM_AUTH_ERR to sshd.
The default of 60 seconds is comfortable for password and TOTP flows and usually sufficient for the Device Authorization flow too, especially if users approve on a phone they already have to hand.
Note
If you see sshd aborting Device Authorization logins before the Go helper has finished polling (for example because users take longer than 60 seconds to find their phone before even starting the approval), raise this option on the pam_authelia.so line in /etc/pam.d/sshd:
auth required pam_authelia.so url=https://auth.example.com timeout=300 \
method-priority=device_authorization oauth2-client-id=pam-autheliaThis option only governs the PAM-side exchange. It is unrelated to Authelia’s own device code expiry (configured server-side via identity_providers.oidc.lifespans.device_code). If your users hit device authorization token expired that’s an Authelia-side timeout and raising this PAM option will not help. See Troubleshooting for how to handle that case.
binary
Absolute path to the pam_authelia Go helper binary. Override only if you installed the binary somewhere non-standard (for example when building from source and installing under /opt/ or /usr/local/bin/).
method-priority
A comma-separated list of 2FA method identifiers the module should try, in order. Valid entries are totp, mobile_push, device_authorization, and the special user keyword. The first entry whose method is usable for the current user is selected; if none match, authentication fails.
When this option is omitted the module uses whichever 2FA method Authelia has stored as the user’s preference. See Method priority and the user entry for worked examples.
oauth2-client-id
OAuth2 client ID for the Device Authorization grant. Required when method-priority contains device_authorization; ignored otherwise. Must match a client configured on the Authelia side with grant_types: ['urn:ietf:params:oauth:grant-type:device_code']. See Configuring Authelia for the Device Authorization flow for the server-side setup.
oauth2-client-secret
OAuth2 client secret. Required when the client referenced by oauth2-client-id is a confidential client (i.e. configured with token_endpoint_auth_method: 'client_secret_post' on the Authelia side). Omit this for public clients.
Important Note
This secret appears in cleartext in /etc/pam.d/* and will be visible to anyone with read access to those files. On most distributions /etc/pam.d/* is already 0644 and owned by root, but verify that the file is not world-readable if you consider the device-flow client secret sensitive, or use a public client (no secret) instead.
oauth2-scope
Comma-separated OAuth2 scopes to request on the Device Authorization endpoint. The module normalizes the comma-separated form into the space-separated form required by RFC 6749 before sending the HTTP request. Only relevant when the Device Authorization flow is enabled.
Both openid and authelia.pam are mandatory and are enforced at config parse time; the Go helper refuses to start if either is missing. openid is required so Authelia issues an ID token the helper can verify; authelia.pam is the custom scope that grants the authelia.pam.username claim used to bind the issued token to the Linux username the PAM module is authenticating. You can append additional scopes (for example openid,authelia.pam,email) without breaking this contract, but you cannot drop either of the two required ones. See Device Authorization identity binding for the full rationale and the server-side claims_policies and custom-scope configuration that produces the claim.
debug
A boolean flag with no value; its presence enables debug logging. Diagnostic lines are written to stderr, which sshd captures in its journal (see Troubleshooting for the exact log format and how to read it).
Authentication flows
pam_authelia supports three authentication levels, controlled by the auth-level option, and four 2FA methods, controlled by method-priority. The three levels are:
1FA: password only
The user is prompted for a password. The module POSTs {"username": "...", "password": "..."} to /api/firstfactor and grants the login on HTTP 200 with status: OK. No second factor is ever attempted; this mode is only useful when Authelia is acting as a centralized password store and you do not want two-factor enforcement on PAM logins.
2FA: password from PAM stack, then second factor
The password is taken from PAM_AUTHTOK, which must be populated by a preceding module such as pam_unix.so:
auth required pam_unix.so
auth required pam_authelia.so url=https://auth.example.com auth-level=2FApam_authelia silently POSTs the same credentials to /api/firstfactor (the user is not re-prompted), and upon success prompts for the second factor. This mode is useful when local Unix passwords are the source of truth and Authelia is only consulted for the second factor.
Important Note
Because the password captured by pam_unix.so is forwarded verbatim to Authelia’s first-factor endpoint, the user’s local Unix password must match their Authelia password. If the two drift out of sync the silent 1FA call to Authelia will fail and the login will be rejected even though pam_unix.so already accepted the password. Operators running this mode should either provision the same password in both places when the account is created, or use 1FA+2FA (described below) instead, which prompts the user once and validates only against Authelia.
1FA+2FA: password then second factor
The most common deployment. The user is prompted for their password, the module validates it against /api/firstfactor, and then prompts for the second factor:
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA+2FASecond factor method selection
For 2FA and 1FA+2FA, the module fetches /api/user/info to discover which 2FA methods the user has enrolled, then picks one according to method-priority:
| Method | Endpoint | User interaction |
|---|---|---|
| TOTP | POST /api/secondfactor/totp | Types a 6- or 8-digit code from an authenticator app |
| Duo push | POST /api/secondfactor/duo | Approves the push on their phone |
| Device Authorization | POST /api/oidc/device-authorization (setup) | Scans the QR code or visits the verification URL, completes Authelia login (1FA and 2FA if required), approves the consent prompt, presses Enter |
POST /api/oidc/token (poll) |
WebAuthn over SSH
pam_authelia cannot drive the direct /api/secondfactor/webauthn flow, because FIDO2 authenticators need USB or NFC access to the client host and the SSH keyboard-interactive channel cannot pass an authenticator ceremony through. Behavior depends on what else the user has enrolled and on the method-priority setting:
- WebAuthn is the user’s Authelia preference but they also have TOTP or Duo enrolled, and
method-priorityis unset or containsuser(the default), pam_authelia automatically falls through to TOTP, then Duo, then Device Authorization, and authenticates via the first usable method. No operator intervention needed. - WebAuthn is the user’s only enrolled method: the direct 2FA path fails, but WebAuthn still works via the Device Authorization flow. At the verification URL the user is logging in through a real browser at the Authelia portal, so WebAuthn (and any other 2FA method Authelia supports) works normally there. Configure
method-priority=device_authorizationormethod-priority=device_authorization,useron the PAM stack. method-priorityis set to an explicit list that excludes bothuseranddevice_authorization(for examplemethod-priority=totpwhen the user has only WebAuthn enrolled): authentication fails withno usable 2FA method for this user. Either enroll an additional method on the Authelia side or widen the priority list so the module can fall through.
Method priority and the user entry
When method-priority is omitted, pam_authelia uses whichever 2FA method the user has marked as preferred in Authelia. For most deployments this is the right behavior. For cases where you want the PAM stack to enforce a specific 2FA flow regardless of the user’s preference (for example, “always use the Device Authorization flow on servers in this fleet”), use an explicit priority list.
A priority list is a comma-separated list of method identifiers. The module walks the list top-to-bottom and uses the first one that resolves to a usable method for the current user. Valid entries are:
totp: use TOTP if the user has it enrolled.mobile_push: use a Duo push if the user has Duo enrolled.device_authorization: use the OAuth2 Device Authorization grant. Requiresoauth2-client-idto be set.user: a special entry that resolves to the user’s Authelia preference at runtime. If that preference is WebAuthn (unsupported over SSH) or empty, the module falls back through TOTP, Duo, and Device Authorization in that order.
Worked examples:
| Priority list | Behavior |
|---|---|
totp | Always TOTP; fail if the user has not enrolled TOTP. |
totp,mobile_push,user | Prefer TOTP, then Duo push, then fall back to whatever Authelia stores as the preference. |
device_authorization,user | Prefer the Device Authorization flow, fall back to the user’s stored preference. |
user | Always respect the user’s Authelia preference (identical to the default behavior). |
Example PAM configurations
The following examples all target /etc/pam.d/sshd. Replace the url= hostname with your Authelia deployment. Each example is self-contained and can be used unmodified (except for url=).
1FA only
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA
account required pam_permit.so
session required pam_permit.so1FA+2FA (recommended default)
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA+2FA
account required pam_permit.so
session required pam_permit.so2FA only (local password + Authelia second factor)
auth required pam_unix.so
auth required pam_authelia.so url=https://auth.example.com auth-level=2FA
account required pam_permit.so
session required pam_permit.soDevice Authorization flow
auth required pam_authelia.so url=https://auth.example.com \
auth-level=1FA+2FA \
method-priority=device_authorization,user \
oauth2-client-id=pam-authelia \
oauth2-client-secret=hashed-secret-here \
oauth2-scope=openid,authelia.pam \
timeout=300
account required pam_permit.so
session required pam_permit.soThe timeout=300 value gives the user five minutes to approve on their phone before the PAM exchange is torn down. Both scopes (openid and authelia.pam) are required; see oauth2-scope for why, and Configuring Authelia for the Device Authorization flow for the matching server-side setup.
Custom CA (self-signed Authelia)
auth required pam_authelia.so url=https://auth.internal \
auth-level=1FA+2FA \
ca-cert=/etc/ssl/certs/internal-ca.pem
account required pam_permit.so
session required pam_permit.soConfiguring Authelia for the Device Authorization flow
The Device Authorization flow is the only 2FA method that requires additional server-side configuration. Beyond registering an OIDC client with the urn:ietf:params:oauth:grant-type:device_code grant type, you must also define a claims policy that emits the authelia.pam.username claim and a custom OIDC scope (authelia.pam) that grants it. pam_authelia uses the claim to bind the issued token to the Linux username it is authenticating. Without these two pieces the device flow aborts at the new identity-binding check with claim "authelia.pam.username" missing from userinfo response. See Device Authorization identity binding for the full rationale.
Add the following blocks to your Authelia configuration:
identity_providers:
oidc:
claims_policies:
pam:
custom_claims:
authelia.pam.username:
attribute: 'username'
scopes:
authelia.pam:
claims:
- 'authelia.pam.username'
clients:
- client_id: 'pam-authelia'
client_name: 'pam_authelia device flow'
client_secret: '$pbkdf2-sha512$310000$...'
public: false
authorization_policy: 'two_factor'
grant_types:
- 'urn:ietf:params:oauth:grant-type:device_code'
token_endpoint_auth_method: 'client_secret_post'
claims_policy: 'pam'
scopes:
- 'openid'
- 'authelia.pam'What each block does:
claims_policies.pam: a reusable policy namedpamwith a single custom claim,authelia.pam.username, whose value is sourced from the backend’susernameattribute. If your backend’s raw username doesn’t match the Linux account verbatim (different case, an@realmsuffix, etc.) anchor the claim at a derived attribute instead; see Case sensitivity and username normalization.scopes.authelia.pam: a custom OIDC scope namedauthelia.pamthat grants theauthelia.pam.usernameclaim. The name is arbitrary but must match whatever you send via the PAMoauth2-scopeoption;authelia.pamis the default the Go helper expects.- Client-level
claims_policy: 'pam': attaches thepamclaims policy to this specific client so the custom claim is actually emitted when a token is issued. - Client-level
scopes: ['openid', 'authelia.pam']: the client is only allowed to request these two scopes, matching the PAM module’s defaults.
Additional notes:
- The
client_secretvalue in Authelia’s configuration is a hashed representation; useauthelia crypto hash generateto produce one from a random plaintext secret. The cleartext value is what you pass to pam_authelia viaoauth2-client-secret. authorization_policy: 'two_factor'forces the device approval itself to require 2FA in Authelia, which effectively gives you two-factor over SSH via the one 2FA challenge the user performs on their phone.- If you prefer a public client (no secret in
/etc/pam.d/*), setpublic: trueon the Authelia side, omitclient_secretthere, and omitoauth2-client-secretin the PAM config. Theclaims_policy+scopesfields still apply.
Once the server-side configuration is in place, reload Authelia and configure the PAM stack as shown in the Device Authorization flow example.
Device Authorization identity binding
The Device Authorization flow has a subtle trust gap that pam_authelia closes explicitly. The OAuth2 token endpoint has no notion of “which local Linux account asked for this code”. If left unchecked, any Authelia account holder who scans a displayed QR code can approve the flow with their own credentials, and the token endpoint will issue a valid access token. Without an identity check, the PAM module would accept that token as proof of authentication and let the approver log in as the requesting Linux user. pam_authelia prevents this by verifying the issued token against the PAM username before returning success.
How the check works
After pam_authelia’s poll of /api/oidc/token returns an access token and ID token, the Go helper runs the following verification steps in order, and fails closed if any step fails:
- OIDC discovery against the configured Authelia URL, fetching the JWKs document used to verify signatures.
- ID token verification: signature against the discovery-supplied JWKs, issuer, audience (must equal
oauth2-client-id), and expiry. - Userinfo request: calls
/userinfounder Bearer authentication with the access token. - Token substitution defense: asserts that
userinfo.sub == id_token.sub. A mismatch here indicates someone swapped an unrelated access token in for one issued during this device flow. - Username binding: looks up the
authelia.pam.usernameclaim in the userinfo response and case-sensitively compares it to the Linux username the PAM shim passed to the Go helper on stdin. Missing claim, wrong type, empty value, or any difference fails the login.
On success the helper writes device identity verified: claim "authelia.pam.username" == pam username "<user>" to the debug log and returns PAM_SUCCESS. On failure it writes a diagnostic line (for example authelia identity "jane" does not match pam username "john") to stderr and returns PAM_AUTH_ERR. There is no partial-success path.
Case sensitivity and username normalization
The comparison is case-sensitive because Linux usernames are. If your Authelia identity store holds usernames in a shape that doesn’t match the Linux account verbatim (mixed case, an @realm suffix, an email, etc.) you must normalize on the Authelia side before the claim is emitted. The cleanest path is to define a derived user attribute via Authelia’s expression engine and anchor the claim at that derived attribute instead of the raw username:
definitions:
user_attributes:
pam_username:
expression: 'username.lowerAscii()'
identity_providers:
oidc:
claims_policies:
pam:
custom_claims:
authelia.pam.username:
attribute: 'pam_username'Strip an @domain suffix the same way:
definitions:
user_attributes:
pam_username:
expression: 'username.split("@")[0].lowerAscii()'Any expression supported by Authelia’s user attributes engine works; substitute a per-user override, concatenate fields, or combine with other attributes. As long as the resolved value equals the local Linux account name verbatim, the bind succeeds.
Verification
Test each configured flow with a plain ssh command. Use a dedicated non-root user that exists in Authelia; if you lock yourself out of your only administrator account you will need out-of-band console access to recover.
1FA
ssh john@server.example.comYou will be prompted for john’s password. Enter the password you configured in Authelia. A successful login should reach the shell prompt within a second.
1FA+2FA with TOTP
ssh john@server.example.comYou will be prompted for the password, then for a TOTP code. Enter the 6- or 8-digit code from your authenticator app. A successful login should land you at the shell prompt.
Device Authorization flow
ssh john@server.example.comA QR code will be rendered in your terminal along with the verification URL and user code. Scan the QR code on your phone (or visit the URL on any browser that can reach Authelia), complete the Authelia consent flow, and press Enter in the SSH session. pam_authelia will poll the token endpoint and return you to the shell once Authelia confirms the approval.
Inspecting the debug log
With debug enabled in the PAM config, a successful 1FA+2FA login with TOTP produces log lines similar to:
pam_authelia: POST https://auth.example.com/api/firstfactor
pam_authelia: response status=200 status_field="OK"
pam_authelia: user info method="totp" has_totp=true has_webauthn=false has_duo=false
pam_authelia: selected "totp" (from priority entry "totp")
pam_authelia: POST https://auth.example.com/api/secondfactor/totp
pam_authelia: response status=200 status_field="OK"On systemd-based distributions these lines are captured by the journal and can be read with:
sudo journalctl -u ssh -t pam_authelia --since '5 minutes ago'Troubleshooting
Authentication failed with no useful logs
If the SSH client reports Authentication failed and the server journal only shows sshd’s PAM: Authentication failure for user line with nothing from pam_authelia, the debug flag is not enabled. Add debug to the pam_authelia.so line in /etc/pam.d/sshd, reproduce the login, and re-check the journal.
ssh: unable to authenticate, attempted methods [none keyboard-interactive], no supported methods remain
This message from the SSH client means PAM returned PAM_AUTH_ERR early. Check the journal for pam_authelia lines; common causes are:
- The Go helper binary is missing or at a non-default path. Confirm
/usr/bin/pam_autheliaexists and is executable, or setbinaryto the correct path. - The CA certificate path in
ca-certis wrong or unreadable; look forfailed to read CA certificateon stderr. sshd_configis missingUsePAM yesorKbdInteractiveAuthentication yes.
device authorization response status=401 in the debug log
The oauth2-client-id or oauth2-client-secret does not match what Authelia has configured for the Device Authorization client, or the client is configured as public on the Authelia side but the PAM config is passing a secret (or vice versa). Reconcile the two sides. The quoted string above is the literal log line the Go helper writes; the lowercase form is intentional, since it matches what you will see in journalctl.
claim "authelia.pam.username" missing from userinfo response
Authelia is not emitting the authelia.pam.username claim on the userinfo endpoint, so pam_authelia’s identity check fails closed. Check that:
- A claims policy exists with
custom_claims.authelia.pam.usernamedefined, anchored at the right backend attribute. - A custom scope named
authelia.pamexists and itsclaimslist grantsauthelia.pam.username. - The OIDC client used by pam_authelia has
claims_policy: 'pam'(or whatever you named the policy) attached and hasauthelia.pamin itsscopeslist. - The PAM
oauth2-scopeoption includesauthelia.pamso the scope is actually requested at device-auth time.
See Configuring Authelia for the Device Authorization flow for the full working YAML.
authelia identity "..." does not match pam username "..."
The user who approved the device flow in the browser is not the same user the PAM module is authenticating. This is pam_authelia’s confused-deputy defense working as intended; it means someone other than the SSH-requesting user attempted to approve the flow, or there is a legitimate case or realm-suffix mismatch between the Linux and Authelia usernames. If it’s the latter, normalize the username on the Authelia side via a derived user attribute and anchor the authelia.pam.username claim at the derived attribute; see Case sensitivity and username normalization.
id token verification failed: ...
The issued ID token did not verify against the Authelia-advertised JWKs. Common causes:
- The
oauth2-client-idin the PAM config does not match theaudienceof the token Authelia issued (usually because you changed the client ID on one side without the other). - Authelia’s JWKs were rotated while a cached discovery document in the Go helper still references the old ones; restart
sshd(or whatever PAM consumer caches the helper process) after rotating keys. - The PAM
urlpoints at a reverse proxy that rewrites the issuer URL on the way through; theissclaim Authelia writes won’t match the discovery document the helper fetches. Put pam_authelia in front of Authelia’s canonical public URL, not an internal host name.
userinfo request failed
The access token was rejected by /userinfo, usually because the custom authelia.pam scope wasn’t actually granted at device-auth time. Double-check that the scope name in the PAM oauth2-scope matches the server-side identity_providers.oidc.scopes entry exactly, and that the client’s scopes list includes it.
--oauth2-scope must include openid / --oauth2-scope must include authelia.pam
Config validation errors from pam_authelia when an operator passes an explicit oauth2-scope that drops one of the two mandatory scopes. Restore both; the defaults already include them, so the simplest fix is usually to remove the explicit oauth2-scope= entirely from /etc/pam.d/sshd.
response status=429 in the debug log
Authelia’s regulation rate-limited the request. Wait for the regulation window to elapse, or tune regulation.max_retries and regulation.find_time on the Authelia side. pam_authelia does not retry rate-limited requests automatically.
Device Authorization flow fails with device authorization token expired
This error comes from Authelia’s token endpoint and means the server-side device code lifetime elapsed before the user approved the flow on their phone. The Go helper is still alive at this point (it is simply relaying the expired_token response Authelia sent back), so raising the PAM timeout option will not help and neither will raising sshd’s LoginGraceTime.
There are two real fixes:
- Approve faster on your phone. The default Authelia device code lifetime is 10 minutes, which is usually plenty.
- If 10 minutes genuinely is not enough for your users, raise Authelia’s device code lifetime server-side by setting
identity_providers.oidc.lifespans.device_code, either globally or on a per-client custom lifespan attached to your Device Authorization client.
The lowercase form of the error string above is intentional; it matches what the Go helper writes to the log.
Security considerations
- TLS is always verified. There is no insecure or
skip-verifymode. Connections to Authelia use TLS 1.2 or later, and verification uses the system trust store or theca-certyou provide. - Credentials never reach logs. Passwords and 2FA tokens are never written to debug output. The debug log records HTTP status codes and the
statusJSON field from Authelia’s responses, but never request or response bodies. - Credentials are zeroed from memory after use via
explicit_bzero(3). - Device Authorization verification URLs are validated. The URL returned by
/api/oidc/device-authorizationmust usehttps://, point to the same host asurl, and be under 2 KiB. This defends against a compromised or man-in-the-middled Authelia response phishing the user via an attacker-controlled URL rendered as a QR code. - Device Authorization tokens are bound to the requesting Linux username. After the token endpoint returns, the Go helper runs OIDC discovery, verifies the ID token, calls
/userinfo, asserts thatuserinfo.sub == id_token.sub, and case-sensitively compares the customauthelia.pam.usernameclaim against the Linux username the shim passed to it. Without this check, any Authelia account holder could approve another user’s QR code and end up logged in as them. See Device Authorization identity binding for the full check list and the required server-sideclaims_policiesplus custom-scope configuration. - Client secrets live in
/etc/pam.d/*. When usingoauth2-client-secretthe value appears in plaintext in PAM configuration files. Verify that those files are not world-readable (0644owned byrootis the default on most distributions), or use a public client to avoid the issue. - Client disconnect detection. If the SSH client disconnects mid-authentication, the C shim notices via
POLLRDHUPon the client socket, kills the Go helper withSIGTERM, and returnsPAM_AUTH_ERR. Without this, Device Authorization polling could outlive the SSH session and keep hitting Authelia’s token endpoint until the device code expired. - Authelia’s regulation still applies. Rate limiting, IP-based throttling, and any other regulation rules enforced by your Authelia deployment apply to every login attempt through pam_authelia. Operators who previously relied on SSH’s own per-source-IP penalties should verify that Authelia’s regulation config is tuned appropriately.
Limitations
- No WebAuthn / FIDO2 over SSH. FIDO2 authenticators require direct USB or NFC access to the client device, which cannot be tunneled through
sshd’s keyboard-interactive channel. - Device-flow QR codes need a Unicode-capable terminal. The QR is rendered with Unicode half-block characters (U+2580, U+2584, U+2588). Terminals that do not render these characters fall back to showing only the verification URL and user code, which still work but defeat the “point your phone at the screen” convenience.
- WebAuthn users without a fallback will fail. If a user’s only enrolled 2FA method is WebAuthn and
method-priorityis set to an explicit list that excludes bothuseranddevice_authorization, authentication fails. Enroll an additional TOTP or Duo credential, widen the priority list, or route the user through the Device Authorization flow (see the WebAuthn over SSH callout in the Authentication flows section for details).
See also
- github.com/authelia/pam: source code, releases, and packaging metadata.
- Authelia issue #497: the original feature request that led to pam_authelia.
- Generating secure values: how to generate the hashed client secret for the Device Authorization OIDC client.
- OpenID Connect 1.0 Clients: full schema reference for OIDC client configuration on the Authelia side.
- OpenID Connect 1.0 Provider:
claims_policies: how to define the claims policy that emitsauthelia.pam.username. - OpenID Connect 1.0 Provider:
scopes: how to declare the customauthelia.pamscope. - User attributes: expression engine for deriving normalized values to use as the
authelia.pam.usernamesource. - RFC 8628: OAuth 2.0 Device Authorization Grant specification.
- RFC 6749: OAuth 2.0 core specification (relevant for the scope format).