跳转到内容

Tessera configuration reference

此内容尚不支持你的语言。

This document is the reference for the main tessera configuration file:

  • /etc/tessera/config.toml — the main configuration of the tessera module and daemon.

The “in which role and on which device” authorization lives inside the credential itself — in the X.509 extensions pam_cert_host_binding and pam_cert_allowed_roles. The name of the login account IS the role, so the role list also answers the question of admission into the account. This file holds no mechanism by which the device could admit a login on its own terms: the scope is assigned by the issuer, not by the party being constrained. See docs/cert-issuance.md.

Each field is described in the format “type → default value → allowed values → effect on behavior → security implication”. All fields are validated on load through tessera_core::config::ValidatedConfig::try_from (see crates/tessera_core/src/config/validated.rs and crates/tessera_core/src/config/raw.rs). Unknown fields or wrong types are a load error → fail-closed.

All examples use test data ([email protected], TERMINAL-001, ca-test.example). There are no real CAs, passwords, or customer hosts in this document.

The full shipped example lives in dist/config/config.toml.example. This example is checked by the regression test crates/tessera_core/tests/dist_examples_parse.rs — it guarantees that the example really validates through ValidatedConfig::try_from.

FieldTypeDefaultAllowed valuesEffectSecurity implication
crypto_backendstring"openssl", "pkcs11_native"Which backend computes signatures and hashes."openssl" is required for GOST via gost-engine.
modestring"pkcs12", "pkcs11"Where the user’s key lives."pkcs11" — non-extractable key; "pkcs12" — software protection.
pkcs11_modulepathabsolute path to a .soWhich PKCS#11 module is used.Required when mode = "pkcs11"; the file and every ancestor must be root-controlled.
pkcs11_token_labelstringNone≤ 64 bytes, no NULFilter by the token’s CKA_LABEL.Guards against accidentally selecting someone else’s token on the machine.
pkcs11_object_labelstringNone≤ 64 bytes, no NULFilter by the object’s CKA_LABEL (cert/privkey).Likewise, protection against selecting the wrong object.
pkcs11_max_pin_attemptsinteger31..=5How many times the module offers to enter the PIN.Too many → anti-paranoia; too few → poor UX.
pkcs11_locking_modestring"mutex""os", "mutex"PKCS#11 locking strategy."mutex" — every module call is serialized by a process-global mutex; cost ≈ 20 ns per call. "os" removes the serialization and is only appropriate when the module vendor confirms thread safety: advertising CKF_OS_LOCKING_OK is not such a confirmation. The mode is fixed by the first load of the module in a process; a later load asking for the other value gets the established one plus a pkcs11_locking_mode_conflict WARN.
pkcs11_pin_promptstring"Введите PIN токена: "UTF-8, non-empty, ≤ 128 bytesPIN prompt text on the PKCS#11 path. The default is the Russian string "Введите PIN токена: " (“Enter token PIN: ”).UX localization, not security.
pkcs11_slot_wait_secondsinteger100..=60How many seconds to wait for the token to be inserted.0 — do not wait; UX vs. convenience.
pkcs11_allow_extractable_keysbooleanfalsetrue, falseWhether to accept keys the token reported as CKA_EXTRACTABLE = TRUE.false (default) — reject (fail-closed): an extractable key breaks the invariant of mode B. true — only pkcs11_extractable_key WARN; enable deliberately. It does NOT cover the case where the token did not report the attribute at all — that one has its own key, pkcs11_allow_unreported_extractable.
pkcs11_allow_unreported_extractablebooleanfalsetrue, falseWhether to accept a key for which the token did not report CKA_EXTRACTABLE.false (default) — reject (fail-closed): non-extractability is unproven, and a silent provider is not the same as FALSE. true — only pkcs11_extractable_attribute_unavailable WARN carrying the reason (sensitive, type_invalid, unavailable, available_but_not_returned — the provider said the value was readable and still withheld it, probe_failed — the follow-up query itself failed); enable when the token vendor confirms the keys are non-extractable and the attribute is withheld by the module’s design. Keys that openly report themselves as extractable are NOT covered by this key.
pkcs12_path_patternstring"certs/user.p12"path relative to the USB mountpoint, optional ${user}Where to look for the .p12 on the USB media (supports ${user}).Relative path only; ../. segments and absolute paths are rejected by the validator.
pkcs12_pin_promptstring"Smart-card PIN: "UTF-8, non-empty, ≤ 128 bytesPrompt text for the .p12 password.UX localization.
gost_engine_pathpathNoneabsolute path to a .soExplicit path to gost-engine; required when OpenSSL allows GOST signatures.Implicit OPENSSL_ENGINES lookup is rejected; the file and every ancestor must be root-controlled.
usb_wait_secondsinteger100..=300How many seconds to wait for the USB media.UX. At 0 — fail-fast.
usb_allowed_devicesarray of strings[]"vid:pid" strings, 4 hex digits each (lsusb format), e.g. ["0951:1666"]Allow-list of USB devices treated as .p12 media; empty/absent = any USB block device.Hygiene against accidental/foreign flash drives, NOT a trust boundary: VID/PID are forgeable, trust comes only from decrypting the .p12 + chain validation.
max_usb_partitionsinteger81..=64Maximum number of partitions scanned when searching for the .p12.DoS protection: a physical attacker cannot force a huge number of mount/umount operations.
on_usb_removedstring"lock""lock", "logout", "hook", "shutdown"Action on confirmed USB removal."shutdown" fits terminals; "lock" fits workstations.
usb_removed_grace_secondsinteger00..=300Cancellation window: reinserting the same serial cancels the action.Protects against false triggers; set to 0 on terminals.
suspend_grace_secondsinteger00..=600Window after resume during which USB removal is ignored.Hubs often make noise during suspend; 30 seconds is a typical value.
monitor_fail_modestring"strict""strict", "permissive"Whether to propagate non-fatal monitord IPC errors to the calling code (strict) or swallow them with a WARN (permissive).DeviceGone/Unauthorized are always fatal. Strict mode currently rejects PKCS#11 authentication because native token-removal observation is not implemented.

