Skip to content

Installing pam_ssoossh

pam_ssoossh is a PAM module that authenticates a local account by having a human approve a certificate request. This page covers getting it onto a host. Wiring it into a service is a separate decision, described on sudo and su, console login and sshd keyboard-interactive.

pam_ssoossh lives in its own repository, github.com/mnestor/ssoossh-pam. It is around 82 KB stripped and links only libraries the operating system already ships: libpam, libcrypto, libcurl. Nothing it needs is bundled, which is why it reports the versions it actually linked on every authentication.

It has both flows, the browser flow and the console code-and-QR flow, and the mode= argument that picks between them. pam_ssoossh(8) is the contract, and the reference page restates it in full.

Platform Crypto Console mode Ships an artifact
Linux glibc (x86-64, arm64) OpenSSL 1.1.1 or newer yes yes
Linux musl / Alpine OpenSSL 1.1.1 or newer yes yes
FreeBSD OpenSSL in base yes yes
macOS 15 or newer (Apple silicon) Security.framework no yes, a signed .pkg

OpenSSL 1.1.1 is a hard floor, set by RHEL 8: a build against anything older fails at compile time. OpenSSL 1.0.2 and the releases carrying it – RHEL 7, CentOS 7 – are out of scope permanently.

The crypto backend decides what can be verified, so this differs by platform. Anything unsupported fails with an error naming the algorithm, never with a vague signature failure.

CA key type in trusted-ca-file Linux, FreeBSD macOS
ecdsa-sha2-nistp256 / 384 / 521 supported supported
rsa-sha2-256, rsa-sha2-512 supported supported
ssh-ed25519 supported supported (macOS 14 and later)
ssh-rsa (SHA-1) refused by policy refused by policy

ssh-rsa names RSA with SHA-1, which OpenSSH has disabled by default since 8.8. It is refused on every platform. Check what your CA key is before you deploy: ssh-keygen -l -f /etc/ssoossh/ca.pub.

On Linux, the module and its SELinux policy are served from ssoossh’s signed repository, which saves choosing an artifact by hand – the metadata for your release offers only the build that matches it:

Terminal window
sudo dnf install pam-ssoossh # RHEL 8, 9 and 10, and rebuilds
sudo apt install pam-ssoossh # Debian, Ubuntu

With SELinux enforcing, the module cannot reach ssoosshd without policy for it. There are two ways to grant that, and neither happens on its own.

The policy package. pam-ssoossh-selinux carries a policy module scoped to the domains where the denial has actually been reproduced. It is installed separately and nothing pulls it in for you. It is published for EL 8 and EL 9 only – EL 10 is a different generation of the base policy, and shipping a module there would assert something nobody has observed on it.

A boolean Red Hat already ships. The base policy carries the same access behind authlogin_yubikey, which is off by default:

Terminal window
sudo setsebool -P authlogin_yubikey on

It grants name_connect to http_port_t for the login_pgm attribute, which holds sshd_t, local_login_t, remote_login_t and xdm_t on EL 8, 9 and 10 – plus cockpit_session_t on EL 8 and sshd_session_t on EL 10.

On EL 10 this is the route, since no policy package is published for it.

Which to choose is a real trade rather than a default. The policy package is narrower, versioned, and removable with the package. The boolean installs nothing, is supported policy from your distribution, and covers login domains the policy package deliberately does not name – which matters if your site forbids third-party SELinux modules, as regulated environments often do.

sudo needs neither, and it is worth knowing why rather than taking it on trust – the reason says when it would stop being true. sudo does not transition to a domain of its own, so the module runs as the calling user. The question is therefore always about the user’s domain and never about sudo, and every user domain the targeted policy ships already reaches http_port_t: unconfined_t through corenet_unconfined_type, and the confined ones – staff_t, user_t, sysadm_t – through an unconditional rule from their *_usertype attribute to port_type.

It is the login domains – sshd, console login – that need one of the two routes above.

For deb hosts this is the path worth taking. apt verifies repositories rather than individual files and Debian disables per-package signature checking by default, so a .deb fetched from the release page and installed with dpkg -i is verified by nothing unless you check SHA256SUMS yourself. See Installing from packages for the one-time setup, and Verifying a download below for the manual route.

Alpine, FreeBSD and macOS are not served from the repository and stay direct downloads.

Releases are built per platform and wrapped as distribution packages by nfpm, and the macOS one as an installer package built with Apple’s own tools.

Artifact For Package formats
linux-{x86_64,aarch64}-glibc-openssl3 RHEL 9 and 10 and rebuilds, Debian 12+, Ubuntu 22.04+, anything with libcrypto.so.3 .deb, .rpm
linux-{x86_64,aarch64}-glibc-openssl1.1 RHEL 8 and rebuilds, anything with libcrypto.so.1.1 .rpm
linux-{x86_64,aarch64}-musl Alpine .apk
freebsd14-x86_64 FreeBSD 14 tarball only
macos15-aarch64 macOS 15 and later on Apple silicon .pkg, Developer ID signed and notarized

There is no .deb for the OpenSSL 1.1 build: the Debian and Ubuntu releases that shipped libssl1.1 are past their support dates.

The module links the host’s libraries rather than shipping them, so the artifact name says where it loads. Package dependencies are sonames wherever the format allows – libcrypto.so.3()(64bit) for rpm, so:libcrypto.so.3 for apk – which is what rpmbuild and abuild derive on their own. Debian has no such provides, so the .deb names packages instead, carrying both spellings of the ones the 64-bit time_t transition renamed (libssl3t64 | libssl3).

