Add keyring package for kernel-keyring-backed secret sealing #3

Merged
benvin merged 5 commits from benvin/keyring into main 2026-10-23 23:00:46 +11:00
Member

Cached Vault tokens in vault-tools sit in plaintext under ~/.cache/vault. Sealing them with a key that lives only in the kernel keyring for the current boot and uid keeps them useless on disk; a lost key just means logging in again.

  • add keyring package: Open, Seal, Unseal, Forget
  • store a random 32-byte user key in the user keyring, readable by the uid without possession
  • link new keys with KEYCTL_MOVE_EXCL so concurrent first users converge on one key
  • replace a revoked/expired key of ours by unlinking the dead keys in the user keyring
  • seal with AES-256-GCM in a versioned envelope; return ErrKeyUnavailable / ErrCorrupt
  • stub non-Linux builds with errors.ErrUnsupported
  • document the package in the README
Cached Vault tokens in vault-tools sit in plaintext under ~/.cache/vault. Sealing them with a key that lives only in the kernel keyring for the current boot and uid keeps them useless on disk; a lost key just means logging in again. - add `keyring` package: `Open`, `Seal`, `Unseal`, `Forget` - store a random 32-byte `user` key in the user keyring, readable by the uid without possession - link new keys with `KEYCTL_MOVE_EXCL` so concurrent first users converge on one key - replace a revoked/expired key of ours by unlinking the dead keys in the user keyring - seal with AES-256-GCM in a versioned envelope; return `ErrKeyUnavailable` / `ErrCorrupt` - stub non-Linux builds with `errors.ErrUnsupported` - document the package in the README
unkin-agent added 1 commit 2026-10-23 21:56:41 +11:00
Add keyring package for kernel-keyring-backed secret sealing
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
e4604707a6
Author
Member
  • keyring/keyring_linux.go:119 — first-use race: two processes that both miss the key each KEYCTL_LINK their own; LINK displaces, so the one that re-read (L97) before the other linked has already sealed with a dead key (Seal succeeds, blob is permanently ErrKeyUnavailable; L97 comment is wrong). Reproduced: 2/160 blobs unreadable with 8 concurrent first Seals → use KEYCTL_MOVE with KEYCTL_MOVE_EXCL from the thread keyring; on EEXIST re-get() the winner, and if that is stale (revoked/wrong size — still counts as existing) unlink it by id and retry.
  • nit: keyring/keyring_test.go — AAD binding is untested: passing nil AAD in seal/unseal keeps every test green → add a case that sets an unknown version/fingerprint with AAD on and asserts GCM rejects it (or drop the AAD claim from L43).
  • nit: keyring/keyring_linux_test.go:21 — the skip path drops make cover to 77.6% (below COVER_MIN 90), so a seccomp-restricted runner fails as "coverage below minimum" instead of skipping → fail instead of skip when CI is set, or exclude keyring_linux.go from the gate.
  • nit: keyring/keyring.go:35 — desc is unused under GOOS!=linux (GOOS=darwin golangci-lint run ./keyring/ → unused) → move the field/struct into keyring_linux.go and declare an empty Keyring in keyring_other.go.
- keyring/keyring_linux.go:119 — first-use race: two processes that both miss the key each `KEYCTL_LINK` their own; LINK displaces, so the one that re-read (L97) before the other linked has already sealed with a dead key (Seal succeeds, blob is permanently `ErrKeyUnavailable`; L97 comment is wrong). Reproduced: 2/160 blobs unreadable with 8 concurrent first Seals → use `KEYCTL_MOVE` with `KEYCTL_MOVE_EXCL` from the thread keyring; on `EEXIST` re-`get()` the winner, and if that is stale (revoked/wrong size — still counts as existing) unlink it by id and retry. - nit: keyring/keyring_test.go — AAD binding is untested: passing `nil` AAD in `seal`/`unseal` keeps every test green → add a case that sets an unknown version/fingerprint with AAD on and asserts GCM rejects it (or drop the AAD claim from L43). - nit: keyring/keyring_linux_test.go:21 — the skip path drops `make cover` to 77.6% (below COVER_MIN 90), so a seccomp-restricted runner fails as "coverage below minimum" instead of skipping → fail instead of skip when `CI` is set, or exclude keyring_linux.go from the gate. - nit: keyring/keyring.go:35 — `desc` is unused under `GOOS!=linux` (`GOOS=darwin golangci-lint run ./keyring/` → `unused`) → move the field/struct into keyring_linux.go and declare an empty `Keyring` in keyring_other.go.
unkin-agent added 1 commit 2026-10-23 22:24:41 +11:00
Link keyring keys exclusively so concurrent first users share one key
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
b636617f1b
Author
Member
  • Makefile:13 — |/keyring/keyring_linux.go exclusion rests on a false premise: CI test step (pipeline 7) reports keyring coverage: 84.3%, identical to a keyctl-enabled host, so the tests do not skip there; the gate passes at 91.5% with the file included → drop the exclusion and the comment at Makefile:11-12.
  • keyring/keyring_linux.go:113 — purgeDead() runs on plain ENOKEY (every first use per boot) and unlinks every revoked/expired key in @u, including other apps' keys the caller cannot even view (verified: 3/3 foreign dead keys removed, their search flips EKEYREVOKED→ENOKEY) → call it only when the search error is EKEYREVOKED/EKEYEXPIRED, the only case where a dead key of our description blocks MOVE_EXCL.
  • nit: keyring/keyring_linux_test.go:117 — sealed, _ := k.Seal(...) drops the error → check it.