Authorization (device + role) is described in the credential itself via X.509 v3 extensions pam_cert_host_binding and pam_cert_allowed_roles. This file contains only trust + identity + roles + monitor + hooks; see cert-issuance.md for issuing credentials with the required extensions.

The PAM authentication entry point validates its configuration and every configured trust anchor, intermediate, CRL, PKCS#11 module, and GOST engine as a regular root-owned file beneath root-owned, non-group/world-writable directories. Unsafe paths fail authentication before native code or trust material is loaded.

The PKCS#11 module is loaded and initialized (C_Initialize) once per process per module path and is never finalized: C_Finalize is not called, and the library stays loaded until the process exits.

This matters when the PAM stack of the same service holds a second PKCS#11 consumer (pam_pkcs11, sshd built with PKCS11Provider, p11-kit):

  • C_Initialize and C_Finalize are process-global operations on one shared loaded library. Finalizing on our side would deinitialize the provider for the neighbour as well, and the neighbour has no way of detecting it — which is why we do not.
  • The PKCS#11 login state is shared too: it is scoped to the “application”, and the application is defined by the C_Initialize call. While the neighbour is logged into the same token, our sessions read private objects without presenting a PIN, and vice versa. Our own sessions issue C_Logout when the authentication attempt ends, successfully or not; that has no effect on state the neighbour left behind.
  • The process-global mutex of mutex mode serializes our calls only. It cannot see the neighbour’s, so provider thread safety remains a requirement rather than something we can guarantee.

Under sshd and login the process serves a single authentication, so keeping the module resident costs nothing. Under fly-dm the display slave serves every login attempt for the machine’s uptime — there one context per process is the goal: a second C_Initialize against a live library is at best rejected by the provider and at worst kills the process.

ValueAction on confirmed USB removalTypical scenario
"lock"LockSession via D-Bus to logind for this session. The host keeps running.Operator workstation.
"logout"TerminateSession for this session. The host keeps running, other sessions intact.Kiosks, terminals (if the host must not power off).
"hook"Runs the external executable given in monitor.on_usb_removed_hook_path.Complex scenarios (audit + custom action).
"shutdown"PowerOff via D-Bus to logind — powers the host off.Terminals / dedicated workstations.

With "hook", the [monitor] section must contain on_usb_removed_hook_path = "/absolute/path". The validator refuses to load the config when on_usb_removed = "hook" and no hook_path is set, or when the executable or any parent directory is not root-controlled. The path is checked again immediately before execution; the hook receives a minimal environment with a fixed system PATH.

