sudo and su
pam_ssoossh authenticates a local sudo or su by generating a single-use
keypair, asking ssoosshd to certify it, printing an approval URL, waiting for
a human to approve it in a browser, and validating the certificate that comes
back. Nothing is written to disk, and the certificate is discarded the moment
it has been checked.
Before you start
Section titled “Before you start”- The module is installed – Installing pam_ssoossh.
- The CA public key is on the host – the trusted CA file.
- A principals map exists, unless every user’s identity provider username is
spelled exactly like the local account they
sudoas – the principals map. This is the part that surprises people: the certificate names the approver, never the local account that was asked for.
The sudo stanza
Section titled “The sudo stanza”One auth sufficient line in /etc/pam.d/sudo, above the line that pulls in
password authentication:
auth sufficient pam_ssoossh.so server=https://ssoossh.example.com \ trusted-ca-file=/etc/ssoossh/ca.pub \ principals-map=/etc/ssoossh/principals.yaml \ skew-tolerance=2s timeout=60sauth required pam_unix.soThe ssoossh line is the same everywhere. What it goes above differs:
| Distribution | The line it goes above |
|---|---|
| Debian, Ubuntu | @include common-auth |
| RHEL, AlmaLinux, Rocky | auth include system-auth. authselect owns /etc/pam.d/system-auth, so edit /etc/pam.d/sudo |
| Alpine | auth include base-auth |
The rest of the file stays exactly as your distribution shipped it. On Debian
that means @include common-account and
@include common-session-noninteractive are untouched below.
The su stanza
Section titled “The su stanza”Same shape, with two neighbours that matter:
auth sufficient pam_rootok.so# auth required pam_wheel.so (if your distribution has it)auth sufficient pam_ssoossh.so server=https://ssoossh.example.com \ trusted-ca-file=/etc/ssoossh/ca.pub \ principals-map=/etc/ssoossh/principals.yaml \ timeout=60sKeep pam_rootok.so first, so root switching to another account still needs
no approval. Keep any pam_wheel.so line your distribution ships above the
ssoossh line: a group restriction on who may su at all should not be
bypassable by an approval.
Then the distribution’s include – @include common-auth on Debian and
Ubuntu, auth include system-auth on RHEL and its rebuilds,
auth include base-auth on Alpine.
Why sufficient
Section titled “Why sufficient”sufficient means an approval ends the auth stack, and anything else hands
over to the next module – normally pam_unix.so, so a local password still
works.
A denied approval therefore reaches the password prompt. That is the intended
posture rather than an oversight: ssoossh is an additional way to
authenticate, not a gate that can lock an operator out of a host when the
browser flow is unavailable. An outage of the ssoossh server degrades to “no
browser approval available”, not “no sudo on any host”.
required or requisite inverts that. The whole auth group fails when
ssoosshd is unreachable, and nothing later in the stack gets a chance to
succeed, so an ssoossh outage becomes a sudo outage everywhere. Choose it
only if that trade-off is genuinely wanted – password sudo disallowed for
compliance reasons, say – and understand that it makes the ssoossh server a
hard dependency of every host’s recovery path.
What each outcome does
Section titled “What each outcome does”| At the terminal | Module returns | With sufficient |
|---|---|---|
| Approved, and all four checks pass | PAM_SUCCESS |
sudo proceeds; nothing else in the stack runs |
| The approver denied it | PAM_AUTH_ERR |
Falls through to the password prompt |
Nobody answered before timeout |
PAM_AUTH_ERR |
Falls through to the password prompt |
| The request expired server-side, the server could not issue, or a check failed | PAM_AUTH_ERR |
Falls through to the password prompt |
ssoosshd unreachable, or answered with something that was not an answer |
PAM_AUTHINFO_UNAVAIL |
Falls through to the password prompt |
| Ctrl-C at the approval prompt | PAM_IGNORE |
The module contributes nothing; the stack continues |
ssh-only is set and the login is local |
PAM_IGNORE |
Nothing is sent at all; the stack continues to the next factor |
server missing, or pam_get_user failed |
PAM_USER_UNKNOWN |
Falls through; nothing was ever sent |
| Trusted CA file missing, unreadable, or holding no usable key | PAM_NO_MODULE_DATA |
Falls through; nothing was ever sent |
Every failure logs its reason to syslog under LOG_AUTHPRIV. Nothing is
written to stdout or stderr, because both belong to sudo.
The distinction between PAM_AUTH_ERR and PAM_AUTHINFO_UNAVAIL is
deliberate: a server that answered and said no is an authentication failure; a
server that could not be reached is missing information, which is what lets a
sufficient stack fall through cleanly.
The server side
Section titled “The server side”PAM certificates are their own certificate type, configured under
cert_options.pam:
cert_options: pam: # Who may approve a PAM request at all, deployment-wide. require: group: staff
# Validated once by the module and discarded. Seconds, not hours. valid_duration: 30s
http: cert_request_rate_limit: pam: 10 # per second, per source addresscert_options.pam.valid_durationdefaults to30s. Set it together with the module’sskew-tolerance: the window is short on purpose, and drift between the host and the server eats into it. See troubleshooting.cert_options.pam.requiregates who may approve a PAM request across the whole deployment. Which local account an approved certificate then authorizes is the host’s decision, made against its ownprincipals-map.cert_options.pam.key_id_templatedeliberately never falls back to theusertemplate, so asudoand an SSH login by the same person stay distinguishable in an audit log.http.cert_request_rate_limit.pambounds request creation per source address.
macOS and Touch ID
Section titled “macOS and Touch ID”On a Mac the interesting case is that local logins already have a good factor
and remote ones do not. Touch ID is right there for someone at the keyboard,
and useless over ssh. The ssh-only
argument splits the two so each gets the factor that fits.
Put the line in /etc/pam.d/sudo_local, not in sudo. macOS includes
sudo_local from sudo and leaves it alone across OS updates. Create it from
Apple’s template if it is not there:
sudo cp /etc/pam.d/sudo_local.template /etc/pam.d/sudo_localauth sufficient /usr/local/lib/pam/pam_ssoossh.so server=https://ssoossh.example.com \ trusted-ca-file=/etc/ssoossh/ca.pub \ principals-map=/etc/ssoossh/principals.yaml \ ssh-only timeout=60sauth sufficient pam_tid.soA login at the console returns PAM_IGNORE from the ssoossh line and goes on
to pam_tid.so, which is Touch ID. A login that arrived over ssh, where
there is no Touch ID to offer, gets the browser flow. sufficient still means
a denial, a timeout, Ctrl-C or an unreachable server falls through to whatever
follows, which here is Touch ID and then the password prompt.
The module is named by absolute path because /usr/lib/pam, where OpenPAM
looks a bare name up, is protected by System Integrity Protection. The
installer package puts the module under /usr/local/lib/pam instead.
ssh-only is not macOS-only. Any host whose local logins carry a factor a
remote login cannot use is the same shape, a workstation with a smartcard
reader being the other common one.
A safe rollout
Section titled “A safe rollout”Do this on one host first, and keep it to one service at a time.
- Pick a test host that is not the one you administer everything else
from, and confirm you have a way back into it that does not use
sudo– console access, or a root SSH key. - Open a second root session and leave it open.
sudo -iin another terminal is enough. Do not close it until step 8. - Back up the file:
cp /etc/pam.d/sudo /etc/pam.d/sudo.bak. - Check the pieces before wiring them in.
ssh-keygen -l -f /etc/ssoossh/ca.pubshould list your CA key. The principals map should name every account that must keep working, including the one you are about to test with. - Add the line above the password include, exactly as above. Save.
- Test from a third, fresh terminal: run
sudo -v. You should get an approval URL. Approve it in a browser and the command should succeed. - Test the fallback. Press Ctrl-C at the prompt and confirm you reach the password prompt and can still authenticate. Then deny an approval in the browser and confirm the same. These are the paths that matter during an outage.
- Read syslog either way –
journalctl -t sudoorgrep pam_ssoossh /var/log/auth.log– and confirm the version line and the outcome are there. Only now close the second root session. - Roll out with configuration management, one wave at a time, and watch
for
PAM_AUTHINFO_UNAVAILand principals-map warnings in the fleet’s logs before widening.
Add debug to the line while testing. It logs the decision for each of the
four checks, the URL sent to the terminal, and the request context at
LOG_DEBUG. Take it off afterwards.
What it looks like in use
Section titled “What it looks like in use”An on-call engineer is paged at 03:00 and needs to restart a service on
web01. They SSH in with their ssoossh certificate as usual, then:
$ sudo systemctl restart nginxApprove this request in your browser:https://ssoossh.example.com/approve/6f1c0a5e-1f2b-4c3d-8e9f-0a1b2c3d4e5fThey open the URL on the laptop they are already signed in on. The approval
page names the machine, the service (sudo), the terminal, the account being
authenticated, and the command line – sudo systemctl restart nginx – all
of it reported by the host and labelled as the host’s own claim, alongside the
source address the server observed for itself. They approve; the certificate
is signed, delivered to the still-waiting terminal, checked, and discarded;
nginx restarts.
Three things came out of that which a typed password would not have produced.
There was no shared or reused secret on the host. The audit trail names who
approved, from where, and what was granted. And when that engineer leaves the
company, disabling them in the identity provider ends their sudo on every
host at once, without touching a single /etc/pam.d file.