- Makefile:13 — `|/keyring/keyring_linux.go` exclusion rests on a false premise: CI test step (pipeline 7) reports `keyring coverage: 84.3%`, identical to a keyctl-enabled host, so the tests do not skip there; the gate passes at 91.5% with the file included → drop the exclusion and the comment at Makefile:11-12. - keyring/keyring_linux.go:113 — `purgeDead()` runs on plain ENOKEY (every first use per boot) and unlinks every revoked/expired key in `@u`, including other apps' keys the caller cannot even view (verified: 3/3 foreign dead keys removed, their search flips EKEYREVOKED→ENOKEY) → call it only when the search error is EKEYREVOKED/EKEYEXPIRED, the only case where a dead key of our description blocks MOVE_EXCL. - nit: keyring/keyring_linux_test.go:117 — `sealed, _ := k.Seal(...)` drops the error → check it.
unkin-agent added 1 commit 2026-10-23 22:31:02 +11:00
Purge dead keys only when ours is dead; gate keyring_linux.go coverage
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
319ae21bc5
Author
Member
  • .woodpecker/test.yaml:79 — ci/woodpecker/pr/test fails on 319ae21 (make cover exit 2); with keyctl blocked the Kernel tests skip and total coverage is 71.6% < 90 → let the test step run keyctl (unconfined seccomp) so the keyring tests execute in CI; do not restore the exclusion
  • keyring/keyring_linux_test.go:21 — skipping on EPERM/EACCES returned by Open hides permission bugs: dropping the possessor link bit from keyPerm makes all 8 Kernel tests SKIP and the package pass → decide the skip with a raw keyctl probe (e.g. KEYCTL_GET_KEYRING_ID on @u), then t.Fatal on any Open error
  • nit: PR body — "leaving other apps' keys alone" is false when our key is dead: purgeDead (keyring_linux.go:113) also unlinks other apps' revoked/expired keys → reword the bullet
- .woodpecker/test.yaml:79 — `ci/woodpecker/pr/test` fails on 319ae21 (`make cover` exit 2); with keyctl blocked the Kernel tests skip and total coverage is 71.6% < 90 → let the test step run keyctl (unconfined seccomp) so the keyring tests execute in CI; do not restore the exclusion - keyring/keyring_linux_test.go:21 — skipping on EPERM/EACCES returned by `Open` hides permission bugs: dropping the possessor link bit from `keyPerm` makes all 8 Kernel tests SKIP and the package pass → decide the skip with a raw keyctl probe (e.g. `KEYCTL_GET_KEYRING_ID` on `@u`), then `t.Fatal` on any `Open` error - nit: PR body — "leaving other apps' keys alone" is false when our key is dead: `purgeDead` (keyring_linux.go:113) also unlinks other apps' revoked/expired keys → reword the bullet
unkin-agent added 1 commit 2026-10-23 22:46:07 +11:00
Fail kernel keyring tests on permission errors; possess the foreign test key
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
581993fe83
unkin-agent added 1 commit 2026-10-23 22:50:23 +11:00
Merge main
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
c38ed389ee
Author
Member

No findings.

No findings.
benvin merged commit 27f934d2ae into main 2026-10-23 23:00:46 +11:00
benvin deleted branch benvin/keyring 2026-10-23 23:00:46 +11:00
Sign in to join this conversation.