Skip to content

feat(s135): secrets out of the data directory, and filesystem roots that fail closed - #128

Merged
HackTuah merged 5 commits into
mainfrom
slice/135-secrets-out-roots-fail-closed
Oct 8, 2026
Merged

HackTuah merged 5 commits into
mainfrom
slice/135-secrets-out-roots-fail-closed

Conversation

@HackTuah

@HackTuah HackTuah commented Oct 8, 2026

Copy link
Copy Markdown
Member

Slice 135, opened on the owner's 2026-10-08 answer sheet (S1).

The defect, reproduced on main: with the default policy (read: :allow), fs_read of a key file under the data directory returned its contents with no approval, because the data directory was always a filesystem root (since slice 022) and the receipt key lived under it. network: :allow meant a fetch could carry it out. The same reproduction on this branch returns {:error, {:fs_denied, ...}}.

What changes

  • Secrets leave the data directory. TRINITY_SECRETS_DIR, with a per-OS default outside it (Linux ~/.config/trinity/secrets, macOS ~/Library/Application Support/Trinity Secrets, Windows %LOCALAPPDATA%\Trinity\Secrets). The boot moves existing keys, the MCP bearer token and the OAuth store there, never overwriting, and names the move on the boot receipt. Keys keep their ids, and earlier receipts verify. The headless Containerfile sets TRINITY_SECRETS_DIR=/data/secrets.
  • Trinity.Tools.FS.Guard judges every path a tool is given. It refuses /proc and /dev, a symlink anywhere in the path, anything under the data or secrets directory, special files and hard links. A boot-time (dev, inode) inventory catches hard links and bind mounts, and the opened descriptor is fstated again. A deny-list runs last. A guard refusal is a hard deny that no approval lifts.
  • A filesystem root equal to, above or below either directory refuses boot, in every profile; the Settings page refuses such a folder.
  • web_fetch asks once a session has read outside the roots. :regulated refuses network: :allow without an egress allow-list (TRINITY_REGULATED_EGRESS).
  • Every fs decision, denials included, is receipted with the requested and canonical path, inode, root, rule and turn id.

Proof: AC1 to AC6 automatic, each shown red first (on main every route returned the key's bytes, and the census found 220 breaches). AC7, a bind mount on the hardened image, is manual. The full suite passes on SQLite (1080) and Postgres. The changed tests (79), format, credo and plan_check were re-run in review.

Known before merge

…ots that fail closed)

A tool the agent calls could read Trinity's own keys: the data directory was always a
filesystem root, the receipt key lived under it, and the default policy allows reads. With the
default policy, fs_read of <data dir>/keys/receipts-ed25519.key returned the key.

- Secrets leave the data directory for TRINITY_SECRETS_DIR (a per-OS default outside it); the
  boot moves existing keys, the MCP bearer token and the OAuth store there, never overwriting,
  and names the move on the boot receipt; keys keep their ids and earlier receipts verify.
- Trinity.Tools.FS.Guard judges every path a tool is given: /proc and /dev, any symlink, any
  path under the data or secrets directory, special files and hard links are refused, and a
  boot-time (dev, inode) inventory catches hard links and bind mounts; the opened descriptor is
  checked again. A short deny-list runs last. A guard refusal is a hard deny no approval lifts.
- A root equal to, above or below either directory refuses boot in every profile.
- web_fetch asks once a session has read outside the roots; :regulated refuses network: :allow
  without an egress allow-list.
- Every fs decision, denials included, is receipted with the canonical path and inode.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>

# Conflicts:
#	CHANGELOG.md
#	ci/headless/Containerfile
Signed-off-by: Ayla Croft <aylacroft@proton.me>

# Conflicts:
#	CHANGELOG.md
…s refusal; SC-12 says where keys live now

Slice 132's mapping, merged to main, requires every census and every regulated refusal to be cited by a row. Slice 135 adds one of each (test/trinity/tools/fs/secrets_census_test.exs, Trinity.Profile.check_egress/3), so the merge failed test/control_mapping_test.exs with both named. Two rows and one responsibility-matrix row cite them, and the SC-12 row no longer says key material lives in the data directory.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…nstead of naming ed25519

On the FIPS leg the receipt key is P-384 (KeyCustody.select/0), so receipts-ed25519.key never exists there and AC4's boot test failed both fips runs on 7e0dab7. The first boot now prints the signer's own key path and the test asserts on that.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 2f49d6e into main Oct 8, 2026
12 checks passed
@HackTuah
HackTuah deleted the slice/135-secrets-out-roots-fail-closed branch October 8, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant