kernel_keyring
kernel_keyring
¶
Linux kernel-keyring binding for the volatile passphrase cache tier.
A thin, dependency-free ctypes wrapper over libkeyutils.so.1 —
the userspace shim for the kernel key-retention service
(add_key(2) / keyctl(2)). It exposes exactly the four
operations the vault's volatile passphrase cache needs, and nothing
else:
store— cache the SQLCipher passphrase for this uid;load— read it back from any same-uid process;forget— clear it;is_cached— answer the status surfaces' presence question without materialising the secret;unavailable_reason— the setup/probe gate, mirroringterok_sandbox.vault.store.systemd_creds.unavailable_reason.
Why these exact choices (see the prior-art survey on the tier — the kernel keyring is how systemd-ask-password and MIT Kerberos cache passphrases):
- Key type
user, notlogon. The passphrase must be read back to open SQLCipher;logonpayloads are unreadable from userspace by anyone, for any permission mask.useris the only workable type (the same choice cryptsetup's readback path and eCryptfs are forced into). - Anchor
@u(the user keyring), not the persistent keyring. The file this tier replaces lived under$XDG_RUNTIME_DIR, which logind wipes on final logout — so its effective lifetime already was the user keyring's lifetime (per-uid, shared across every same-uid terminal, torn down at logout).@uis the semantic drop-in; the persistent keyring would over-deliver (survive logout) and needskeyctl_get_persistentmachinery we deliberately avoid. - Read it from the operator's own user namespace, nowhere else. A
user keyring is per user namespace: a process inside podman's
rootless namespace resolves
@uto its own empty keyring, and the cache is invisible there however the permissions read. So this tier serves a supervisor that runs as a user unit of the operator's systemd manager, in the operator's namespaces; a supervisor inside the container namespace gets the session-file backing instead (session_cachechooses, by the same fact the OCI hook reads). No bridge through the session keyring: that reader would depend on which login cached the passphrase. - Explicit
keyctl_setperm. A freshuserkey defaults topossessor=all, uid=view— the uid can see the key but not read or search it. systemd gets away without a setperm because its readers possess@uthrough a shared session keyring; our CLI in a different terminal does not possess the supervisor's key, so it would fall to the uid class and be unable to find or read it. We therefore open uidview|read|write|search|setattrand zero the group/other classes — no other user can read it, and any same-uid terminal can read, revoke, or update it. Applying that mask needs the writer to possess the key, sostorefirst links@uinto the session keyring (a headless supervisor / cron / CI has no pam_keyinit possession otherwise, and the setperm would fail EACCES). - No auto-expiry. The cache persists for the whole login session —
until an explicit
vault lock(or a move to a durable tier), just like the tmpfs file it replaces — rather than timing out mid-session. The payload lives in unswappable kernel memory, so it never reaches disk or swap regardless.
Linux-only: on any host without the kernel key facility
(CONFIG_KEYS off, no libkeyutils, WSL1, non-Linux) every entry
point degrades to "unavailable" and the tier simply drops out of the
resolution chain, exactly like systemd-creds on a systemd < 257 box.
KEY_TYPE = b'user'
module-attribute
¶
KEY_DESCRIPTION_PREFIX = b'terok-sandbox:vault-passphrase:'
module-attribute
¶
__all__ = ['KEY_DESCRIPTION_PREFIX', 'KEY_TYPE', 'forget', 'is_cached', 'key_description', 'load', 'store', 'unavailable_reason']
module-attribute
¶
key_description(db_path)
¶
Return the @u key description scoping the cache to one vault on this host.
Anchored on (hostname, absolute credentials-DB path) and hashed to
a fixed-width, ASCII-safe token appended to
KEY_DESCRIPTION_PREFIX:
two vaults on one uid never collide, and a path carrying spaces or
non-UTF-8 bytes can't corrupt the description. The hostname component
separates environments that share one @u but differ by UTS
namespace (concurrent rootless containers with identical in-container
paths); the path component keeps a test's throwaway DB off the
operator's real key even on the same host. abspath (not
realpath) and gethostname keep this pure and stable — the
writer and every same-host reader derive both from the same config and
the same UTS namespace, so they always agree.
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
cache_digest(db_path)
¶
Return the per-vault cache token every backing scopes its entry with.
key_description
composes it into the @u description;
session_file uses it as
the cache file name. One derivation, so the backings always agree.
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
store(passphrase, db_path)
¶
Cache passphrase for db_path so later processes can unlock that vault.
The cache is deliberately untimed: it lives for the login session and
is cleared only by an explicit vault lock or a move to a durable
tier, matching the tmpfs file this tier replaces. Failure is soft —
a cache is never the sole home of the secret — so an unreachable
facility, an exhausted key quota or a refused permission change is
logged and reported rather than raised.
Returns:
| Type | Description |
|---|---|
bool
|
True when the passphrase is cached and readable by this uid. |
Raises:
| Type | Description |
|---|---|
ValueError
|
The passphrase is empty — SQLCipher reads that back as "no encryption" — or implausibly large for a passphrase. |
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
load(db_path)
¶
Return the passphrase cached for db_path.
Silent on every miss: an absent key and an unusable facility are
both the ordinary "locked" outcome, which the next tier of the
resolver chain handles. Reach for
is_cached
when only presence matters — this materialises the secret.
Returns:
| Type | Description |
|---|---|
str | None
|
The cached passphrase, or None when nothing is cached for this vault. |
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
forget(db_path)
¶
Clear the passphrase cached for db_path.
Backs vault lock. An already-absent key counts as success: the
contract is the end state — nothing cached for this vault — not the
act of removing something. A lookup that fails is not that end
state, so it reports failure rather than claim the passphrase is
gone. Any same-uid terminal may call it, not only the one that
cached the passphrase.
The key is anchored in @u and this unlinks it from there, so
clearing the cache is an operator-context operation.
Returns:
| Type | Description |
|---|---|
bool
|
True when no passphrase remains cached for this vault. |
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
is_cached(db_path)
¶
Whether a passphrase is currently cached for db_path.
The presence question every status surface asks — vault status,
the doctor checks, the TUI pill's poll — answered without reading
the payload, so reporting on the secret never materialises it.
Returns:
| Type | Description |
|---|---|
bool
|
True when this vault's key exists in the user keyring. |
Source code in src/terok_sandbox/vault/store/kernel_keyring.py
unavailable_reason()
¶
Explain why this host cannot hold the cache, or None if it can.
The gate the setup chooser and the status surfaces consult before
offering the tier, mirroring
systemd_creds.unavailable_reason
so both tiers are gated alike. A probe, not a guarantee —
store's return
value is the definitive answer — and it neither creates nor reads a
key.
Returns:
| Type | Description |
|---|---|
str | None
|
A human-readable reason the tier is unusable here, or None when |
str | None
|
it is usable. |