docs/content/integration/guides/pam-authelia/index.md
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.
This guide makes the following assumptions:
ca-cert option).root on the host running the PAM consumer and can edit /etc/pam.d/* and /etc/ssh/sshd_config and reload sshd.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 ──┘
sshd accepts the TCP connection and hands the authentication over to PAM per its /etc/pam.d/sshd stack.pam_authelia.so, which forks the Go helper (pam_authelia) with the PAM module options passed as CLI flags.auth-level=2FA the password is taken from PAM_AUTHTOK (set by a preceding pam_unix entry) so the user is never prompted by pam_authelia for it./api/firstfactor, then optionally /api/user/info and 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 through pam_conv./userinfo with the issued access token, to verify the approved identity matches the Linux username. See Device Authorization identity binding for the full check list.PAM_SUCCESS to sshd; on failure it returns PAM_AUTH_ERR.The Go helper never writes credentials to logs, zeroes them from memory after use, and enforces HTTPS-only communication with Authelia.
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.
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_authelia
If 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.
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-bin
or with yay:
yay -S pam_authelia-bin
All three packages install the same file layout, so the PAM configuration examples later in this guide apply unchanged.
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.so
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.
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:
1.26 or newergcc (or any C11-capable compiler that understands -fstack-protector-strong and -D_FORTIFY_SOURCE=3)makelibpam development headers: libpam0g-dev on Debian/Ubuntu, linux-pam-dev on Alpine, pam is included in the base system on ArchClone the repository:
git clone https://github.com/authelia/pam.git
cd pam
Build 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_authelia
Build 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 shim
Install 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"
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-interactive
If 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 5m
Reload sshd after editing the file:
sudo systemctl reload sshd
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 example oauth2-client-id is required when method-priority contains device_authorization, otherwise it is ignored).{{< confkey type="string" required="yes" >}}
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+2FA
{{< confkey type="string" default="1FA+2FA" required="no" >}}
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 from PAM_AUTHTOK (set by a preceding module such as pam_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.{{< confkey type="string" default="authelia_session" required="no" >}}
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.
{{< confkey type="string" required="no" >}}
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.pem
The file must be readable by the user sshd drops privileges to during authentication (typically root for the PAM preauth child).
{{< confkey type="integer" default="60" required="no" >}}
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.
{{< callout context="note" title="Note" icon="outline/info-circle" >}}
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-authelia
This 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.
{{< /callout >}}
{{< confkey type="string" default="/usr/bin/pam_authelia" required="no" >}}
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/).
{{< confkey type="string" required="no" >}}
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.
{{< confkey type="string" required="situational" >}}
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.
{{< confkey type="string" required="situational" >}}
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.
{{< callout context="caution" title="Important Note" icon="outline/alert-triangle" >}}
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.
{{< /callout >}}
{{< confkey type="string" default="openid,authelia.pam" required="no" >}}
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.
{{< confkey type="boolean" default="false" required="no" >}}
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).
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 onlyThe 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 factorThe 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=2FA
pam_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.
{{< callout context="caution" title="Important Note" icon="outline/alert-triangle" >}}
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.
{{< /callout >}}
1FA+2FA: password then second factorThe 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+2FA
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) |
{{< callout context="caution" title="WebAuthn over SSH" icon="outline/alert-triangle" >}}
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:
method-priority is unset or contains user (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.method-priority=device_authorization or method-priority=device_authorization,user on the PAM stack.method-priority is set to an explicit list that excludes both user and device_authorization (for example method-priority=totp when the user has only WebAuthn enrolled): authentication fails with no 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.
{{< /callout >}}user entryWhen 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. Requires oauth2-client-id to 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). |
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=).
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA
account required pam_permit.so
session required pam_permit.so
auth required pam_authelia.so url=https://auth.example.com auth-level=1FA+2FA
account required pam_permit.so
session required pam_permit.so
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.so
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.so
The 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.
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.so
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 named pam with a single custom claim, authelia.pam.username, whose value is sourced from the backend's username attribute. If your backend's raw username doesn't match the Linux account verbatim (different case, an @realm suffix, etc.) anchor the claim at a derived attribute instead; see Case sensitivity and username normalization.scopes.authelia.pam: a custom OIDC scope named authelia.pam that grants the authelia.pam.username claim. The name is arbitrary but must match whatever you send via the PAM oauth2-scope option; authelia.pam is the default the Go helper expects.claims_policy: 'pam': attaches the pam claims policy to this specific client so the custom claim is actually emitted when a token is issued.scopes: ['openid', 'authelia.pam']: the client is only allowed to request these two scopes, matching the PAM module's defaults.Additional notes:
client_secret value in Authelia's configuration is a hashed representation; use authelia crypto hash generate to produce one from a random plaintext secret. The cleartext value is what you pass to pam_authelia via oauth2-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./etc/pam.d/*), set public: true on the Authelia side, omit client_secret there, and omit oauth2-client-secret in the PAM config. The claims_policy + scopes fields 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.
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.
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:
oauth2-client-id), and expiry./userinfo under Bearer authentication with the access token.userinfo.sub == id_token.sub. A mismatch here indicates someone swapped an unrelated access token in for one issued during this device flow.authelia.pam.username claim 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.
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.
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.
ssh [email protected]
You 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.
ssh [email protected]
You 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.
ssh [email protected]
A 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.
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'
Authentication failed with no useful logsIf 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 remainThis message from the SSH client means PAM returned PAM_AUTH_ERR early. Check the journal for pam_authelia lines; common causes are:
/usr/bin/pam_authelia exists and is executable, or set binary to the correct path.ca-cert is wrong or unreadable; look for failed to read CA certificate on stderr.sshd_config is missing UsePAM yes or KbdInteractiveAuthentication yes.device authorization response status=401 in the debug logThe 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 responseAuthelia is not emitting the authelia.pam.username claim on the userinfo endpoint, so pam_authelia's identity check fails closed. Check that:
custom_claims.authelia.pam.username defined, anchored at the right backend attribute.authelia.pam exists and its claims list grants authelia.pam.username.claims_policy: 'pam' (or whatever you named the policy) attached and has authelia.pam in its scopes list.oauth2-scope option includes authelia.pam so 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:
oauth2-client-id in the PAM config does not match the audience of the token Authelia issued (usually because you changed the client ID on one side without the other).sshd (or whatever PAM consumer caches the helper process) after rotating keys.url points at a reverse proxy that rewrites the issuer URL on the way through; the iss claim 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 failedThe 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.pamConfig 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 logAuthelia'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 token expiredThis 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:
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.
skip-verify mode. Connections to Authelia use TLS 1.2 or later, and verification uses the system trust store or the ca-cert you provide.status JSON field from Authelia's responses, but never request or response bodies.explicit_bzero(3)./api/oidc/device-authorization must use https://, point to the same host as url, 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./userinfo, asserts that userinfo.sub == id_token.sub, and case-sensitively compares the custom authelia.pam.username claim 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-side claims_policies plus custom-scope configuration./etc/pam.d/*. When using oauth2-client-secret the value appears in plaintext in PAM configuration files. Verify that those files are not world-readable (0644 owned by root is the default on most distributions), or use a public client to avoid the issue.POLLRDHUP on the client socket, kills the Go helper with SIGTERM, and returns PAM_AUTH_ERR. Without this, Device Authorization polling could outlive the SSH session and keep hitting Authelia's token endpoint until the device code expired.sshd's keyboard-interactive channel.method-priority is set to an explicit list that excludes both user and device_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).claims_policies: how to define the claims policy that emits authelia.pam.username.scopes: how to declare the custom authelia.pam scope.authelia.pam.username source.