Skip to content

Census TOML format reference

A complete field-by-field reference for every TOML file Census reads. It complements getting-started.md (which shows the common case) with the full surface: every field, its type, whether it is required, its default, and its meaning.

Census reads these file kinds:

FileWho writes itStrictnessThis doc
Declaration (declaration.toml)operator / control planestrict (unknown keys rejected)§1
Role slice (role-store <role>.toml)operator / Tesseratolerant top-level; strict [[payload.files]]§2
Catalog permission (share/permissions/**/*.toml)catalog authorstrict§3 (summary + links)
Framework (frameworks/<fw>/*.toml)compliance authorstrict§4 (summary + links)
Managed registry (/var/lib/census/managed.toml)Census only§5 (do not edit)
Audit config (/etc/census/exposure.toml)operatorstrict§6 (optional)

The authoritative, machine-readable schemas live in contract/*.schema.json (generated from the parsers, golden-locked). This document is the prose mirror; on any disagreement the schema wins.

Conventions. “Required” means the parse fails without it. “strict” means deny_unknown_fields — a typo’d key is an error (fail-closed), not silently ignored. “tolerant” means unknown keys are skipped (a format another tool co-owns).


The declaration is the device’s desired state: which role-accounts and groups should exist. It is parsed strictly — an unknown key anywhere is an error.

KeyTypeRequiredMeaning
schemaintegeryesParser format version of the declaration. 1 today. Census checks it first, before any other validation, and refuses a declaration whose schema exceeds what this build supports (fail-closed, no mutation) — see §1.1.1.
versionintegeryesMonotonic anti-rollback counter for the declaration’s content (replay protection for the signature) — not a format version. Enforced only in managed/signed mode; not checked under --trust-fs. Bump it on every newly signed declaration. See §1.1.1.
role_storepathyesPath to the role-store directory, resolved relative to the working directory Census runs from (or absolute).
[defaults]tableyesDefault account attributes (§1.2).
[[role_account]]array of tablesnoThe role-accounts to provision (§1.3).
[[group]]array of tablesnoStandalone groups to provision (§1.4).
[[role_group]]array of tablesnoBind a role’s grants to a declared group (§1.5).
signaturestring (hex)only in managed modeDetached Ed25519 signature over the declaration bytes. Present when the declaration is centrally signed; absent under --trust-fs (standalone). You do not write this by hand — the control plane adds it.

1.1.1 schema vs version — two different numbers

Section titled “1.1.1 schema vs version — two different numbers”

These two integer fields look alike and are easy to confuse, but they answer different questions:

FieldAnswersEnforced
schema”Which TOML format is this?” — the parser format version.Always. Checked first, before any other field; a schema newer than this build supports → refuse, fail-closed, no mutation.
version”How recent is this content?” — a monotonic anti-rollback counter that protects the signature against replay.Only in managed (signed) mode; not checked under --trust-fs.
  • schema is about the shape of the file. Bump it only when the format itself changes incompatibly; a build that does not understand a newer schema stops cleanly with a clear message instead of failing deep inside on an unknown key.
  • version is about the contents. Bump it on every newly signed declaration so an attacker cannot replay an older signed copy to roll a device back to stale access. In standalone (--trust-fs) mode there is no signature to replay, so version is recorded but not enforced.

They move independently: re-issuing a device’s access bumps version while schema stays put; migrating the file format bumps schema while version is unaffected. Collapsing them into one field would force either a false rollback refusal on a format upgrade, or a hole in replay protection on a schema refactor.

Applied to every role-account unless the account overrides them. Strict.

KeyTypeRequiredMeaning
uid_range[integer, integer]yesInclusive [low, high] UID window. Every account uid must fall inside it; an auto-assigned UID is drawn from it.
shellstringyesDefault login shell (e.g. /bin/bash).
home_basepathyesParent directory for role-account homes; an account’s home defaults to <home_base>/<role>.
[defaults]
uid_range = [9000, 9999]
shell = "/bin/bash"
home_base = "/var/lib/census/home"

One entry per role-account. Strict. An account is one of two mutually exclusive kinds, distinguished by its identity source:

  • Created — carries an explicit uid. Census creates the Unix user (named after the role) at that fleet-stable UID. This is the normal case.
  • Adopted — carries user (the name of an existing OS account) and adopt = true, and must not carry uid. Census binds the role’s grants to that pre-existing account and never runs useradd/userdel — it does not assign a UID to a user it did not create.

uid and user are mutually exclusive; declaring both is rejected.

KeyTypeRequiredDefaultMeaning
rolestringyesThe role name; must match a slice in the role-store (§2).
uidintegerCreated: yesExplicit, fleet-stable UID. Must be inside uid_range. Absent ⇒ the account must be Adopted.
userstringAdopted: yesName of the existing OS user to adopt. Mutually exclusive with uid; requires adopt = true.
adoptboolnofalsetrue marks the account Adopted (requires user, forbids uid). false is a Created account keyed by uid.
shellstringno[defaults].shellPer-account login-shell override.
homepathno<home_base>/<role>Per-account home override.
# Created account (the normal case)
[[role_account]]
role = "oper"
uid = 9001
# Adopted account — bind the role's grants to an existing `svc` user
[[role_account]]
role = "legacy-svc"
user = "svc"
adopt = true

A Created role-account is provisioned with a locked password and no authorized_keys — its only entry path is the authenticator’s PAM service. These are not declaration fields; Census enforces them on creation. An Adopted account’s credential state is left as-is (Census never runs useradd/userdel on it).

Standalone groups Census should own. Strict.

KeyTypeRequiredDefaultMeaning
namestringyesGroup name.
gidintegernoautoPinned GID. If the GID already belongs to a different group, apply refuses — it never renumbers.
adoptboolnofalseAdopt a pre-existing group of this name instead of creating it.
membersarray of stringno[]Member account names.
[[group]]
name = "kiosk-ops"
members = ["oper"]

A grant binding: attach a role’s resolved permissions to a group, so every member of the group inherits them (many-to-one — several roles may bind to the same group). Strict.

KeyTypeRequiredMeaning
rolestringyesThe role whose grants are bound.
groupstringyesTarget group — must name a [[group]] declared in the same declaration (§1.4).
[[group]]
name = "kiosk-ops"
[[role_group]]
role = "oper"
group = "kiosk-ops"

2. Role slice — <role-store>/<role>.toml

Section titled “2. Role slice — <role-store>/<role>.toml”

One file per role. The top level is tolerant — Census reads only the keys it consumes and ignores the rest (the role schema is co-owned by Tessera, which adds adapter fields Census does not need). Everything Census acts on lives under [payload].

KeyTypeUsed by CensusMeaning
rolestringinformationalRole name (should match the declaration’s role).
versionintegerinformationalSlice schema version.
osstringinformationalTarget OS family (e.g. linux).
namestringinformationalHuman-readable role title.
levelintegerinformationalRole tier/level (Tessera-owned).
[payload]tableyesThe access Census materializes (§2.2).

Unknown top-level keys are ignored (tolerant).

All fields optional; tolerant (unknown keys ignored). The raw primitives (groups, sudo, sudo_role, limits, files) are an escape hatch that is unioned with the expansion of permissions — you can use either, or both.

KeyTypeMeaning
permissionsarrayPermission references expanded against the catalog (§2.3). The normal way to grant access.
groupsarray of stringRaw supplementary groups added directly (escape hatch — bypasses the catalog).
sudoarray of stringRaw inline sudo command rules carried directly (escape hatch — bypasses the catalog). Literal absolute command paths only; see §2.7.
sudo_rolestringA raw sudo role name carried directly (escape hatch).
[payload.limits]tableResource limits (§2.4).
[[payload.files]]array of tablesRaw inline file-access grants (§2.5).
role = "oper"
version = 1
os = "linux"
name = "Device operator"
level = 3
[payload]
permissions = ["service-restart", "log-read", { id = "service-control", units = "nginx" }, "nginx.operate"]
groups = ["video"] # escape hatch, unioned in
sudo = ["/usr/sbin/reboot"] # escape hatch, raw sudo command (§2.7)
sudo_role = "operations" # escape hatch
[payload.limits]
nofile = 8192
[[payload.files]]
path = "/var/lib/app/state"
access = "rw"
recursive = true

Each element of permissions is one of:

  1. Bare id — a string naming a leaf, bundle, or package:
    permissions = ["log-read", "network-config", "nginx.operate"]
  2. Parametrized — a table with a required id plus parameters that fill the permission’s {placeholder} templates (e.g. the unit(s) a service-* permission applies to). A list parameter expands to one rendered rule per element:
    permissions = [
    { id = "service-control", units = "nginx" },
    { id = "service-observe", units = ["nginx", "mosquitto"] },
    ]
    The table form is tolerant: keys other than id are captured as parameters, so a parameter name the catalog record does not use is simply inert (not an error).

A leaf is a single capability; a bundle aggregates others (resolved transitively, its risk class = the max of its members); a package is a curated <app>.{observe|operate|admin} tier. See §3 and the catalog itself for the full permission list.

KeyTypeMeaning
nofileintegerRLIMIT_NOFILE (max open files).
nprocintegerRLIMIT_NPROC (max processes).

Inline file-access grants, the same shape as a catalog [[file]] grant. Unlike the rest of the payload this block is strict (deny_unknown_fields): a role file grant is materialized as root via setfacl, so a typo’d key fails closed.

KeyTypeRequiredDefaultMeaning
pathstringyesAbsolute path to a directory, file, or glob. Must be literal — a placeholder/template is rejected in a role file grant (no {…}).
accessstringyesThe access bits — see §2.6.
recursiveboolnofalseFor a directory: apply recursively and set a default-ACL so new files inherit the access.

Directory vs single file. A directory grant (recursive = true) is rewrite-proof and enforced by the always-available ACL backend. A grant on a single file requires a per-file backend; on a system without one, apply refuses it (atomically — nothing applies). Prefer directory grants.

access is a set of bits — read (r), write (w), execute (x), traverse (X, directory search / conditional execute). Two legacy aliases cover the common cases, and there are canonical compact strings for the rest:

ValueBitsUse
"ro"{read, traverse} (r-X)read-only access to a tree
"rw"{read, write, traverse} (rwX)read-write access to a tree
canonical compact stringsany combination of r w x Xprecise control

For most grants "ro" and "rw" are what you want.

2.7 payload.sudo — raw sudo commands (escape hatch)

Section titled “2.7 payload.sudo — raw sudo commands (escape hatch)”

sudo under [payload] is the command-level twin of [[payload.files]]: a raw list of sudo command rules carried directly into the role, unioned with whatever the role’s permissions expand to — the same way a catalog permission’s sudo field is. Use it for a command that has no catalog permission yet.

[payload]
sudo = ["/usr/sbin/reboot", "/usr/bin/systemctl"]

Constraints — validated before anything is written to sudoers, fail-closed on a violation:

  • Literal absolute command paths only — each entry must start with /.
  • No arguments and no {placeholder} templates. Parametrization with confinement (a {unit} guarded by [params] constraints) stays the prerogative of a catalog id — there is nothing to bound an inline parameter, so it is rejected.
  • Printable ASCII, no shell metacharacters (; | & $ < > …) — refused so a value cannot smuggle a second command into the sudoers line.

Each entry materializes into sudoers.d/census-<role> as root, so it is a genuine privilege grant. Because it bypasses the curated catalog it carries no risk labelcensus show and census compile --lint therefore flag inline payload.sudo (like [[payload.files]]) as raw / unlabeled escalation-capable, so a reviewer always sees it. Prefer a catalog permission when one exists; reach for payload.sudo only as a deliberate escape hatch.


Catalog files under share/permissions/<layer>/*.toml define the permissions a role references. Authoring them is covered in depth in catalog-authoring.md and authoring-packages.md (both in Russian); the shape in brief:

KeyTypeMeaning
idstringPermission id (dotted for packages, e.g. nginx.operate).
riskstringcontained or escalation-capable (a bundle’s = max of members).
categorystringDomain grouping (e.g. network, app, os-config).
sudoarray of stringAbsolute sudo command rules (may carry {placeholder} templates).
groupsarray of stringSupplementary groups granted.
[limits]tablenofile / nproc.
[[file]]array of tablesFile-access grants (same shape as §2.5; catalog grants may use {placeholder} templates).
includesarrayOther permission ids this one aggregates (bundle); a table element { id, <bindings> } binds a member’s parameters.
include_categoriesarray of stringAggregate every permission in the named categories.
[params.<name>]tableA parameter guard rail (`kind = token

Per-OS layering: a permission resolves along linux → linux-<distro> → linux-<distro>-<version>; a layer can replace or append fields of the base. Human text (title / summary / risk_note) lives in the separate l10n/<locale>/ tree, keyed by [<id>], not in the permission file.

Authoritative schema: contract/catalog-permission.schema.json.


The advisory compliance cross-reference. Frameworks live under frameworks/<fw>/:

  • framework.toml — manifest (dimension = flat | os-layered, version, provides).
  • mappings/*.toml — keyed by permission id; each link carries a polarity: satisfies (addresses the control — the only polarity coverage counts), risk (undermines it), or related (neutral).
  • controls.toml (optional) — the control list; an owned flag marks the controls Census actually covers (so framework coverage can report gaps).

It is read-only and advisory — it never participates in compile/grant/ apply, so a tampered mapping can only mislabel coverage, never escalate privilege. Authoritative schema: contract/framework.schema.json. See the README’s “Compliance frameworks” section.


5. Managed registry — /var/lib/census/managed.toml

Section titled “5. Managed registry — /var/lib/census/managed.toml”

The root-only record of what Census has provisioned (accounts, groups, the grants attached to each, the applied declaration version). Census owns this file — do not hand-edit it. It is how Census knows what is its to reconcile or tear down, so editing it can orphan real OS objects or make Census touch something it did not create. Inspect it read-only with census status. Authoritative schema: contract/managed-registry.schema.json.


6. Audit config — /etc/census/exposure.toml

Section titled “6. Audit config — /etc/census/exposure.toml”

Optional configuration for the read-only exposure audit (census audit fs / census audit expose). Parsed strictly (deny_unknown_fields). The file is optional and every key is optional: an absent file, or an absent key, falls back to the built-in default. A present but malformed file is a hard error — never a silent default (a misconfigured security tool must fail loudly, not scan the wrong thing while looking healthy). Pass a non-default path with --config.

KeyTypeRequiredDefaultMeaning
scan_rootsarray of pathno["/etc","/var","/opt","/usr/local","/srv","/home","/root"]Trees the default scan covers. Every entry must be absolute; an empty list is rejected (a scan of nothing that reports “all clear” is a trap). Overridden per-run by --root/--full.
secret_globsarray of stringno["/etc/shadow*","**/*.key","**/*.pem","**/id_rsa*","**/.env*","**/*credentials*"]Globs that mark an object secret-class (so an other-readable match is a leak). Each pattern may contain at most one ** (the matcher backtracks across **; two would be exponential on a --full scan) — a pattern with more is rejected at load.
broad_groupsarray of stringno["adm","wheel","sudo","staff","users"]Group names treated as “broad” for the broad-group-writable axis. Matched against the group’s real name resolved from /etc/group, so a host that renumbered the gid is still caught.
/etc/census/exposure.toml
scan_roots = ["/etc", "/var", "/opt", "/usr/local", "/srv", "/home", "/root"]
secret_globs = ["/etc/shadow*", "**/*.key", "**/*.pem", "**/id_rsa*", "**/.env*", "**/*credentials*"]
broad_groups = ["adm", "wheel", "sudo", "staff", "users"]

The default **/*.pem glob also matches public certificates (e.g. /etc/ssl/certs), which are world-readable by design and surface as low-signal secret findings — narrow secret_globs if that noise is unwanted. See audit.md §6.


census plan prints the high-level create/update/delete actions. Add --diff to see the concrete artifacts each change would write, as a unified diff — current managed state vs the resolved target:

Terminal window
census plan --declaration declaration.toml --additional-catalog-dir /opt/census/share/permissions --diff

plan --diff shows, per changed account:

  • the sudoers fragment that would be written (including the run-as spec) and its target file path (/etc/sudoers.d/census-<role>);
  • the file-access ACL grant delta — which path grants are added or removed.

It is read-only: no filesystem mutation, no root required. Use it to review exactly what an apply would change before running it — especially after editing a role’s permissions, to confirm the resulting sudo lines and ACLs are what you intend.


  • getting-started.md — install, configure, first apply, operate.
  • catalog-authoring.md — authoring catalog permissions and per-OS layers (Russian).
  • authoring-packages.md — authoring add-on packages and curated app tiers (Russian).
  • contract/*.schema.json — the authoritative machine-readable schemas.