Expand description
The one persisted GitHub credential, in a place a boot-time service can read.
07-security.md counts the persisted credential surface and gets to one:
“The product now holds exactly one persisted GitHub credential and one
short-lived sensitive value.” This module is where that one credential
lives. The short-lived one — the encoded JIT configuration — belongs to
crate::process::RestrictiveHandoff and is not stored here or anywhere
else.
§The constraint that decides the whole design
D13 requires the service to start at machine boot.
05-infrastructure.md: “A boot-time service runs outside any user’s login
session and cannot read a per-user keychain on any supported OS: macOS
LaunchAgents start only at login, and Windows Credential Manager vaults are
per-user.” So the default store is machine-scoped, and every per-user
secret facility on all three operating systems is unavailable to it by
construction rather than by preference.
| OS | SecretScope::Machine | SecretScope::User |
|---|---|---|
| Windows | DPAPI machine scope, in a file under %ProgramData% with its own protected DACL | DPAPI user scope, in a file under %LOCALAPPDATA% with its own protected DACL |
| macOS | System Keychain (/Library/Keychains/System.keychain) | the account’s login keychain |
| Linux | 0600 file under /var/lib/runner-manager, plus the systemd credential the service is started with | 0600 file under $XDG_DATA_HOME/runner-manager |
service install --start-at login is the escape hatch
07-security.md promises operators who reject machine-scoped storage:
“Operators who reject this can use service install --start-at login and
keep a user-scoped store, accepting no unattended restart.” Both columns
implement SecretStore, the active one is chosen by
SecretScope::for_start_mode, and ActiveStore is what host show
and service status print so the choice is inspectable rather than
implied.
§The accepted trade-off, implemented honestly
A local administrator or root on this machine can read a machine-scoped
secret. 07-security.md records that as an accepted consequence, on the
grounds that such an account “can already read the runner’s own
credentials and job workspaces”. Nothing here tries to defeat it, and
nothing here pretends to. What is defended is the case that is actually in
the threat model: an ordinary local user who is not an administrator must
not be able to read the stored value, and SecretStore::protection
reports whether that holds, per OS, through the one cross-platform name
crate::process::permissions_summary already defines.
Delete is delete, not erasure. crate::process::RestrictiveHandoff sets
out why no userspace program can promise that the bytes are unrecoverable —
a journal, a snapshot, or an SSD’s wear levelling each keep copies an
overwrite never reaches — and the same disclaimer applies here. The Linux
backend overwrites before unlinking because the value is at rest in
plaintext there and the overwrite is free; that is a best effort and is not
a claim.
§What never holds this value
SQLite, TOML configuration, logs, diagnostics, UI state, and command-line
arguments. The type system carries as much of that as it can: the value
crosses this module’s surface only as a SecretString, which has no
Display and a redacting Debug, and no error in SecretStoreError
carries the value or any part of it. client_id is public by design
(07-security.md: “Public by design; may appear in logs and
documentation”) and is not a secret this store handles.
§Blocking, not async
DPAPI, Security.framework and open(2) are blocking calls that take
microseconds. This runs twice in the life of a process — once at auth login, once at startup — so an async surface would buy nothing and would
oblige every caller to be in a runtime to read a file.
§Where a test may point it, and where it may not
PlatformSecretStore::standard resolves the production location in the
table above. PlatformSecretStore::rooted_at puts the same backend under
a directory the caller names, exactly as
crate::paths::AppPaths::rooted_at does for the four application-data
directories, and for the same two reasons: a test needs a disposable store,
and a service installed against an explicitly configured root has to
reproduce one.
No test in this crate writes to a standard location, and that is a
deliberate constraint rather than an oversight. Two of the six standard
locations need root to create (/var/lib, the System Keychain), and the
other four are the operator’s real store — a suite that wrote there would
destroy a developer’s auth login every time it ran. So the standard
locations are asserted by resolution, and every round trip runs against a
rooted store. What that does and does not cover is written out at
PlatformSecretStore::rooted_at.
Structs§
- Active
Store - Which store is in use, and whether that is the one the configured start mode obliges.
- Platform
Secret Store - The real store: DPAPI on Windows, a keychain on macOS, a
0600file plus systemd credentials on Linux. - Protection
- What actually stands between the stored value and an ordinary local user.
Enums§
- Removal
- What
SecretStore::deletefound. - Secret
Scope - Which of the two stores a value lives in.
- Secret
Store Error - Something went wrong reaching the secret store.
Constants§
- CREDENTIALS_
DIRECTORY - The environment variable
systemdsets for a unit that was given credentials. Read once, atPlatformSecretStore::standardtime. - ROOTED_
KEYCHAIN_ PASSWORD - The password protecting a keychain created by
PlatformSecretStore::rooted_aton macOS. - SYSTEMD_
CREDENTIAL - The name of the systemd credential the Linux machine-scoped store reads before it reads its own file.
Traits§
- Secret
Store - Store, load, delete — and say where the value lives and what protects it.