FieldTypeDefaultAllowed valuesEffectSecurity implication
on_usb_removed_hook_pathpathNoneabsolute pathExecutable for on_usb_removed = "hook". Valid only with that value of on_usb_removed.Runs as root; the path is checked for unsafe permissions.
idle_timeout_secondsinteger301..=3600Idle timeout of the IPC connection to monitord.Anti-DoS: hanging connections are closed.
max_concurrent_connectionsinteger641..=4096Maximum simultaneous IPC connections to monitord.Anti-DoS: caps the daemon’s resource consumption.
socket_pathpath/run/tessera/monitord.sockabsolute pathmonitord’s Unix socket.The socket’s permissions restrict access to the IPC.
timeout_msinteger2000millisecondsConnect+IO timeout of a single RPC.Fail-mode responsiveness when the daemon hangs.
fail_modestringsame as monitor_fail_modePer-section override of the top-level monitor_fail_mode.Determines behavior when monitord is unavailable.
state_file_pathpath/run/tessera/sessions.jsonabsolute pathSession registry (tmpfs; survives a daemon restart, not a boot).Moving it off tmpfs would leave stale records after a reboot.
on_usb_removedstringsame as top-levelPer-section override of on_usb_removed.See the top-level key.
usb_removed_grace_secondsintegersame as top-levelPer-section override of the cancellation window.See the top-level key.
suspend_grace_secondsintegersecondsWindow after resume during which removal events are ignored (default 30).Too large a window weakens the response to removal.
FieldTypeDefaultAllowed valuesEffectSecurity implication
anchorslist of paths≥ 1 PEM fileRoot trust CAs.The root of trust. Must be 0640 root:root.
intermediateslist of paths[]PEM filesIntermediate CAs (optional).Relieves the load of chain building.
max_chain_depthinteger51..=16Maximum X.509 chain depth.Anti-DoS.
clock_skew_secondsinteger00..=600Allowed clock deviation when checking notBefore/notAfter.Too much — an attacker with a stale certificate.
allowed_signature_algorithmslist of strings[]OIDs or namesSignature whitelist. Empty/omitted is replaced by a safe default: sha256/384/512WithRSAEncryption, ecdsa-with-SHA256/384/512 (no SHA-1 and no GOST).The SHA-1/MD5/weak-RSA ban applies even without explicit configuration; GOST requires an explicit opt-in.
max_supported_profile_versionintegercompiled-in defaultu32Maximum understood pam_cert_profile_version; a cert with a higher version rejects the whole chain (fail-closed, version-gate).Protection against silently ignoring the unknown semantics of newer profile versions.

Entries are compared exactly (no substrings) against the OpenSSL display form of the certificate’s algorithm (see pre_validate_end_entity in crates/tessera_core/src/x509/pre_validate.rs):

  • RSA: "sha256WithRSAEncryption", "sha384WithRSAEncryption", "sha512WithRSAEncryption"
  • ECDSA: "ecdsa-with-SHA256", "ecdsa-with-SHA384", "ecdsa-with-SHA512"
  • GOST R 34.10-2012-256: "id-tc26-signwithdigest-gost3410-12-256"
  • GOST R 34.10-2012-512: "id-tc26-signwithdigest-gost3410-12-512"
FieldTypeDefaultAllowed valuesEffectSecurity implication
modestring— (required)"none", "crl", "ocsp", "crl_then_ocsp"Which revocation sources are used.Required: omitting the [trust.revocation] section or the mode key is a validation error (no silent default). "none" — revocation is not checked (NOT for production), must be set explicitly.
crl_pathslist of paths[]PEM/DER filesLocal CRLs.Required when mode = "crl".
crl_max_age_hoursintegerNone1..=8760 (hours)Maximum age of a CRL from thisUpdate before rejection.Not set — CRL freshness is not checked; not recommended.
ocsp_responder_urlURL stringhttp://… / https://…Address of the OCSP responder. REQUIRED when mode ∈ {ocsp, crl_then_ocsp}. The AIA is not extracted from the cert.The only source of the address is the config (predictability for offline audit).
ocsp_timeout_secondsinteger51..=30Overall deadline for one OCSP exchange (connect+write+read).Login budget = (chain depth − 1) × timeout.
ocsp_cache_ttl_secondsinteger36000..=86400Upper bound on cache-entry lifetime (0 = cache disabled).The cache limits network calls; an entry is valid until min(nextUpdate, mtime+ttl).

Revocation-mode semantics:

modeBehavior
noneRevocation is not checked; the compensation is a short TTL on leaf certs (a deployment policy).
crlStrict offline CRL: an expired/missing covering CRL → reject.
ocspEvery non-anchor cert in the chain is checked via OCSP; the CRL store is not involved.
crl_then_ocspCRL first: a fresh CRL whose issuer DN covers the cert gives a status without a network call; otherwise OCSP is required.

