Tessera configuration reference
此内容尚不支持你的语言。
This document is the reference for the main tessera
configuration file:
/etc/tessera/config.toml— the main configuration of thetesseramodule 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 /etc/tessera/config.toml file
Section titled “The /etc/tessera/config.toml file”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.
Global parameters
Section titled “Global parameters”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
crypto_backend | string | — | "openssl", "pkcs11_native" | Which backend computes signatures and hashes. | "openssl" is required for GOST via gost-engine. |
mode | string | — | "pkcs12", "pkcs11" | Where the user’s key lives. | "pkcs11" — non-extractable key; "pkcs12" — software protection. |
pkcs11_module | path | — | absolute path to a .so | Which PKCS#11 module is used. | Required when mode = "pkcs11"; the file and every ancestor must be root-controlled. |
pkcs11_token_label | string | None | ≤ 64 bytes, no NUL | Filter by the token’s CKA_LABEL. | Guards against accidentally selecting someone else’s token on the machine. |
pkcs11_object_label | string | None | ≤ 64 bytes, no NUL | Filter by the object’s CKA_LABEL (cert/privkey). | Likewise, protection against selecting the wrong object. |
pkcs11_max_pin_attempts | integer | 3 | 1..=5 | How many times the module offers to enter the PIN. | Too many → anti-paranoia; too few → poor UX. |
pkcs11_locking_mode | string | "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_prompt | string | "Введите PIN токена: " | UTF-8, non-empty, ≤ 128 bytes | PIN prompt text on the PKCS#11 path. The default is the Russian string "Введите PIN токена: " (“Enter token PIN: ”). | UX localization, not security. |
pkcs11_slot_wait_seconds | integer | 10 | 0..=60 | How many seconds to wait for the token to be inserted. | 0 — do not wait; UX vs. convenience. |
pkcs11_allow_extractable_keys | boolean | false | true, false | Whether 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_extractable | boolean | false | true, false | Whether 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_pattern | string | "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_prompt | string | "Smart-card PIN: " | UTF-8, non-empty, ≤ 128 bytes | Prompt text for the .p12 password. | UX localization. |
gost_engine_path | path | None | absolute path to a .so | Explicit 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_seconds | integer | 10 | 0..=300 | How many seconds to wait for the USB media. | UX. At 0 — fail-fast. |
usb_allowed_devices | array 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_partitions | integer | 8 | 1..=64 | Maximum 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_removed | string | "lock" | "lock", "logout", "hook", "shutdown" | Action on confirmed USB removal. | "shutdown" fits terminals; "lock" fits workstations. |
usb_removed_grace_seconds | integer | 0 | 0..=300 | Cancellation window: reinserting the same serial cancels the action. | Protects against false triggers; set to 0 on terminals. |
suspend_grace_seconds | integer | 0 | 0..=600 | Window after resume during which USB removal is ignored. | Hubs often make noise during suspend; 30 seconds is a typical value. |
monitor_fail_mode | string | "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_bindingandpam_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.
PKCS#11 module lifetime within a process
Section titled “PKCS#11 module lifetime within a process”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_InitializeandC_Finalizeare 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_Initializecall. While the neighbour is logged into the same token, our sessions read private objects without presenting a PIN, and vice versa. Our own sessions issueC_Logoutwhen the authentication attempt ends, successfully or not; that has no effect on state the neighbour left behind. - The process-global mutex of
mutexmode 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.
Values of on_usb_removed
Section titled “Values of on_usb_removed”| Value | Action on confirmed USB removal | Typical 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.
The [monitor] section
Section titled “The [monitor] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
on_usb_removed_hook_path | path | None | absolute path | Executable 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_seconds | integer | 30 | 1..=3600 | Idle timeout of the IPC connection to monitord. | Anti-DoS: hanging connections are closed. |
max_concurrent_connections | integer | 64 | 1..=4096 | Maximum simultaneous IPC connections to monitord. | Anti-DoS: caps the daemon’s resource consumption. |
socket_path | path | /run/tessera/monitord.sock | absolute path | monitord’s Unix socket. | The socket’s permissions restrict access to the IPC. |
timeout_ms | integer | 2000 | milliseconds | Connect+IO timeout of a single RPC. | Fail-mode responsiveness when the daemon hangs. |
fail_mode | string | — | same as monitor_fail_mode | Per-section override of the top-level monitor_fail_mode. | Determines behavior when monitord is unavailable. |
state_file_path | path | /run/tessera/sessions.json | absolute path | Session registry (tmpfs; survives a daemon restart, not a boot). | Moving it off tmpfs would leave stale records after a reboot. |
on_usb_removed | string | — | same as top-level | Per-section override of on_usb_removed. | See the top-level key. |
usb_removed_grace_seconds | integer | — | same as top-level | Per-section override of the cancellation window. | See the top-level key. |
suspend_grace_seconds | integer | — | seconds | Window after resume during which removal events are ignored (default 30). | Too large a window weakens the response to removal. |
The [trust] section
Section titled “The [trust] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
anchors | list of paths | — | ≥ 1 PEM file | Root trust CAs. | The root of trust. Must be 0640 root:root. |
intermediates | list of paths | [] | PEM files | Intermediate CAs (optional). | Relieves the load of chain building. |
max_chain_depth | integer | 5 | 1..=16 | Maximum X.509 chain depth. | Anti-DoS. |
clock_skew_seconds | integer | 0 | 0..=600 | Allowed clock deviation when checking notBefore/notAfter. | Too much — an attacker with a stale certificate. |
allowed_signature_algorithms | list of strings | [] | OIDs or names | Signature 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_version | integer | compiled-in default | u32 | Maximum 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"
The [trust.revocation] section
Section titled “The [trust.revocation] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
mode | string | — (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_paths | list of paths | [] | PEM/DER files | Local CRLs. | Required when mode = "crl". |
crl_max_age_hours | integer | None | 1..=8760 (hours) | Maximum age of a CRL from thisUpdate before rejection. | Not set — CRL freshness is not checked; not recommended. |
ocsp_responder_url | URL string | — | http://… / 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_seconds | integer | 5 | 1..=30 | Overall deadline for one OCSP exchange (connect+write+read). | Login budget = (chain depth − 1) × timeout. |
ocsp_cache_ttl_seconds | integer | 3600 | 0..=86400 | Upper 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:
mode | Behavior |
|---|---|
none | Revocation is not checked; the compensation is a short TTL on leaf certs (a deployment policy). |
crl | Strict offline CRL: an expired/missing covering CRL → reject. |
ocsp | Every non-anchor cert in the chain is checked via OCSP; the CRL store is not involved. |
crl_then_ocsp | CRL 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
unknownstatus, an unverifiable response signature, or athisUpdate/nextUpdatewindow outside tolerance (accounting forclock_skew_seconds) → authentication is rejected (PAM_AUTH_ERR). There is no “WARN and skip” degradation in OCSP modes — whoever wants leniency choosesnoneor 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 offlinecrl. OCSP is for network-connected segments (office workstations, customer test benches). Theocsp_*keys are rejected by validation whenmode ∈ {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.
The [trust.pinning] section
Section titled “The [trust.pinning] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
enabled | bool | false | true, false | Enables pinning on the SPKI of root CAs. | Protection against CA compromise. |
allowed_root_spki_sha256 | list of strings | [] | 64-character lower-case hex | List of allowed root SPKI hashes. | Any root not in the list is rejected. |
The [host_identity] section
Section titled “The [host_identity] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
sources | list 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. |
fallback | string | "deny" | "deny", "warn", "allow" | What to do if all sources are empty. | In production — "deny" only. |
override | string | None | UTF-8, no line breaks | A hard-coded host_id value (for tests). | Do NOT use in production. |
custom_command | path | None | absolute path to a script | A script that prints host_id to stdout. | The script runs as root. Must be 0750 root:root. |
custom_command_timeout_seconds | integer | 5 | 1..=30 | Timeout 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.
The [logging] section
Section titled “The [logging] section”| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
level | string | — | "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_facility | string | optional | "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_priority | bool | optional | true, false | Deprecated, 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
debugand above; atinfoand below — only the CN.
The [roles] section
Section titled “The [roles] section”Controls role selection at login and the device’s role store (see
docs/cert-issuance.md — the pam_cert_allowed_roles
extension).
| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
dir | path | /var/lib/tessera/roles | absolute path to a directory | Role-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_seconds | integer | 43200 (12 h) | seconds | Session TTL when neither the credential nor the role sets one. | No unbounded session arises — the ceiling is always finite. |
account_lookup_timeout_seconds | integer | 10 | 1–60 (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.
The [tags] section
Section titled “The [tags] section”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.
| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
enforce | boolean | false | true/false | Whether to read the tag source. false — a device with no applied tags. | Group delegations on an untagged device are rejected anyway (fail-closed). |
mode | string | standalone | standalone, managed | Trust model of the source: a tags file or a signed manifest.toml. | managed requires a signed manifest. |
source | path | /var/lib/tessera/tags.toml (standalone) / role-store directory (managed) | absolute path | The tags file or the directory with the manifest. | Standalone loads enforce a root-owned, non-group/world-writable file and complete ancestor path. |
The [[hooks]] section
Section titled “The [[hooks]] section”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/.
| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
stage | string | — | "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). |
command | list of strings | — | [ "/usr/local/sbin/foo", "arg" ], the first element is an absolute path | The hook’s argv. Passed literally; placeholders in argv are NOT substituted. | Dynamic data is passed only through env — argv injection is impossible. |
timeout_seconds | integer | 10 | 1..=120 | Execution timeout. | The hook is killed with SIGKILL when it expires. |
on_failure | string | None | "warn", "ignore"; any other value → abort | What 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_as | string | None | "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. |
env | table | {} | { KEY = "literal ${placeholder}" } strings | Environment 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 values — command 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 computedhost_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 = 5on_failure = "warn"env = { AUDIT_USER = "${pam_user}", AUDIT_SERIAL = "${cert_serial}" }The [fly_dm_greeter] section (0.3.19+)
Section titled “The [fly_dm_greeter] section (0.3.19+)”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.
| Field | Type | Default | Description |
|---|---|---|---|
update_wallpaper | bool | false | Enable the wallpaper writer. |
wallpaper_target | path | /usr/share/wallpapers/fly-default-light.jpg | The JPG that the daemon repaints. |
wallpaper_backup | path | /var/lib/tessera/daemon/wallpaper.orig.jpg | Where the one-time original of the source is saved. |
wallpaper_font | path | /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf | The TrueType font used for rendering. |
wallpaper_font_size | int | 64 | Font size in points (1..=512). |
wallpaper_text_color | string | "#000000" | Color in hex (#RRGGBB). |
wallpaper_gravity | enum | "south" | north / south / east / west / center — the positioning anchor. |
wallpaper_offset_x | int | 0 | Horizontal offset in pixels from the gravity anchor. |
wallpaper_offset_y | int | 120 | Vertical offset in pixels from the gravity anchor (for south — upward). |
template_ru | string | "Устройство %n host_id={host_id_short} ({source})" | Template for the ru locale. |
template_en | string | "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.
The [[trust_override]] section
Section titled “The [[trust_override]] section”An array of tables. Each entry is an override of [trust] for a limited
set of host_ids.
| Field | Type | Default | Allowed values | Effect | Security implication |
|---|---|---|---|---|---|
when_host_id_in | list of strings | — | list of host_ids | On which machines to apply the override. | Must be non-empty. |
anchors | list of paths | — | ≥ 1 PEM file | Which trust roots to use instead of the main ones. | Required and non-empty; narrows trust on specific machines. |
intermediates | list of paths | [] | PEM files | Which 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 = 10on_usb_removed = "lock"usb_removed_grace_seconds = 5suspend_grace_seconds = 30monitor_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"Authorization in the credential
Section titled “Authorization in the credential”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(OID2.25.183976554325829274683049824615098) — aSEQUENCE OF UTF8String, where each entry is either*, orsha256:<HEX>, or a “raw”machine_idvalue (in which case the comparison goes through SHA-256 of the string).pam_cert_allowed_roles(OID2.25.185305973969816596290730578528098241367) — aSEQUENCE 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.
Typical scenarios
Section titled “Typical scenarios”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 = 3pkcs11_slot_wait_seconds = 5
usb_wait_seconds = 5on_usb_removed = "shutdown" # terminal — power offusb_removed_grace_seconds = 0 # no cancellationsuspend_grace_seconds = 0monitor_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 = trueallowed_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_removedis 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"withcrl_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 newhost_id→ the certificate must be reissued with the new value inpam_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 = 3pkcs11_slot_wait_seconds = 10
usb_wait_seconds = 10on_usb_removed = "lock"usb_removed_grace_seconds = 30suspend_grace_seconds = 60monitor_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 = 5on_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 — throughenv, 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 mountpointpkcs12_pin_prompt = "PKCS#12 password: "
usb_wait_seconds = 5on_usb_removed = "lock"usb_removed_grace_seconds = 5suspend_grace_seconds = 0monitor_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.
MAC integrity (Astra МКЦ, 0.3.0+)
Section titled “MAC integrity (Astra МКЦ, 0.3.0+)”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.
Fields
Section titled “Fields”| Field | Type | Default | Description |
|---|---|---|---|
backend | string | — | Explicit runtime plugin name, for example "parsec". Absence selects StubBackend. |
cert_integrity | enum | "optional" | One of required / optional / ignore. See below. |
fallback_max_integrity.level | int (-128..127) | — | The fallback label’s level, when the MAX_INTEGRITY extension is absent and cert_integrity = "optional". |
fallback_max_integrity.categories | string (hex or CSV) | — | The category bitmask for the fallback. An empty string = ''B. |
runtime | enum | "auto" | One of required / auto / disabled. See below (0.3.7+). |
warn_on_homedir_label_mismatch | bool | true | Log homedir_label_above_session_cap on a mismatch. |
cert_integrity semantics
Section titled “cert_integrity semantics”required— the certificate must containMAX_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_skippedis 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.
runtime semantics (0.3.7+)
Section titled “runtime semantics (0.3.7+)”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 withplugin_rejected/mac_runtime_required.auto(default) — an active selected plugin is used; otherwise the host falls back toStubBackendwith a one-timemac_runtime_fallbackevent (WARN). Suitable for dev machines and a mixed fleet.disabled— alwaysStubBackend, even when a plugin is installed. Plugin callbacks are not invoked.mac_runtime_disabledis 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"orcert_integrity = "required"withoutbackendis rejected at startup.
The effective label
Section titled “The effective label”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.
Full example
Section titled “Full example”[mac]backend = "parsec"cert_integrity = "optional"
[mac.fallback_max_integrity]level = 0categories = ""See docs/threat-model.md §“Privilege-escalation via MAC label” and
docs/cert-issuance.md §“MAX_INTEGRITY”.
Further reading
Section titled “Further reading”- docs/install.md — step-by-step installation.
- docs/architecture.md — the trust model and the IPC protocol.
- docs/threat-model.md — every field through the lens of threats.
- docs/operations.md — how to change the config on a running machine without dropping sessions.