Package Module path
.deb (Debian, Ubuntu) /usr/lib/<triplet>/security/pam_ssoossh.so, for example /usr/lib/x86_64-linux-gnu/security/pam_ssoossh.so
.rpm (RHEL and rebuilds) /usr/lib64/security/pam_ssoossh.so
.apk (Alpine) /lib/security/pam_ssoossh.so
.pkg (macOS) /usr/local/lib/pam/pam_ssoossh.so

On Linux and FreeBSD the module lands where that platform’s libpam already looks, so a pam.d line names it by bare filename. macOS is the exception: /usr/lib/pam, where OpenPAM looks a bare name up, is protected by System Integrity Protection, so the package installs under /usr/local/lib/pam and the pam.d line has to name that absolute path. See macOS and Touch ID.

The macOS package is signed with a Developer ID Installer certificate, notarized by Apple with the ticket stapled on, so it installs without a Gatekeeper prompt. Check it before installing:

Terminal window
spctl --assess --type install -v pam_ssoossh-v1.2.0-macos15-aarch64.pkg

Everything else is documentation:

Path Contents
/usr/share/man/man8/pam_ssoossh.8.gz The module manual page
/usr/share/man/man5/pam_ssoossh-ca.5.gz The trusted CA file format
/usr/share/man/man5/pam_ssoossh-principals.5.gz The principals map format
/usr/share/doc/pam-ssoossh/examples/pam.d/ A commented fragment for sudo, su, sshd and login
/usr/share/doc/pam-ssoossh/examples/principals.yaml A principals map to start from
/usr/share/doc/pam-ssoossh/BUILDINFO The compiler, the library versions built against, and the sonames needed

The paths the examples use for configuration – /etc/ssoossh/ca.pub and /etc/ssoossh/principals.yaml – are conventions. Nothing in the module fixes them; both are named on the pam.d line.

Each release carries a SHA256SUMS, build provenance, and, when the repository holds the signing keys, package signatures. Both public halves are attached to the release as pam-ssoossh-release-key.asc (OpenPGP) and pam-ssoossh.rsa.pub (the RSA key Alpine expects).

Terminal window
$ gpg --import pam-ssoossh-release-key.asc
$ gpg --verify SHA256SUMS.asc SHA256SUMS
$ sha256sum -c SHA256SUMS --ignore-missing

Per format:

Terminal window
$ rpm --import pam-ssoossh-release-key.asc && rpm -K pam-ssoossh-*.rpm
$ sudo cp pam-ssoossh.rsa.pub /etc/apk/keys/ && sudo apk add ./pam-ssoossh-*.apk

The .apk public key filename is what apk matches the signature against, and it must land as exactly /etc/apk/keys/pam-ssoossh.rsa.pub.

For the .deb, apt verifies repositories rather than files, so the SHA256SUMS signature is the check that matters. The embedded _gpgorigin signature can be checked with dpkg-sig --verify where that tool exists.

GitHub’s own build provenance:

Terminal window
$ gh attestation verify pam-ssoossh_1.2.0_linux-glibc-openssl3_amd64.deb -R <owner>/<repo>

A release built without the signing secrets is unsigned, and the release job log says so. SHA256SUMS is still published either way.

A tag with a hyphen (v1.2.0-rc1) is a pre-release. Package versions come from git describe, and a pre-release sorts before the release it precedes in every format, so an untagged or release-candidate build can never outrank a real one on a host.

Build dependencies are libpam, libcrypto and libcurl:

Platform Packages
Debian, Ubuntu build-essential pkg-config libssl-dev libcurl4-openssl-dev libpam0g-dev
RHEL 8+, AlmaLinux, Rocky gcc make pkgconf-pkg-config openssl-devel libcurl-devel pam-devel
Alpine build-base pkgconf openssl-dev curl-dev linux-pam-dev
FreeBSD gmake pkgconf curl – libpam and OpenSSL are in base, libcurl is not
macOS 15+ Xcode command line tools only. No OpenSSL, no Homebrew
Terminal window
$ make # pam_ssoossh.so, or pam_ssoossh.bundle on macOS
$ make test # symbol gate, load gate, and the unit suite
$ make dist # the release tarball
$ make packages # deb, rpm and apk from that tarball, needs nfpm on PATH

Two vendored libraries are compiled in rather than linked – a JSON tokenizer and the QR encoder console mode draws with – so neither appears in ldd output or in the exported symbol table. Every link-time dependency is a library the operating system already ships.

For a host whose distribution has stopped issuing OpenSSL updates, build against a self-maintained one without changing any source:

Terminal window
$ make OPENSSL_PREFIX=/opt/openssl-3.5
  1. Put the CA public key where the module will read it – the trusted CA file.
  2. Write a principals map, unless every identity provider username is spelled exactly like the local account it logs in as – the principals map.
  3. Add the module to one service, from a second root session – sudo and su.

There is no automatic stack registration on RHEL

Section titled “There is no automatic stack registration on RHEL”

Step 3 is a per-service edit on every platform, and on RHEL and its rebuilds that is the whole story: no authselect feature is provided, and that is a decision rather than an omission.

authselect features work by modifying system-auth and password-auth. Those are exactly the files that make a shared include dangerous for this module: a screen locker, display manager or polkit reads them too, and an approval prompt that nobody can see is a lock-out rather than a failed login. So an authselect feature could not be safer than editing the stack by hand – it is the same trade with different filenames, applied to more services at once.

Editing one service file directly is not a workaround here. authselect owns system-auth and password-auth; it does not own /etc/pam.d/sudo or the other per-service files, so an edit there is stable and survives authselect apply-changes.

Debian and Ubuntu do ship a pam-auth-update profile, because that platform has no comparable per-service mechanism. It installs disabled and nothing enables it for you – turning it on is an explicit, informed choice, and the same lock-out warning applies when you make it.