Fail-closed in OCSP modes. An unavailable responder, a timeout, an unknown status, an unverifiable response signature, or a thisUpdate/nextUpdate window outside tolerance (accounting for clock_skew_seconds) → authentication is rejected (PAM_AUTH_ERR). There is no “WARN and skip” degradation in OCSP modes — whoever wants leniency chooses none or non-strict CRL.

Do not enable OCSP for zero-egress segments (terminals) — there is no network to the responder there; their mode is none + a short TTL, or offline crl. OCSP is for network-connected segments (office workstations, customer test benches). The ocsp_* keys are rejected by validation when mode ∈ {none, crl} (they cannot be silently ignored). The cache is /var/cache/tessera/ocsp/*.der, and the directory is created by the package’s postinst.

FieldTypeDefaultAllowed valuesEffectSecurity implication
enabledboolfalsetrue, falseEnables pinning on the SPKI of root CAs.Protection against CA compromise.
allowed_root_spki_sha256list of strings[]64-character lower-case hexList of allowed root SPKI hashes.Any root not in the list is rejected.
FieldTypeDefaultAllowed valuesEffectSecurity implication
sourceslist of strings"machine_id", "dmi_board_serial", "dmi_system_uuid", "dmi_system_serial", "hostname", "custom_command", "override"Chain of host_id sources. The first non-empty one wins.The more stable the source, the stronger the host binding.
fallbackstring"deny""deny", "warn", "allow"What to do if all sources are empty.In production — "deny" only.
overridestringNoneUTF-8, no line breaksA hard-coded host_id value (for tests).Do NOT use in production.
custom_commandpathNoneabsolute path to a scriptA script that prints host_id to stdout.The script runs as root. Must be 0750 root:root.
custom_command_timeout_secondsinteger51..=30Timeout for executing custom_command.Anti-DoS.

The chain implementation is in crates/tessera_core/src/host_identity/chain.rs. The fallback = "deny" behavior guarantees fail-closed: if no source yields a value, authentication does not pass.

FieldTypeDefaultAllowed valuesEffectSecurity implication
levelstring"error", "warn", "info", "debug", "trace"Verbosity level of the daemon’s log. The TESSERA_LOG environment variable takes priority over this field."trace" — debugging; do not leave it on in production.
syslog_facilitystringoptional"auth", "authpriv", "user", "daemon"Deprecated, ignored. The PAM module writes to the syslog auth facility, fixed. The field is validated (local0..7 are not supported — a load error), but has no runtime effect; if the key is present, a “deprecated and ignored” WARN is emitted to the log.No effect on behavior.
journald_prioritybooloptionaltrue, falseDeprecated, ignored. If the key is present — a “deprecated and ignored” WARN.No effect on behavior.

PINs and passwords are never logged. Full certificate DNs are logged at debug and above; at info and below — only the CN.

Controls role selection at login and the device’s role store (see docs/cert-issuance.md — the pam_cert_allowed_roles extension).

FieldTypeDefaultAllowed valuesEffectSecurity implication
dirpath/var/lib/tessera/rolesabsolute path to a directoryRole-store directory (<role>.toml slices).Standalone loads enforce root:root on the directory, every slice, and all ancestors; no group/world write.
default_session_ttl_secondsinteger43200 (12 h)secondsSession TTL when neither the credential nor the role sets one.No unbounded session arises — the ceiling is always finite.
account_lookup_timeout_secondsinteger10160 (seconds)How long name resolution may take while checking whether the login account is a system one.Running out means the login proceeds on the local file rather than being refused; 0 is rejected so the check cannot be silently disabled.

Role checking is unconditional. A role is required on every login, and no setting disables the check or downgrades it to a warning. A config that still contains the removed [roles].enforce key is rejected at validation with a diagnostic naming the removal.

Fail-closed. An empty or invalid role store leads to refusal of logins, with a “roles not configured” diagnostic.

Role selection at login. There is no default role. The role is the login account name: the engineer logs into a role account named after the role (ssh serv@device), and the requested role equals PAM_USER. There is no other source of the role — no name suffix, no PAM prompt, no environment variable.

The module never rewrites PAM_USER: the name read by the stack is the name the decision is made on. An account name that does not match the role_id format (^[a-z][a-z0-9-]{0,15}$) is rejected before the medium is touched.

The engineer’s identity is not lost: it lives in the certificate and in the issuance log, not in the account name.

Role accounts are provisioned separately (Census). Closing the remaining ways into them — ~/.ssh/authorized_keys, su, sudo -u, password login, PAM stacks without the module — is the job of provisioning and of the device administrator, not of the product: it manages neither sshd_config nor sudoers nor anybody else’s PAM stacks. The explicit commands and their verification are in install.md §8.4.

What the product does guarantee here it guarantees unconditionally: the login is refused if the uid of the account named in PAM_USER falls outside the regular-user range — below 1000 (where the accounts of the distribution and its packages live) or above 61183 (where the uids systemd hands to units with DynamicUser=yes begin, with nobody and nogroup beyond them). Both boundaries, and why the upper one differs from UID_MAX, are in install.md §8.3. The refusal does not depend on the contents of the role store or on what the credential permits: ssh root@device will not become a role login even with a root slice present and a credential covering that role. The same rule makes a slice named after a system account fail to load, both in the store and in tessera-cli role lint — so that a provisioning mistake is seen by the administrator rather than by the first engineer to log in.

The check needs no network. The uid comes from the local /etc/passwd, and that alone is enough for a login to proceed. Name resolution (NSS) is consulted in addition, and only to catch accounts no file holds by construction — systemd’s DynamicUser= identities, which nss-systemd synthesises. That source can only ADD a refusal: if the directory does not answer, answers with an error, or exceeds account_lookup_timeout_seconds, the verdict stays the one the local file reached. An unreachable LDAP does not close a login — including the emergency console one.

Admission is checked by a single credential extension — pam_cert_allowed_roles (“the holder may activate these roles”). It also answers the question “into which account is the holder admitted”, because it is the same string. There is no separate list of permitted accounts: two lists over one name would describe an unrealizable state, “admitted into serv, but not entitled to be serv”.

No other source of admission exists. The device configuration holds no mechanism that would permit a login by credential attributes (CN, SAN) the issuer never meant for admission: the scope is carried by the credential, and a path where the constrained party assigns it undermines the model itself. A config containing the removed [[user_mapping]] section is rejected at validation with a diagnostic naming the removal.

Device tags for delegation constraints (device-tags). Absence of the section = the device has no tags (a fail-closed default): a delegation envelope with a group constraint on an untagged device is rejected.

FieldTypeDefaultAllowed valuesEffectSecurity implication
enforcebooleanfalsetrue/falseWhether to read the tag source. false — a device with no applied tags.Group delegations on an untagged device are rejected anyway (fail-closed).
modestringstandalonestandalone, managedTrust model of the source: a tags file or a signed manifest.toml.managed requires a signed manifest.
sourcepath/var/lib/tessera/tags.toml (standalone) / role-store directory (managed)absolute pathThe tags file or the directory with the manifest.Standalone loads enforce a root-owned, non-group/world-writable file and complete ancestor path.

An array of tables. Each hook is an external command executed at a lifecycle stage. The full implementation is in crates/tessera_core/src/hooks/.

FieldTypeDefaultAllowed valuesEffectSecurity implication
stagestring"pre_auth", "post_auth_success", "session_open", "session_close", "usb_removed"At which lifecycle stage the hook is invoked.Hooks run under sandbox restrictions (see docs/threat-model.md).
commandlist of strings[ "/usr/local/sbin/foo", "arg" ], the first element is an absolute pathThe hook’s argv. Passed literally; placeholders in argv are NOT substituted.Dynamic data is passed only through env — argv injection is impossible.
timeout_secondsinteger101..=120Execution timeout.The hook is killed with SIGKILL when it expires.
on_failurestringNone"warn", "ignore"; any other value → abortWhat to do on a non-zero hook return code.Default: abort (deny) for pre_auth (there, "warn" is also forced to abort); "warn" for the other stages.
run_asstringNone"root", "user"The privilege the hook runs under: root or user (the authenticated PAM user).Defaults to root. Any other value (a typo, an account name) is a configuration error, not a silent fall-back to root. Dropping privileges (user) is best practice.
envtable{}{ KEY = "literal ${placeholder}" } stringsEnvironment variables passed to the hook.Base: a whitelist of PATH/HOME/USER/LOGNAME/LANG + all TESSERA_* variables; custom keys may override them.

${...} substitution works only in env valuescommand is executed literally (see crates/tessera_core/src/hooks/fork_exec.rs). In addition, the hook always receives a ready-made set of variables TESSERA_STAGE, TESSERA_USER, TESSERA_SERVICE, TESSERA_HOST_ID, TESSERA_HOST_ID_HASH, TESSERA_HOST_ID_SOURCE, TESSERA_CERT_CN, TESSERA_CERT_SERIAL, TESSERA_USB_SERIAL, TESSERA_USB_VID_PID, TESSERA_SESSION_ID (an empty string if the value is unavailable).

Allowed placeholders for env values (see crates/tessera_core/src/hooks/placeholder.rs):

  • ${pam_user} — the UNIX user.
  • ${pam_service} — the PAM service.
  • ${host_id} / ${host_id_hash} / ${host_id_source} — the computed host_id, its SHA-256, and the source name.
  • ${cert_cn} — the certificate’s Common Name.
  • ${cert_serial} — the certificate serial (hex).
  • ${usb_serial} / ${usb_vid_pid} — the USB media’s data.
  • ${session_id} — the PAM session UUID.

Example: dynamic data — through env, not through argv:

[[hooks]]
stage = "post_auth_success"
command = ["/usr/local/sbin/audit-login"]
timeout_seconds = 5
on_failure = "warn"
env = { AUDIT_USER = "${pam_user}", AUDIT_SERIAL = "${cert_serial}" }

Optional. Controls the wallpaper writer for fly-dm — it stamps host_id into the JPG background pointed to by [background].path in /etc/X11/fly-dm/fly-modern/settings.ini. A workaround for the MIC-3 (mandatory integrity control, МКЦ, level 3) fly-modern theme, where PAM_TEXT_INFO is not forwarded to the UI.

FieldTypeDefaultDescription
update_wallpaperboolfalseEnable the wallpaper writer.
wallpaper_targetpath/usr/share/wallpapers/fly-default-light.jpgThe JPG that the daemon repaints.
wallpaper_backuppath/var/lib/tessera/daemon/wallpaper.orig.jpgWhere the one-time original of the source is saved.
wallpaper_fontpath/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttfThe TrueType font used for rendering.
wallpaper_font_sizeint64Font size in points (1..=512).
wallpaper_text_colorstring"#000000"Color in hex (#RRGGBB).
wallpaper_gravityenum"south"north / south / east / west / center — the positioning anchor.
wallpaper_offset_xint0Horizontal offset in pixels from the gravity anchor.
wallpaper_offset_yint120Vertical offset in pixels from the gravity anchor (for south — upward).
template_rustring"Устройство %n host_id={host_id_short} ({source})"Template for the ru locale.
template_enstring"Device %n host_id={host_id_short} ({source})"Template for the en locale.

Substitutions in the template: {host_id_short} (the first 8 hex of the sha256), {source} (the source name — MachineId, DmiBoardSerial …), %n (hostname). Behavior, the baseline for settings.ini, and troubleshooting — see fly-dm-greeter.md.

The legacy field update_greet_string (0.3.16–0.3.18) rewrote /etc/X11/fly-dm/override/GreetString.desktop. On production MIC-3 fly-modern it is ignored (a no-op). Kept for backward compatibility, but does NOT work on terminals. Use update_wallpaper instead.

An array of tables. Each entry is an override of [trust] for a limited set of host_ids.

FieldTypeDefaultAllowed valuesEffectSecurity implication
when_host_id_inlist of stringslist of host_idsOn which machines to apply the override.Must be non-empty.
anchorslist of paths≥ 1 PEM fileWhich trust roots to use instead of the main ones.Required and non-empty; narrows trust on specific machines.
intermediateslist of paths[]PEM filesWhich intermediates to use.Likewise.

Worked example: a minimal valid configuration

Section titled “Worked example: a minimal valid configuration”
crypto_backend = "openssl"
mode = "pkcs12"
pkcs12_path_pattern = "certs/${user}.p12" # relative to the USB mountpoint
usb_wait_seconds = 10
on_usb_removed = "lock"
usb_removed_grace_seconds = 5
suspend_grace_seconds = 30
monitor_fail_mode = "strict"
[trust]
anchors = ["/etc/tessera/ca/bundle.pem"]
[trust.revocation]
mode = "none"
[host_identity]
sources = ["machine_id", "hostname"]
fallback = "deny"
[roles]
dir = "/var/lib/tessera/roles"
[logging]
level = "info"

The credential’s binding to devices and role accounts is fully described by two X.509 v3 extensions of the leaf:

  • pam_cert_host_binding (OID 2.25.183976554325829274683049824615098) — a SEQUENCE OF UTF8String, where each entry is either *, or sha256:<HEX>, or a “raw” machine_id value (in which case the comparison goes through SHA-256 of the string).
  • pam_cert_allowed_roles (OID 2.25.185305973969816596290730578528098241367) — a SEQUENCE OF UTF8String, where each entry is a role identifier (^[a-z][a-z0-9-]{0,15}$). It is also the login account name, so the list answers the question of admission into the account as well.

To authorize a credential for a specific host_id and in a specific role account, at least one matching entry in each of the extensions is required. The absence of either extension, a corrupt DER encoding, or a complete absence of matches is a rejection (PAM_AUTH_ERR); no fallback to any device-side mechanism exists.

Details and ready-made openssl.cnf recipes are in cert-issuance.md.

3.1 Terminal — offline, CRL with TTL, PKCS#11 without continuous presence

Section titled “3.1 Terminal — offline, CRL with TTL, PKCS#11 without continuous presence”

Properties: the machine is in a metal enclosure, no Internet, and the key is on a token. Native PKCS#11 removal observation is not implemented yet, so this profile is suitable only where bounded role/session TTL is an acceptable compensating control. Do not deploy it where token removal must immediately end the session.

crypto_backend = "pkcs11_native"
mode = "pkcs11"
pkcs11_module = "/usr/lib/librtpkcs11ecp.so"
pkcs11_max_pin_attempts = 3
pkcs11_slot_wait_seconds = 5
usb_wait_seconds = 5
on_usb_removed = "shutdown" # terminal — power off
usb_removed_grace_seconds = 0 # no cancellation
suspend_grace_seconds = 0
monitor_fail_mode = "permissive" # required for PKCS#11 until native removal monitoring
[trust]
anchors = ["/etc/tessera/ca/terminal-ca.pem"]
allowed_signature_algorithms = [
"1.2.643.7.1.1.3.2", # GOST-2012-256
]
[trust.revocation]
mode = "crl"
crl_paths = ["/etc/tessera/crl/terminal.crl"]
crl_max_age_hours = 72
[trust.pinning]
enabled = true
allowed_root_spki_sha256 = [
"ee0bd4f3a3c8e21d4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f"
]
[host_identity]
sources = ["dmi_board_serial", "machine_id"]
fallback = "deny"
[roles]
dir = "/var/lib/tessera/roles"
[logging]
level = "warn"

Rationale for the choices:

  • mode = "pkcs11" + librtpkcs11ecp.so: a non-extractable key.
  • monitor_fail_mode = "permissive": PKCS#11 token serials are not USB block-device serials. Strict authentication is refused until monitord has a native token-event source; on_usb_removed is therefore not an enforcement boundary for this profile.
  • usb_removed_grace_seconds = 0: on a terminal there can be no “pulled it out and changed my mind”.
  • mode = "crl" with crl_max_age_hours = 72: three days is a compromise between UX (the CRL is updated daily) and security.
  • host_identity.sources = ["dmi_board_serial", ...]: the motherboard is tied to the enclosure, a replacement → a new host_id → the certificate must be reissued with the new value in pam_cert_host_binding.
  • pinning.enabled = true: a CA compromise does not automatically open all terminals.

3.2 Workstation in a protected segment — CRL, GOST token

Section titled “3.2 Workstation in a protected segment — CRL, GOST token”
crypto_backend = "pkcs11_native"
mode = "pkcs11"
pkcs11_module = "/usr/lib/librtpkcs11ecp.so"
pkcs11_token_label = "STAFF"
pkcs11_max_pin_attempts = 3
pkcs11_slot_wait_seconds = 10
usb_wait_seconds = 10
on_usb_removed = "lock"
usb_removed_grace_seconds = 30
suspend_grace_seconds = 60
monitor_fail_mode = "permissive" # required for PKCS#11 until native removal monitoring
[trust]
anchors = ["/etc/tessera/ca/staff-ca.pem"]
intermediates = ["/etc/tessera/ca/staff-int.pem"]
allowed_signature_algorithms = [
"1.2.643.7.1.1.3.2", # GOST-2012-256
"1.2.643.7.1.1.3.3", # GOST-2012-512
]
[trust.revocation]
mode = "crl"
crl_paths = ["/etc/tessera/crl/staff.crl"]
crl_max_age_hours = 24
[host_identity]
sources = ["machine_id", "hostname"]
fallback = "deny"
[roles]
dir = "/var/lib/tessera/roles"
[logging]
level = "info"
[[hooks]]
stage = "post_auth_success"
command = ["/usr/local/sbin/audit-login"]
timeout_seconds = 5
on_failure = "warn"
run_as = "user"
env = { AUDIT_USER = "${pam_user}", AUDIT_SERIAL = "${cert_serial}" }

Rationale:

  • usb_removed_grace_seconds = 30: the user may pull out the token to reinsert something and keep working.
  • mode = "crl" + crl_max_age_hours = 24: the only supported revocation source; CRL freshness is controlled by the TTL.
  • [[hooks]] for auditing: a third-party audit system receives the “login” event (data — through env, argv is passed literally).

3.3 Test environment — mode = "pkcs12", no revocation

Section titled “3.3 Test environment — mode = "pkcs12", no revocation”
crypto_backend = "openssl"
mode = "pkcs12"
pkcs12_path_pattern = "certs/${user}.p12" # relative to the USB mountpoint
pkcs12_pin_prompt = "PKCS#12 password: "
usb_wait_seconds = 5
on_usb_removed = "lock"
usb_removed_grace_seconds = 5
suspend_grace_seconds = 0
monitor_fail_mode = "permissive"
[trust]
anchors = ["/etc/tessera/ca/test-ca.pem"]
[trust.revocation]
mode = "none"
[host_identity]
sources = ["hostname"]
fallback = "warn"
[roles]
dir = "/var/lib/tessera/roles"
[logging]
level = "debug"

Rationale:

  • mode = "pkcs12": to avoid dealing with a real token in tests.
  • monitor_fail_mode = "permissive": monitord crashes on dev machines more often than in production.
  • level = "debug": everything is visible, for debugging.
  • revocation.mode = "none": tests must not depend on external services.

This configuration must not be used in production. Marker: the file comment reads # TEST CONFIG — DO NOT DEPLOY.

The [mac] section is optional. The same open binary runs on Debian/Ubuntu/Astra. Real enforcement appears only when a signed runtime plugin is installed and explicitly selected with backend.

FieldTypeDefaultDescription
backendstringExplicit runtime plugin name, for example "parsec". Absence selects StubBackend.
cert_integrityenum"optional"One of required / optional / ignore. See below.
fallback_max_integrity.levelint (-128..127)The fallback label’s level, when the MAX_INTEGRITY extension is absent and cert_integrity = "optional".
fallback_max_integrity.categoriesstring (hex or CSV)The category bitmask for the fallback. An empty string = ''B.
runtimeenum"auto"One of required / auto / disabled. See below (0.3.7+).
warn_on_homedir_label_mismatchbooltrueLog homedir_label_above_session_cap on a mismatch.
  • required — the certificate must contain MAX_INTEGRITY. If the extension is absent or the DER is broken, authentication is rejected (mac_required_no_label / mac_parse_failed).
  • optional — the extension is applied when present. A present malformed extension rejects authentication; only genuine absence may use the fallback:
    • [mac.fallback_max_integrity] is present → the fallback is applied;
    • no fallback → the labeling step is skipped (mac_label_skipped is logged).
  • ignore — a valid extension is parsed for diagnostics (mac_label_parsed) but not applied; malformed DER still rejects authentication. Safe for migrating a fleet of machines without runtime MIC.

backend selects a separate signed shared library; runtime defines how it is used. The open host verifies signature and ABI before dlopen/init. Additional files are never auto-activated.

  • required — the selected plugin and its runtime are mandatory. Missing files, bad signature/ABI/init, or an inactive runtime fail closed with plugin_rejected/mac_runtime_required.
  • auto (default) — an active selected plugin is used; otherwise the host falls back to StubBackend with a one-time mac_runtime_fallback event (WARN). Suitable for dev machines and a mixed fleet.
  • disabled — always StubBackend, even when a plugin is installed. Plugin callbacks are not invoked. mac_runtime_disabled is logged (INFO).

Config validation:

  • runtime = "disabled" + cert_integrity = "required" is rejected at startup (logically incompatible: the stub cannot read or set the label that the cert policy requires).
  • runtime = "required" or cert_integrity = "required" without backend is rejected at startup.

At open_session the following is chosen:

effective = intersect(cert_label, runtime_caps)

where runtime_caps is the ceiling that libpdp returns from ipdp_get_caps(). The effective label’s level is min(cert.level, caps.level); the categories are cert.categories & caps.categories. If, after the intersection, effective.level < cert.level, a mac_level_intersected event is written; likewise for categories.

[mac]
backend = "parsec"
cert_integrity = "optional"
[mac.fallback_max_integrity]
level = 0
categories = ""

See docs/threat-model.md §“Privilege-escalation via MAC label” and docs/cert-issuance.md §“MAX_INTEGRITY”.