Tessera architecture
This document is the single reference architecture for version 0.4.0. After reading it, an engineer should be able to answer correctly:
- what happens when
pam_sm_authenticateis called? - what lives in
/run/tessera/? - what does
monitorddo on a udev REMOVE event? - how is the IPC serialized, and which messages flow between the PAM module
and
monitord?
1. Goals and non-goals
Section titled “1. Goals and non-goals”1.1 What Tessera does
Section titled “1.1 What Tessera does”- Authenticates a local UNIX user with an X.509 certificate on a USB medium or a PKCS#11 token.
- Binds the bearer to the machine and to the role account through the X.509 v3
extensions
pam_cert_host_bindingandpam_cert_allowed_roles, embedded in the leaf certificate itself. - Monitors the state of the USB medium throughout the session and reacts to its removal (lock / logout / hook / shutdown).
- Handles suspend/resume correctly.
- Delegates GOST cryptography to the certified
gost-engine.
1.2 What Tessera does NOT do
Section titled “1.2 What Tessera does NOT do”- Does not implement its own cryptography (everything goes through OpenSSL and
gost-engine). - Does not manage the CA lifecycle (certificate issuance/revocation is the job of an external CA).
- Does not manage token PINs (that is the job of the administrator and the user).
- Does not protect against compromise of the root account or the OS kernel (outside the TOE).
- Performs network requests only on the revocation path in OCSP modes
(
mode ∈ {ocsp, crl_then_ocsp}): a synchronous HTTP POST to the config-suppliedocsp_responder_url, with a hard timeout and an on-disk cache. In thenone/crlmodes there is no network at all (offline CRL). Zero-egress environments (terminals) stay onnone/crl.
The full description of the TOE boundaries is in docs/threat-model.md.
2. Components
Section titled “2. Components”tessera is a workspace of six crates plus one OS integration
(systemd, udev, logind). Below are the four runtime crates that run on the
device. The other two belong to certificate issuance: tessera_ext — the
shared definitions of Tessera’s X.509 extensions (OIDs, DER codecs), used by
both the core and the issuance tools; tessera_issuer — the issuance tools,
whose pure-Rust core also builds for wasm32 (the external web cabinet links
it as its WASM core), described in issuer.md.
2.1 tessera_core (rlib)
Section titled “2.1 tessera_core (rlib)”The synchronous core. It contains:
- Loading and validation of the configuration (
config::raw::RawConfig→config::validated::ValidatedConfig). - X.509 parsing and verification (
x509/). - Trust chains and CRL checking (
trust/,crl/). - Challenge-response (
challenge/). - GOST delegation via
gost-engine(gost/). - PKCS#12 and PKCS#11 (
pkcs12/,token/). - USB mount and the MountGuard RAII (
usb/,mount/). - Hooks (
hooks/). - Host identity chain (
host_identity/). - Cert-scope verification — parsing of the
pam_cert_host_binding/pam_cert_allowed_rolesextensions and matching their entries againsthost_id_hashand the requested role, which is alsopam_user(x509/,verify_cert_scope). - IPC client side (
ipc/).
No tokio, no async. All operations are blocking — and this is justified: the
PAM module is called from the synchronous libpam context.
2.2 tessera_proto (rlib)
Section titled “2.2 tessera_proto (rlib)”The wire protocol for the IPC between the PAM module and the daemon. It contains:
ClientMessageandServerMessage— the message variants (crates/tessera_proto/src/client.rs,.../server.rs).WireErrorand the encode/decode functions (wire.rs).framing::FramingError— NDJSON framing.SessionTarget— encodes the tty/display/logind-id for a specific session.PROTOCOL_VERSION— the current value is2.
#![forbid(unsafe_code)] — the crate is purely safe.
2.3 pam_tessera (cdylib libpam_tessera.so)
Section titled “2.3 pam_tessera (cdylib libpam_tessera.so)”The PAM service module. It contains:
- The PAM entry points:
pam_sm_authenticate,pam_sm_setcred,pam_sm_acct_mgmt,pam_sm_open_session,pam_sm_close_session(seecrates/pam_tessera/src/entry.rs). - Panic guard (
panic_guard.rs) — every C boundary is protected bycatch_unwind; a panic maps toPAM_AUTHINFO_UNAVAIL. - DI wiring (
di.rs) — assembles the core dependencies from the config. - Flow orchestrator (
flow.rs) — the main authorization pipeline. - PAM conversation helpers (
pam_conv.rs). - Persistent data between
pam_sm_*calls (data_handle.rs).
It is built into /lib/security/pam_tessera.so (see
debian/rules).
2.4 tessera_cli (binary tessera)
Section titled “2.4 tessera_cli (binary tessera)”A multi-command CLI: tessera daemon (the long-running daemon, unit
tessera.service with the launch line
/usr/bin/tessera daemon --config /etc/tessera/config.toml),
tessera check, tessera dump-host-id, tessera role, tessera tags,
tessera enroll. The daemon owns:
- The IPC socket (
/run/tessera/monitord.sock). - udev monitoring of USB devices (
udev_monitor.rs). - The D-Bus connection to
systemd-logind(logind.rs). - The registry of active sessions (
registry.rs,state.rs).
It is built on tokio multi-thread (see main.rs). It uses sd_notify to
integrate with systemd Type=notify. It is built into /usr/bin/tessera and
shipped by the unit
tessera.service.
2.5 External dependencies
Section titled “2.5 External dependencies”| Component | Source | Trust |
|---|---|---|
libpam0g | system, Astra/Debian repo | yes |
libssl3 | system, Astra/Debian repo | yes |
gost-engine | Astra SE 1.7+ (FSB-certified CSP) | yes (part of the certified OS) |
librtpkcs11ecp.so | Rutoken, shipped separately | yes (FSB-certified CSP) |
libjcPKCS11.so | JaCarta, shipped separately | yes (FSB-certified CSP) |
libudev1 | system, Astra/Debian repo | yes |
libdbus-1-3 | system, Astra/Debian repo | yes |
libsystemd0 | system, Astra/Debian repo | yes |
GOST and PKCS#11. Signing with GOST algorithms works only on the PKCS#12 path — through
gost-enginein OpenSSL. On the PKCS#11 path (librtpkcs11ecp.so,libjcPKCS11.so), GOST mechanisms are not supported (thecryptokicrate does not cover GOST mechanisms); Rutoken/JaCarta over PKCS#11 are usable for RSA/ECDSA certificates. GOST-over-PKCS#11 support is the proposalopenspec/changes/gost-pkcs11.
3. Crate dependency diagram
Section titled “3. Crate dependency diagram”flowchart TD libpam[libpam.so] --> cdylib[libpam_tessera.so] cdylib --> core[tessera_core] cdylib --> proto[tessera_proto] monitord[tessera] --> proto monitord --> core cdylib -. "AF_UNIX SOCK_STREAM NDJSON" .-> monitord core --> openssl[libssl3 + gost-engine] core --> pkcs11[PKCS#11 module] monitord --> udev[libudev] monitord --> dbus[libdbus / logind]4. PAM call lifecycle
Section titled “4. PAM call lifecycle”The PAM stack makes several calls in the order auth → account → session.
tessera handles all of them, but the real work is in
pam_sm_authenticate. The rest read the stored AuthContext from PAM data.
4.1 pam_sm_authenticate
Section titled “4.1 pam_sm_authenticate”- Unpack the module arguments (
config=...). - Load and validate
config.toml(viatessera_core::config::load_validated_config). On error —PAM_AUTHINFO_UNAVAIL. - Run
self_check(engine, paths, hooks placeholders). On error —PAM_AUTHINFO_UNAVAIL. - Read
PAM_USER,PAM_SERVICE,PAM_TTYfrom libpam. The requested role is derived fromPAM_USER: the login account name IS the role. There is no other source of the role, andPAM_USERis never rewritten. A name that does not match therole_idformat refuses the login before the medium is touched. - Assemble the DI graph via
di::wire(mount, trust, token). - Resolve
host_idthrough the chain of sources from the config and computehost_id_hash = sha256(host_id). - Run
flow::authenticate(ctx):- mount the USB or open a PKCS#11 session;
- find the certificate, verify the chain and revocation;
- challenge-response with the private key;
- extract the
pam_cert_host_bindingandpam_cert_allowed_rolesextensions from the leaf certificate and match them againsthost_id_hashand the requested role viaverify_cert_scope.pam_cert_allowed_rolesis the sole source of admission into the account:PAM_USERIS the role, so one list also answers the question “into which account is the bearer admitted”. No fallback to the device configuration exists; an absent extension is a denial, not a switch to another mechanism (return codes — see §13).
- On success — build the
AuthContextand store it viapam_set_data. - Send
Hello+SessionOpento monitord (receiveAck). - Return
PAM_SUCCESS. Any error maps toPAM_AUTHINFO_UNAVAIL/PAM_PERM_DENIED/PAM_MAXTRIES/PAM_AUTH_ERR/PAM_SYSTEM_ERRper the semantics offlow::FlowError::pam_code(the table is in §13).
4.2 pam_sm_setcred
Section titled “4.2 pam_sm_setcred”Does nothing beyond PAM_SUCCESS. Certificates are not placed in the user’s
keyring.
4.3 pam_sm_acct_mgmt
Section titled “4.3 pam_sm_acct_mgmt”Reads the AuthContext and checks that:
- the certificate’s
notAfterhas not yet expired (with a tolerance ofclock_skew_seconds; the value is taken from the config at the moment ofpam_sm_authenticateand stored in theAuthContext).
On a mismatch it returns PAM_ACCT_EXPIRED.
4.4 pam_sm_open_session
Section titled “4.4 pam_sm_open_session”Reads the AuthContext. Sends SessionOpen to monitord with the full
payload (see client.rs::SessionOpenPayload):
session_id(UUID);pam_user,pam_service;target(Tty / Display / LogindSession);usb_serial— the serial of the medium that authorized the session;host_id_hash— hex SHA-256 ofhost_id;opened_at— wall-clock unix time;cert_cn,cert_serial;engineer_ski— the lowercase-hexSubjectKeyIdentifierof the engineer’s certificate (v2; an empty string on frames from a v1 client);engineer_cert_sha256— the lowercase-hexSHA-256(cert DER)of the engineer’s leaf (v2);uid— the Unix uid that the PAM module authenticated (v2;0when absent from a v1 client’s frame);role,role_version— the role id and the version of the role snapshot the session was opened with (optional v2 NDJSON fields — optional for compatibility with frames from earlier versions; in the current version every login carries a role).
Monitord adds the session to the registry and starts monitoring the USB.
4.5 pam_sm_close_session
Section titled “4.5 pam_sm_close_session”Sends SessionClose { session_id, closed_at }. Monitord removes the session
from the registry and does not trigger on_usb_removed — the user
explicitly ended the session.
5. Runtime file layout
Section titled “5. Runtime file layout”What tessera keeps on disk while running, who writes and who reads each
path. The diagram below is the overall map; the table gives the exact owners
and permissions.
flowchart LR etc["/etc/tessera/"] --> cfg[config.toml] etc --> ca["ca/bundle.pem"] etc --> crl["crl/*.pem"] run["/run/tessera/"] --> sock[monitord.sock] run --> sessions["sessions/sid/"] run --> state[sessions.json] run --> lock[daemon.lock] var["/var/lib/tessera/ (root-owned policy)"] --> daemon["daemon/ (tessera-owned state)"] daemon --> wp["wallpaper.orig.jpg"]| Path | Written by | Read by | Permissions |
|---|---|---|---|
/etc/tessera/config.toml | administrator | cdylib + monitord | 0640 root:root |
/etc/tessera/ca/bundle.pem | administrator | cdylib + monitord | 0640 root:root |
/run/tessera/monitord.sock | monitord | cdylib | 0660 tessera:tessera |
/run/tessera/sessions/<sid>/ | cdylib | removed by MountGuard on drop | 0700 root:root |
/run/tessera/sessions.json | monitord | monitord (between daemon restarts within a boot; tmpfs, volatile) | 0600 tessera:tessera |
/run/tessera/daemon.lock | monitord (flock singleton; next to sessions.json, fallback /var/lib/tessera/daemon/) | monitord | — |
/var/lib/tessera/roles/*.toml, /var/lib/tessera/tags.toml | administrator | cdylib | root-owned; no group/world write on the complete path |
/var/lib/tessera/daemon/wallpaper.orig.jpg | monitord | monitord | daemon state under 0750 tessera:tessera |
/var/cache/tessera/ocsp/*.der | cdylib (auth path, OCSP cache) | cdylib (with re-verification before use) | 0640 root:root (directory 0750 root:root) |
/run/tessera/ and /var/lib/tessera/daemon/ are created by systemd
through the RuntimeDirectory and nested StateDirectory directives of the unit.
The package keeps /var/lib/tessera/ itself 0750 root:tessera, so the
unprivileged daemon cannot replace the trusted role/tag trees.
(see tessera.service
and dist/tmpfiles/tessera.conf).
/var/cache/tessera/ocsp/ is created by the package’s postinst
(debian/postinst).
6. Sequence diagram — pam_sm_authenticate happy path with PKCS#11
Section titled “6. Sequence diagram — pam_sm_authenticate happy path with PKCS#11”sequenceDiagram participant U as User participant L as libpam participant P as cdylib participant C as core participant T as PKCS#11 module participant M as monitord
U->>L: login attempt L->>P: pam_sm_authenticate P->>C: load_validated_config P->>C: self_check P->>C: resolve_host_identity C->>T: C_OpenSession P->>U: prompt for PIN U->>P: PIN P->>T: C_Login T-->>P: cert + privkey handles C->>C: build_chain + verify_chain C->>C: revocation check P->>U: prompt for challenge P->>T: C_Sign(challenge) T-->>P: signature C->>C: verify(signature, pubkey, challenge) C->>C: parse_cert_extensions + verify_cert_scope P->>M: Hello(protocol_version=2) M-->>P: HelloAck P->>L: PAM_SUCCESS7. Sequence diagram — pam_sm_open_session + IPC SessionOpen
Section titled “7. Sequence diagram — pam_sm_open_session + IPC SessionOpen”sequenceDiagram participant L as libpam participant P as cdylib participant M as monitord
L->>P: pam_sm_open_session P->>P: read AuthContext from pam_data P->>M: SessionOpen payload M->>M: registry.insert(session_id) M->>M: start udev watch for usb_serial M-->>P: Ack P->>L: PAM_SUCCESS8. Sequence diagram — USB removal → grace → lock
Section titled “8. Sequence diagram — USB removal → grace → lock”sequenceDiagram participant K as kernel/udev participant M as monitord participant L as logind participant U as User session
K->>M: udev REMOVE event M->>M: lookup session by serial M->>M: start grace timer alt USB not returned within grace M->>L: LockSession(id) L->>U: lock screen else USB returned K->>M: udev ADD event M->>M: cancel grace timer M->>M: log removal cancelled endBehavior of on_usb_removed:
"lock"—LockSession(default)."logout"—TerminateSession."hook"— runs theusb_removedhook."shutdown"—PowerOffvia D-Bus to logind.
9. Sequence diagram — suspend / resume
Section titled “9. Sequence diagram — suspend / resume”sequenceDiagram participant L as logind participant M as monitord
L->>M: PrepareForSleep(true) M->>M: snapshot active sessions Note right of M: udev REMOVE events<br/>during suspend<br/>will be marked transient L->>M: PrepareForSleep(false) M->>M: arm suspend_grace timer Note right of M: while the timer is active,<br/>any udev REMOVE<br/>with reinsertion within grace<br/>is ignoredWith monitor_fail_mode = "strict", the cdylib waits for an Ack from
monitord until the timeout; with "permissive" it survives brief
unavailability.
10. IPC wire protocol
Section titled “10. IPC wire protocol”10.1 Transport
Section titled “10.1 Transport”AF_UNIXSOCK_STREAM.- Socket path:
/run/tessera/monitord.sock. - Permissions:
0660 tessera:tessera(see tmpfiles + systemd RuntimeDirectory). - Peer authentication:
SO_PEERCRED— monitord checks thatuid == 0. Any other peer is closed. - Implementation:
crates/tessera_cli/src/peercred.rs.
10.2 Framing
Section titled “10.2 Framing”Newline-delimited JSON (NDJSON):
- each frame is a single UTF-8 JSON line;
- the terminator is a single
\nbyte; - the maximum frame size is
MAX_FRAME_BYTES = 64 KiB(seecrates/tessera_proto/src/wire.rs).
Rationale for choosing NDJSON:
- standard tools (jq, the journalctl formatter) can process it without special support;
- framing is trivial — the
\ndelimiter; - the cost of parsing JSON is justified by the low message rate (≤ 10 per second on a typical day).
10.3 Versioning
Section titled “10.3 Versioning”PROTOCOL_VERSION: u32 = 2(seecrates/tessera_proto/src/version.rs). Version 2 addedGetActiveSessionByUid/ActiveSession, the optionalSessionOpenfields (engineer_ski,engineer_cert_sha256,uid, plus the optionalrole/role_version) and the error codeNO_ACTIVE_SESSION(1200); frames from a v1 client without the new fields still deserialize.- The first frame on any connection is
Hello { protocol_version }. - If
protocol_versiondoes not equal the server’s, monitord replies withError { code: 1000 (PROTOCOL_MISMATCH) }and closes the connection. - Version semantics: a MAJOR mismatch → disconnect; MINOR (if any appear) — best-effort backward compatibility.
10.4 Messages
Section titled “10.4 Messages”Client → Server (ClientMessage)
Section titled “Client → Server (ClientMessage)”From crates/tessera_proto/src/client.rs:
{"type": "hello", "protocol_version": 2, "agent": "libpam_tessera/0.4.0"}{"type": "session_open", "session_id": "1c5e8a90-3b6f-4a1d-9c2e-77f0b1c2d3e4", "pam_user": "alice", "pam_service": "sudo", "target": {"kind": "logind_session", "id": "12"}, "usb_serial": "RUTOKEN-001", "host_id_hash": "ee0bd4f3a3c8e21d4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f", "opened_at": 1735689600, "cert_cn": "Alice", "cert_serial": "01a2b3c4d5e6f70809"}{"type": "session_close", "session_id": "1c5e8a90-3b6f-4a1d-9c2e-77f0b1c2d3e4", "closed_at": 1735689700}{"type": "ping"}Server → Client (ServerMessage)
Section titled “Server → Client (ServerMessage)”From crates/tessera_proto/src/server.rs:
{"type": "hello_ack", "server_version": "0.4.0", "protocol_version": 2}{"type": "ack"}{"type": "pong"}{"type": "error", "code": 1000, "message": "protocol version mismatch"}10.5 Timeout and expected responses
Section titled “10.5 Timeout and expected responses”There are no separate per-frame timeouts. The client applies a single
configurable monitor.timeout_ms (default 2000 ms, range 100..=60000 ms) to
the whole connection — the value is set via set_read_timeout /
set_write_timeout on the socket at MonitordClient::connect (see
crates/tessera_core/src/ipc/client.rs)
and covers both the handshake and all subsequent RPCs on that connection.
| Initiator | Message | Recipient | Expected response | Action on timeout |
|---|---|---|---|---|
| client | Hello | server | HelloAck or Error | close the connection |
| client | SessionOpen | server | Ack or Error | per monitor_fail_mode |
| client | SessionClose | server | Ack | log + continue |
| client | Ping | server | Pong | log + continue |
10.6 Error codes
Section titled “10.6 Error codes”From crates/tessera_proto/src/server.rs:
| Code | Name | Semantics | cdylib action |
|---|---|---|---|
| 1000 | PROTOCOL_MISMATCH | The protocol versions did not match. | fail-closed |
| 1001 | DEVICE_GONE | The USB device with usb_serial is absent. | fail-closed |
| 1003 | UNAUTHORIZED | The peer is not uid=0 (per SO_PEERCRED). | disconnect |
| 1100 | BAD_REQUEST | Invalid frame (schema violation). | disconnect + log |
| 1101 | PROTOCOL_VIOLATION | Wire-protocol violation: an oversize frame, an idle timeout, etc. The server closes the connection after sending. | disconnect + log |
| 1200 | NO_ACTIVE_SESSION | No active session for the requested uid (a v2 daemon’s reply to GetActiveSessionByUid). | ordinary “not found” |
| 1500 | INTERNAL | Internal daemon error. | per monitor_fail_mode |
DEVICE_GONE and UNAUTHORIZED are always fatal — they change the
authentication verdict and are propagated even in permissive
(ipc/failmode.rs). The other errors are governed by the
monitor_fail_mode policy at the specific call-site; on the auth path a
failure to notify monitord does not undo an authentication success that has
already happened (see §13).
10.7 JSON schema of SessionOpenPayload
Section titled “10.7 JSON schema of SessionOpenPayload”{ "title": "SessionOpenPayload", "type": "object", "properties": { "session_id": {"type": "string", "format": "uuid"}, "pam_user": {"type": "string"}, "pam_service": {"type": "string"}, "target": {"type": "object"}, "usb_serial": {"type": ["string", "null"]}, "host_id_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "opened_at": {"type": "integer"}, "cert_cn": {"type": "string"}, "cert_serial": {"type": "string", "pattern": "^[0-9a-f]+$"}, "engineer_ski": {"type": "string", "pattern": "^[0-9a-f]*$"}, "engineer_cert_sha256": {"type": "string", "pattern": "^[0-9a-f]*$"}, "uid": {"type": "integer"}, "role": {"type": ["string", "null"]}, "role_version": {"type": ["integer", "null"]} }, "required": ["session_id", "pam_user", "pam_service", "target", "host_id_hash", "opened_at", "cert_cn", "cert_serial"]}The engineer_ski, engineer_cert_sha256, uid fields are from version 2: a
v2 client always serializes them, while a v1 client’s frame without them
deserializes to the defaults (empty string / 0), which is why they are not
listed in required. role / role_version are present only when the
session is opened with a role.
11. Threading and concurrency model
Section titled “11. Threading and concurrency model”11.1 cdylib
Section titled “11.1 cdylib”- Fully synchronous, no
tokio. - The connection to monitord is single per
pam_sm_*call; it is closed after the response. - No shared mutable state: each PAM call has its own
flow::Context.
11.2 monitord
Section titled “11.2 monitord”tokiomulti-thread runtime (the number of worker threads is tokio’s system default by default).- One dedicated task per incoming connection
(
server.rs::handle_connection). - The session registry is a
Mutex<RegistryStore>(seeregistry.rs). - udev and logind have their own dedicated long-running tasks.
- Writing
/run/tessera/sessions.jsonis an atomic rename via a tempfile (no flock; a separate singleton lockdaemon.lockguards against a double start of the daemon). The file lives on tmpfs (RuntimeDirectory=tessera), intentionally volatile: the registry is needed only between daemon restarts within a single boot — all the processes holding these sessions (sshd/login/sudo) die on reboot anyway.
11.3 Shared access to /run/tessera/sessions/
Section titled “11.3 Shared access to /run/tessera/sessions/”- The cdylib creates the
<sid>directory viaMountGuard::new(RAII). - It removes the directory in
Drop(or inpam_sm_close_session). - monitord does not write to this directory directly — it only reads it for diagnostics.
12. Host identity chain
Section titled “12. Host identity chain”host_id is computed at the moment of pam_sm_authenticate through a chain
of sources from the [host_identity] section. The implementation is
crates/tessera_core/src/host_identity/chain.rs.
The sources in order of preference:
machine_id—/etc/machine-id(stable across reboots, changes on reinstall).dmi_board_serial—/sys/class/dmi/id/board_serial(stable at the hardware level, changes when the motherboard is replaced).dmi_system_uuid/dmi_system_serial— the system’s DMI identifiers (stability depends on the vendor/hypervisor).hostname—/etc/hostname(unstable, easily spoofed; OK for tests).custom_command— an administrator’s script.override— a fixed value from the config (bootstrap/tests).
The chain is traversed in the order given in sources. The first non-empty
result wins. If all sources are empty:
fallback = "deny"→PAM_AUTH_ERR(production default);fallback = "warn"→PAM_SUCCESSwith a warning log (test environment);fallback = "allow"→PAM_SUCCESSsilently (dangerous, do not use).
13. Fail-closed rules
Section titled “13. Fail-closed rules”The module is designed fail-closed: a failure of any certificate check (the trust chain, revocation, challenge-response, host/user binding) leads to a denied login, not a pass-through. The table below lists the conditions and the PAM codes the stack receives; the single deliberate exception — monitord being unavailable does not undo an authentication success that has already happened — is set out in “Principles” below the table.
| # | Condition | Return |
|---|---|---|
| 1 | panic in any pam_sm_* | PAM_AUTHINFO_UNAVAIL (9) |
| 2 | loading config.toml failed | PAM_AUTHINFO_UNAVAIL (9) |
| 3 | self_check failed (engine, paths, hooks) | PAM_AUTHINFO_UNAVAIL (9) |
| 4 | USB/mount/discovery produced no medium, the PKCS#11 module did not load | PAM_AUTHINFO_UNAVAIL (9) |
| 5 | the certificate fails chain verification | PAM_PERM_DENIED (6) |
| 6 | revocation check failed (crl: serial in the CRL, CRL absent/stale; ocsp/crl_then_ocsp: responder unreachable, timeout, status unknown/revoked, invalid response signature) | PAM_PERM_DENIED (6) |
| 7 | challenge-response did not match | PAM_PERM_DENIED (6) |
| 8 | the requested role is not covered by pam_cert_allowed_roles — the extension is absent, invalid (the list is treated as empty), or does not contain the role | PAM_PERM_DENIED (6) |
| 9 | PIN attempt limit exhausted (MaxTries, PinLocked) | PAM_MAXTRIES (11) |
| 10 | the pam_cert_host_binding extension is absent or invalid | PAM_AUTH_ERR (7) |
| 11 | host_id_hash is not among the pam_cert_host_binding entries | PAM_AUTH_ERR (7) |
| 12 | a single PIN error, a PAM conversation error, a hook refusal | PAM_AUTH_ERR (7) |
| 13 | a violation of internal invariants (Internal) | PAM_SYSTEM_ERR (4) |
| 14 | an Error from monitord with code = DEVICE_GONE / UNAUTHORIZED | propagated always, even in permissive |
The full mapping table of FlowError → PAM code is the doc-comment on
FlowError::pam_code in
crates/pam_tessera/src/flow.rs.
Principles:
- panics and infrastructure errors →
PAM_AUTHINFO_UNAVAIL(which tells the PAM stack: “the next module may try”). - Failures of a cryptographic check (chain, revocation, challenge-response)
and a role denial →
PAM_PERM_DENIED; host-scope failures (pam_cert_host_binding) and other auth errors →PAM_AUTH_ERR; an exhausted PIN budget →PAM_MAXTRIES. - Monitord registration is part of the authentication verdict. With
monitor_fail_mode = "strict", an IPC transport or registration failure denies the login because the session could not be placed under removal enforcement. Withpermissive, theFailModeWrapperabsorbs transport failures, whileDEVICE_GONEandUNAUTHORIZEDremain fatal in every mode (crates/tessera_core/src/ipc/failmode.rs).
14. Logging: tracing → syslog / journald
Section titled “14. Logging: tracing → syslog / journald”The cdylib pam_tessera.so logs to syslog, not to stderr: libpam discards
the module’s stderr, so production diagnostics on stderr are unavailable. The
tracing subscriber is built at the moment of the first pam_sm_* call and
sends records to syslog through the LOG_AUTH facility with the ident
pam_tessera. On systems with journald these lines are visible via
journalctl -t pam_tessera and land in /var/log/auth.log (on a plain syslog
stack) with the prefix pam_tessera[<pid>]:. (The switch from stderr to syslog
happened in 0.1.1, see changelog.md (Russian).)
tessera uses tracing-journald and writes to journald through the native
Type=notify channel. On SysV-init hosts without journald, tracing records
go to the daemon’s stderr; where they end up next is determined by how the
init script redirects stderr (in the standard distribution
start-stop-daemon hands stderr to the system syslog via logger).
The full semantics of what is logged and at which level is in docs/operations.md §6.
15. Further reading
Section titled “15. Further reading”- docs/threat-model.md — which threats each of these fail-closed rules covers.
- docs/configuration.md — which fields affect the behavior described here.
- docs/operations.md — how to read the log and diagnose anomalies.