souveraine-components: per-binary docs + RedFlag-shaped audit tracker
One page each for machined, secrets, sessiond — what each owns, what crosses its boundary, and the open gaps before it reaches the RedFlag capability-token bar. Gaps mapped to concrete RedFlag primitives (token, closure hash, canonical message, KeyID, independent verifier, binary integrity, eBPF) with file:line anchors, not doctrine-summary hand-waving. audit-status.md rolls up priority: P0 unlock/handoff attestation, P1 machined token issuance, P2 secrets caller identity, P3 per-agent UIDs, P4 binary watchdog shared-deferred with RedFlag SEC-022.
This commit is contained in:
parent
0f7ab9e38e
commit
4235848bee
5 changed files with 427 additions and 0 deletions
45
docs/souveraine-components/README.md
Normal file
45
docs/souveraine-components/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Souveraine components — substrate binaries
|
||||
|
||||
One page per secure Rust binary in the Souveraine substrate. These are the
|
||||
system- and user-tier daemons that hold authority the shell and agents never
|
||||
touch directly: machine identity, the secret service, and the session
|
||||
authority. Each page names what the binary owns, what crosses its boundary,
|
||||
and the open gaps before it reaches the RedFlag security bar.
|
||||
|
||||
The family is small on purpose. Adding a binary here means it holds
|
||||
authority no other component should hold — signing keys, unlock verbs,
|
||||
secret material. If a daemon does not meet that bar, it is not a component,
|
||||
it is a library.
|
||||
|
||||
## Members
|
||||
|
||||
| Binary | Tier | Owns | Page |
|
||||
| --- | --- | --- | --- |
|
||||
| `souveraine-machined` | system | machine Ed25519 seed | [machined.md](machined.md) |
|
||||
| `souveraine-secrets` | user (session) | `org.freedesktop.secrets` backed by the seed lineage | [secrets.md](secrets.md) |
|
||||
| `souveraine-sessiond` | user (session) | ext-session-lock; the unlock verb | [sessiond.md](sessiond.md) |
|
||||
|
||||
## The security bar these are built toward
|
||||
|
||||
Every component is on a path to the same posture RedFlag already embodies
|
||||
(SESSION-AUTHORITY-DOCTRINE §10/§11, SECURITY-AUDIT P1–P7):
|
||||
|
||||
- **Binaries verify each other.** A component that asks another for an
|
||||
authority action (sign, unlock, release a secret) must present an
|
||||
attestation the target checks — a hash or capability token resolved over a
|
||||
closure, not a name in a config file.
|
||||
- **Capability tokens, not installation.** Authority rides the federated
|
||||
seed identity (P7), not "this binary is in /usr/local/bin." The kernel
|
||||
stops are defense-in-depth, not the gate (RedFlag constraint #5).
|
||||
- **One authority.** Session authority and binary authority are the same
|
||||
authority (§11). sessiond verifies capability tokens on the things that ask
|
||||
it to unlock or step up; machined signs them; secrets releases against
|
||||
them.
|
||||
- **Seeds are preconditions.** No daemon creates identity. A missing seed
|
||||
fails loudly; generation is a deliberate, guarded CLI action.
|
||||
|
||||
Where a component has not yet reached that bar, the gap is recorded in its
|
||||
page under **Audit gaps** and rolled up in [audit-status.md](audit-status.md).
|
||||
The intent is that the audit doc is the live tracker: when a gap closes, the
|
||||
line moves there, and the component page's Audit gaps section is updated to
|
||||
match.
|
||||
88
docs/souveraine-components/audit-status.md
Normal file
88
docs/souveraine-components/audit-status.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Component audit status — toward the RedFlag security bar
|
||||
|
||||
Live tracker. Each component page carries an **Audit gaps** section with the
|
||||
detail; this page is the rollup: which RedFlag primitive each gap maps to,
|
||||
and where it sits in priority. When a gap closes, move it here to "Closed"
|
||||
and update the component page.
|
||||
|
||||
The bar is read from RedFlag's source, not the doctrine summary. The five
|
||||
primitives that matter, with anchors into `~/Projects/RedFlag`:
|
||||
|
||||
| Primitive | What it is in RedFlag | Anchor |
|
||||
| --- | --- | --- |
|
||||
| **Capability token** | Ed25519-signed, forward-only (`Version`), operation-scoped, expiring authorization | `agent/internal/capability/token.go` |
|
||||
| **Closure hash** | `sha256` over sorted/dedup `name@version#sha256` — binds token to exactly what it authorizes | `Token.ClosureHash()` |
|
||||
| **Canonical message** | Deterministic field-delimited string that is signed/verified | `Token.CanonicalMessage()` |
|
||||
| **KeyID fingerprint** | `hex(sha256(pubkey)[:16])` — binds a signature to a pinned key | `KeyIDFor()` |
|
||||
| **Independent verifier** | The privileged executor verifies the token itself; never trusts the relay's claim | `helper/src/main.rs`, token-executor mode |
|
||||
| **Binary integrity** | SHA-256 compare (`verify-binary`), exit-on-mismatch | `helper/src/main.rs:2226-2283` |
|
||||
| **Kernel stop (defense-in-depth)** | eBPF on `execve`, denies outside a valid token context; not the gate | `docs/tasks/KERN-001` |
|
||||
| **Least-privilege executor** | network-less, single-op, exact-match sudoers | `helper/src/main.rs` |
|
||||
|
||||
Two things to internalize from RedFlag's own status, because they set
|
||||
expectations for Souveraine:
|
||||
|
||||
1. **Binary mutual attestation is deliberately unwired in RedFlag too**
|
||||
(SEC-022: "kill-on-mismatch and phone-home are deliberately not wired").
|
||||
The standing guarantee in both systems is DAC — `root`/owner-owned
|
||||
binaries the running user cannot overwrite. The watchdog raises the cost
|
||||
on top of that; it is not the gate.
|
||||
2. **The token is the universal primitive, not just for installs.** RedFlag's
|
||||
ARCH-001 direction is "every fleet operation is a capability" — nothing
|
||||
runs on standing authority. Souveraine's equivalent: every authority verb
|
||||
(sign, unlock, handoff, release-secret, step-up) should be a capability,
|
||||
not a "the caller is local so it's fine."
|
||||
|
||||
## Priority order
|
||||
|
||||
### P0 — the unlock/handoff verb has no attestation
|
||||
**Component:** sessiond. **Gap:** `shell_ready` / `lock` honored on any
|
||||
same-uid caller; no token. **Primitive missing:** capability token +
|
||||
independent verifier. **Why P0:** unlock-adjacent verbs are the highest-value
|
||||
authority in the system; a same-uid process that reaches the socket can
|
||||
induce a handoff or retake. **Closes when:** the shell presents a
|
||||
machined-signed token (operation-scoped, short expiry, closure carrying the
|
||||
shell binary hash) that sessiond verifies against the pinned machine pubkey
|
||||
before honoring `shell_ready`. Depends on machined issuing tokens (P1).
|
||||
|
||||
### P1 — machined signs but does not issue tokens
|
||||
**Component:** machined. **Gap:** signs raw `domain:payload` bytes; no
|
||||
operation, expiry, closure, or KeyID. **Primitive missing:** capability
|
||||
token (full), closure hash, canonical message, KeyID. **Why P1:** it is the
|
||||
root authority; every other attestation depends on it minting real tokens.
|
||||
**Closes when:** machined issues forward-only, expiring, operation-scoped
|
||||
tokens over a canonical message that includes the authorized target (verb +
|
||||
binary hash), advertises a KeyID fingerprint, and the signing/verification
|
||||
logic is byte-identical across mint and verifier (RedFlag invariant:
|
||||
`agent/internal/capability/token.go` ↔ `helper/src/main.rs`).
|
||||
|
||||
### P2 — secrets releases on D-Bus policy, not caller identity
|
||||
**Component:** secrets. **Gap:** any session-bus process can `GetSecrets`;
|
||||
no per-caller capability, no item-scoping, no `SO_PEERCRED`. **Primitive
|
||||
missing:** capability token with closure = item path; independent verifier.
|
||||
**Why P2:** the data is highest-sensitivity but the trust boundary (the
|
||||
unlocked user session) is currently accepted by design; closes when personal-
|
||||
tier credentials require a step-up token (§10) honored by secrets, not just
|
||||
session-unlocked.
|
||||
|
||||
### P3 — caller identity is uid 1000 for everything
|
||||
**Cross-cutting.** **Gap:** `SO_PEERCRED` is logged (machined) or unread
|
||||
(sessiond); every substrate caller is the same UID, so the audit trail
|
||||
cannot distinguish Annie from a curl script. **Primitive it blocks:**
|
||||
`AgentID`-equivalent — the token's "who is asking" field. **Closes when:**
|
||||
per-agent UNIX accounts land (services-infrastructure review), making
|
||||
`SO_PEERCRED` kernel-backed caller identity that the token binds to. This is
|
||||
the enabler for P0/P1/P2, tracked separately because it is account
|
||||
provisioning, not crypto.
|
||||
|
||||
### P4 — binary integrity watchdog (shared with RedFlag)
|
||||
**All components.** **Gap:** no self/peer SHA-256 verification. **Primitive
|
||||
missing:** `verify-binary` equivalent, wired. **Status:** deliberately
|
||||
deferred, same as RedFlag SEC-022. DAC (`root`/owner-owned, non-overwritable)
|
||||
is the standing guarantee. When the capability token lands (P1), the token's
|
||||
closure should carry each component's expected hash so a swapped binary
|
||||
cannot replay a valid old token — that is the point at which the watchdog
|
||||
becomes load-bearing rather than ornamental.
|
||||
|
||||
## Closed
|
||||
(none yet — component family documented 2026-07-16, all gaps open.)
|
||||
102
docs/souveraine-components/machined.md
Normal file
102
docs/souveraine-components/machined.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# souveraine-machined
|
||||
|
||||
System-tier machine identity daemon. The root of the authority family — it
|
||||
holds the machine's Ed25519 seed and hands out signatures, never key
|
||||
material. Every other component that needs to prove machine identity asks
|
||||
this one to sign.
|
||||
|
||||
Source: `src/bin/souveraine-machined.rs`, `src/machined/{protocol,server,signer,client}.rs`.
|
||||
RedFlag analog: the server's `SigningService` (authority that mints and
|
||||
signs) in `server/internal/services/capability_minter.go`.
|
||||
|
||||
## What it owns
|
||||
|
||||
- The machine Ed25519 seed at `/var/lib/souveraine/seed-id`.
|
||||
- A Unix socket at `/run/souveraine/machined.sock` (group `souveraine`,
|
||||
group-readable/writable).
|
||||
|
||||
The seed is a **precondition, never created by the daemon**. If no seed
|
||||
exists, startup fails loudly. Identity creation lives in the deliberate,
|
||||
guarded CLI verb `souveraine machine init`. Same doctrine as secrets and
|
||||
sessiond; see `SeedId::load` in `core/identity/seed.rs`.
|
||||
|
||||
## What crosses the boundary
|
||||
|
||||
Requests are JSON-lines over the socket. Two verbs:
|
||||
|
||||
- **pubkey** — the machine's public key (safe to share).
|
||||
- **sign { domain, payload_hex }** — a signature over domain-separated
|
||||
bytes. The signed material is `{SIGNING_CONTEXT}:{domain}:{payload}`,
|
||||
never caller-supplied raw. `domain` is restricted to ASCII
|
||||
alnum/`-`/`_`/`.`/`,`, max 64 chars; the daemon refuses anything else.
|
||||
|
||||
The private key never leaves the process. Callers get signatures and the
|
||||
public key — that is the entire surface.
|
||||
|
||||
## Identity of the caller
|
||||
|
||||
`SO_PEERCRED` is read on every connection; uid/gid/pid are logged on every
|
||||
sign request and every refusal (`src/machined/server.rs:25-43`). Today this
|
||||
is **observability, not enforcement** — any process in the `souveraine`
|
||||
group can request a signature. That is the central audit gap (below).
|
||||
|
||||
## Today's security posture
|
||||
|
||||
- **Domain separation** prevents the daemon from being turned into an oracle
|
||||
for arbitrary signatures: a signature under one domain does not validate
|
||||
in another. This is the same principle as RedFlag's `CanonicalMessage`
|
||||
(`agent/internal/capability/token.go`) — the signed bytes are a fixed,
|
||||
field-delimited string, never caller-controlled in shape.
|
||||
- Seeds are never daemon-generated; no background process can quietly mint
|
||||
a machine identity.
|
||||
- The socket is group-scoped, so membership in `souveraine` is the coarse
|
||||
gate.
|
||||
|
||||
## Audit gaps (toward the RedFlag bar)
|
||||
|
||||
The target posture is RedFlag's capability-token model, read from its
|
||||
source (not the doctrine summary):
|
||||
|
||||
- **Capability token = Ed25519-signed, forward-only, scoped, expiring.**
|
||||
RedFlag's `Token` carries `Version` (forward-only — new versions add
|
||||
fields, never reinterpret), `Operation`, `IssuedAt`/`NotBefore`/`ExpiresAt`,
|
||||
and a `Signature` (`agent/internal/capability/token.go`). machined today
|
||||
signs caller payloads but **does not issue scoped, expiring tokens** — it
|
||||
signs raw `domain:payload` bytes with no operation, no expiry, no closure.
|
||||
Gap: callers cannot present a *machined-issued* token to a verifier,
|
||||
because machined mints signatures, not tokens.
|
||||
- **Closure hash binds the token to exactly what it authorizes.** RedFlag's
|
||||
`ClosureHash` is `sha256` over sorted, de-duplicated `name@version#sha256`
|
||||
lines, so neither order nor duplicates change the digest. The Souveraine
|
||||
analog for "what is this signature authorizing" does not exist yet — the
|
||||
`payload_hex` is opaque to the daemon. When tokens land, the authorized
|
||||
*thing* (a verb, a target binary hash, a session) must be inside the
|
||||
canonical message the way RedFlag's closure is.
|
||||
- **KeyID binds a signature to a key fingerprint.** RedFlag's `KeyIDFor` is
|
||||
`hex(sha256(pubkey)[:16])`. machined exposes the pubkey but does not
|
||||
advertise a fingerprint verifiers can pin; a rotating seed has no
|
||||
discoverable, comparable identity today.
|
||||
- **The verifier must be independent.** In RedFlag the privileged executor
|
||||
(`helper/`) verifies the token itself — it does not trust the agent's
|
||||
claim. Souveraine's analog: whatever consumes a machined signature must
|
||||
verify it against the pinned pubkey, not trust the relay. No such verifier
|
||||
is wired yet.
|
||||
- **`SO_PEERCRED` is logged, not enforced.** Logging is necessary for audit
|
||||
(P3: use is visible) but not sufficient as a gate. Per-agent UIDs (the
|
||||
services-infrastructure review) make `SO_PEERCRED` kernel-backed *caller*
|
||||
identity, which is what the token's `AgentID` field is in RedFlag — but
|
||||
today every caller is uid 1000.
|
||||
|
||||
## Binary integrity of machined itself
|
||||
|
||||
RedFlag's binary-attestation primitive is `verify-binary <path>
|
||||
<expected_sha256>` (`helper/src/main.rs:608-667`) — SHA-256 compare, exit
|
||||
`EXIT_INTEGRITY=21` on mismatch. It is **deliberately not wired** in RedFlag
|
||||
either (SEC-022: "kill-on-mismatch and phone-home are deliberately not
|
||||
wired"). machined has no self- or peer-integrity check today. When this
|
||||
lands, the same SHA-256-compare shape applies, and — as in RedFlag — the
|
||||
DAC/sudoers layer (`root:root 0755`, non-overwritable by the running user)
|
||||
is the integrity guarantee that holds *until* the watchdog is wired.
|
||||
|
||||
See [audit-status.md](audit-status.md) for where these land in priority
|
||||
order.
|
||||
91
docs/souveraine-components/secrets.md
Normal file
91
docs/souveraine-components/secrets.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# souveraine-secrets
|
||||
|
||||
User-tier Secret Service daemon. Implements `org.freedesktop.secrets`
|
||||
(`org.freedesktop.Secret.Service`) on the session D-Bus so libsecret clients
|
||||
— Chatty/libcmatrix, and our own `keyring-core` credential store — get a
|
||||
secrets backend without gnome-keyring or KWallet.
|
||||
|
||||
Source: `src/bin/souveraine-secrets.rs`, `src/secrets/{service,collection,item,store,session,session_object,transport,dh,types,error}.rs`.
|
||||
|
||||
## The load-bearing choice
|
||||
|
||||
The store is keyed from the machine's existing Ed25519 `SeedId`, **not** a
|
||||
passphrase-unlocked keyring. Concretely (`src/secrets/store.rs`): the seed
|
||||
never leaves the process and is never used directly as the storage key.
|
||||
Instead the daemon signs a fixed domain-separation string and runs the
|
||||
(deterministic) Ed25519 signature through HKDF-SHA256 to derive a dedicated
|
||||
AES-256 key. That keeps "sign federation events" (machined's domain) and
|
||||
"encrypt local secrets" as **separate derived keys from one root**, rather
|
||||
than reusing the same key material for two purposes.
|
||||
|
||||
The seed is a **precondition, never created by the daemon**. No seed at
|
||||
`~/.souveraine/seed-id` → loud startup failure. Generation lives in the
|
||||
guarded CLI verb `souveraine identity generate`.
|
||||
|
||||
## Why everything is "permanently unlocked"
|
||||
|
||||
Because the seed identity is resident in the process for its whole lifetime,
|
||||
there is no separate "unlock the keyring" step. `OpenSession` always
|
||||
succeeds immediately; `Unlock` is a no-op (`src/secrets/service.rs`). The
|
||||
protection is not a passphrase gate at the D-Bus layer — it is that the
|
||||
encrypted blob on disk is useless without the seed, and the seed lives in
|
||||
machined's domain.
|
||||
|
||||
## What crosses the boundary
|
||||
|
||||
The Secret Service protocol's own crypto is implemented faithfully so
|
||||
libsecret clients interoperate:
|
||||
|
||||
- **Key exchange** (`dh.rs`): `dh-ietf1024-sha256-aes128-cbc-pkcs7` —
|
||||
classic (non-EC) DH over RFC 2409 Second Oakley Group (MODP 1024-bit),
|
||||
shared secret through HKDF-SHA256 → 16-byte AES-128 session key. Mirrors
|
||||
libsecret's `secret-session.c` byte-for-byte.
|
||||
- **Transport** (`transport.rs`): AES-128-CBC + PKCS7, IV = 16 fresh random
|
||||
bytes per secret, wire shape `(session_path, iv, ciphertext, content_type)`.
|
||||
- **Algorithms**: AES negotiated first, `plain` fallback only if the daemon
|
||||
returns `NotSupported` — covering every real libsecret client (`session.rs`).
|
||||
|
||||
## Audit gaps (toward the RedFlag bar)
|
||||
|
||||
secrets is the furthest of the three from the RedFlag posture, because its
|
||||
threat model is currently "the user's session is the trust boundary" rather
|
||||
than "every caller is attested." Read against RedFlag's primitives:
|
||||
|
||||
- **No caller attestation on `GetSecret`.** Any process on the session bus
|
||||
that can talk `org.freedesktop.secrets` can read items once the collection
|
||||
is open. RedFlag's answer to "who is asking" is the capability token's
|
||||
`AgentID` + `Operation` fields, verified independently by the executor.
|
||||
secrets has no equivalent: there is no per-caller capability on
|
||||
`GetSecrets`, no `SO_PEERCRED`-backed caller identity, no "this token
|
||||
authorizes reading *this* item." The D-Bus policy is the only gate, and
|
||||
D-Bus policy is installation-based, not identity-based (exactly what P7
|
||||
rejects).
|
||||
- **No closure/scoping.** RedFlag's token binds to a closure hash — the
|
||||
secret is only released for the exact artifact set authorized. A secrets
|
||||
equivalent would bind a token to a specific item or collection path, so a
|
||||
token minted for Chatty's Matrix credential cannot read the email
|
||||
credential. Nothing like this exists today.
|
||||
- **`Unlock` is a no-op.** This is intentional given the seed-rooted design,
|
||||
but it means there is no step-up / freshness at the secrets layer. Step-up
|
||||
lives in sessiond (the `souveraine-stepup` PAM service); secrets should
|
||||
eventually honor a step-up token before releasing personal-tier
|
||||
credentials, not rely on "the session is already unlocked."
|
||||
- **KeyID/fingerprint not pinned.** As with machined, the seed identity has
|
||||
no advertised fingerprint (`hex(sha256(pubkey)[:16])`) that a caller or
|
||||
auditor pins. A seed rotation silently changes what "the keyring" is.
|
||||
- **Binary integrity.** Same as machined and RedFlag SEC-022: no
|
||||
self/peer hash verification. DAC (`root`/owner-owned, non-overwritable)
|
||||
is the standing guarantee.
|
||||
|
||||
## Federation note
|
||||
|
||||
Because the root is the machine seed, and seeds federate (FEDERATION.md),
|
||||
there is a deliberate design tension to resolve in the review: a secret
|
||||
encrypted under machine A's derived key is not readable on machine B even
|
||||
though both share the federated *agent* identity. The seed-per-machine vs
|
||||
seed-per-agent question (machined owns the *machine* seed; agents have
|
||||
their own seed lineage) is exactly where secrets' trust model is decided.
|
||||
Recorded here, not answered — it belongs in the services-infrastructure
|
||||
review.
|
||||
|
||||
See [audit-status.md](audit-status.md) for priority.
|
||||
101
docs/souveraine-components/sessiond.md
Normal file
101
docs/souveraine-components/sessiond.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# souveraine-sessiond
|
||||
|
||||
User-tier session authority daemon. Owns the ext-session-lock-v1 surface
|
||||
and the unlock verb — the one thing in the system that can release the
|
||||
session lock. The shell is a *client* of this authority, not the authority
|
||||
itself.
|
||||
|
||||
Source: `src/bin/souveraine-sessiond.rs`, `src/sessiond/{protocol,server,lock,draw,auth}.rs`.
|
||||
Companion design: `docs/session-authority-boot-order.md` (Phase C),
|
||||
`docs/SESSION-AUTHORITY-DOCTRINE.md` §11.
|
||||
|
||||
## What it owns
|
||||
|
||||
- The ext-session-lock-v1 client that holds the session locked at boot,
|
||||
before the shell exists, and after the shell dies.
|
||||
- The PAM conversation for unlock — hand-rolled libpam FFI (`auth.rs`),
|
||||
no wrapper crate (fewer deps for on-device builds).
|
||||
- A Unix socket at `$XDG_RUNTIME_DIR/souveraine/sessiond.sock`, same-user
|
||||
today (`0600`), machined-shaped JSON-lines protocol.
|
||||
|
||||
## The zero-gap handoff (load-bearing)
|
||||
|
||||
A lock client that drops its Wayland connection **without unlocking** leaves
|
||||
the compositor holding the session locked. With Hyprland's
|
||||
`misc:allow_session_lock_restore`, the next client inherits that locked
|
||||
state. sessiond uses this for two transitions, both fail-closed:
|
||||
|
||||
1. **Boot handoff.** sessiond takes the lock first (splash dissolves onto a
|
||||
locked panel), quickshell starts ~3s later, the bridge calls
|
||||
`shell_ready`, sessiond drops its connection, the shell's lock inherits
|
||||
— no unlocked instant between them.
|
||||
2. **Crash retake.** The bridge connection is the shell's heartbeat. EOF on
|
||||
that connection = shell dead. sessiond retakes the lock
|
||||
(`allow_session_lock_restore`) and renders its own spartan PIN surface.
|
||||
A shell crash while locked is no longer an ssh-only recovery.
|
||||
|
||||
The retake is self-correcting: if sessiond tries to lock against a *live*
|
||||
shell lock, the compositor yields Finished/Denied — no deadlock.
|
||||
|
||||
## What crosses the boundary
|
||||
|
||||
JSON-lines over the socket (`protocol.rs`). Request ops: `status`,
|
||||
`shell_ready`, `locked_ack`, `lock`. Phases: `holding`, `awaiting_shell_lock`,
|
||||
`released`, `idle`. The `shell_ready` connection *is* the heartbeat; its EOF
|
||||
is the retake trigger. Responses are guarded `ok`/`reason` shaped, same
|
||||
convention as machined.
|
||||
|
||||
Unlock is never an RPC verb. The only path to unlock is the PAM
|
||||
conversation on sessiond's own lock surface.
|
||||
|
||||
## Today's security posture
|
||||
|
||||
- **Lock-before-shell.** No desktop surface (NM dialog, bar, background)
|
||||
can precede the lock at boot — the boot fail-open hole that motivated
|
||||
this daemon.
|
||||
- **Lock-past-shell-death.** Shell crash while locked retakes to a PIN, not
|
||||
a locked brick.
|
||||
- **PAM is the unlock path.** `auth include login` (same credentials as
|
||||
console login), unprivileged via `unix_chkpwd`. No bypass verb.
|
||||
- **Fail-closed default.** Default behavior on start is to lock; `--no-initial-lock`
|
||||
is the explicit opt-out for dev.
|
||||
|
||||
## Audit gaps (toward the RedFlag bar)
|
||||
|
||||
sessiond is the component that most directly maps onto RedFlag's "capability
|
||||
token over a resolved closure, signed by an authority, verified by the
|
||||
executor" shape (SESSION-AUTHORITY-DOCTRINE §10), because *unlock is
|
||||
exactly the kind of verb that should require a token*. Today it does not:
|
||||
|
||||
- **`shell_ready` has no caller attestation.** Any same-uid process that
|
||||
can reach the socket can call `shell_ready` and induce a handoff, or call
|
||||
`lock`. RedFlag's analog: the privileged executor verifies the token's
|
||||
signature, `AgentID`, `Operation`, expiry, and closure hash *before*
|
||||
acting — it does not trust "a process asked." sessiond trusts the
|
||||
heartbeat's mere existence. The fix shape: the shell presents a
|
||||
machined-signed capability token (operation=`session_handoff`, expiry
|
||||
short, closure = the shell binary hash) that sessiond verifies against
|
||||
the pinned machine pubkey before honoring `shell_ready`.
|
||||
- **Inhibit / step-up verbs (§6/§10) are stubs.** sessiond is the natural
|
||||
home for the `souveraine-stepup` PAM service and the portal Inhibit
|
||||
backend, and for verifying capability tokens on session verbs. These are
|
||||
named in the doctrine but not implemented; the socket protocol reserves
|
||||
the space.
|
||||
- **`SO_PEERCRED` not yet read.** machined logs uid/gid/pid on every
|
||||
request; sessiond does not yet. Same-user-only today (`server.rs`
|
||||
comment: "widens to a group grant deliberately when agents get accounts").
|
||||
Per-agent UIDs (services review) turn this into kernel-backed caller
|
||||
identity — the substrate of the capability gate.
|
||||
- **Binary integrity.** As with the other two and RedFlag SEC-022: no
|
||||
self/peer hash check. DAC is the standing guarantee. When the capability
|
||||
token lands, the token's closure should carry sessiond's *own* expected
|
||||
hash so a swapped binary cannot present a valid old token — RedFlag's
|
||||
`ClosureEntry.SHA256` shape pointed inward.
|
||||
- **eBPF (KERN-001 shape).** The lock gate's kernel-level stop is
|
||||
ext-session-lock-v1 itself (a Wayland protocol the compositor enforces),
|
||||
which is already a kernel-adjacent guarantee stronger than RedFlag's
|
||||
eBPF-scaffold-for-execve. So sessiond's "defense-in-depth kernel stop"
|
||||
is largely already present via the compositor; the gap is the
|
||||
*token gate* on top, not the kernel enforcement underneath.
|
||||
|
||||
See [audit-status.md](audit-status.md) for priority.
|
||||
Loading…
Reference in a new issue