Skip to content

feat(secrets): wire the secretspec provider hard-delete (RIG-3436) - #1066

Open
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-server/rig-3436-provider-hard-delete
Open

feat(secrets): wire the secretspec provider hard-delete (RIG-3436)#1066
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-server/rig-3436-provider-hard-delete

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

SpecResolver.Delete was a validate-only no-op, documented as an upstream
gap. The secretspec delete verb has existed since 0.18 and the pin is
0.20.0, so an operator delete left the provider value in the backing
store — a data-retention gap, not a correctness bug, since an undeclared
name never resolves.

Delete now shells the real verb, mirroring the Set path: a manifest
declaring just this name, argv from a pure deleteArgs builder, stderr
captured, and an error wrapping name/cli/stderr but never a value. The
audit reason is threaded through as Set already threads it.

Arming that method arms it for BOTH callers, so both doors needed work.

The F1 guard now covers each delete door. HasServerSecretPrefix was
enforced only at store.DeclareSecret, which SetSecret reaches via its
declare step — but neither delete path declares. On the user door a
reserved-prefix name reached the resolver unguarded, so a user deleting
GATEWAY_CREDENTIALS_MASTER_KEY would have destroyed the master key and
stranded every encrypted credential row. On the admin door the inverse
held: no prefix check and no declaration check, so an admin could delete
ANY name from the shared provider keyspace — including an unprefixed one
the user keyspace owns, leaving that user's required=true declaration
pointing at nothing and poisoning every live session's resolve. Both
doors now check before any store or provider call.

The reject side is case-INSENSITIVE via a new ShadowsServerSecretPrefix.
The name grammar admits lowercase, and a provider keyspace's case
sensitivity is provider-dependent, so on a case-insensitive provider
gateway_credentials_master_key reaches the real master key's value. The
byte-exact HasServerSecretPrefix stays the ADMIT check, because the
server_secrets CHECK behind it is itself case-sensitive — admitting a
variant would pass the Go door and fail in Postgres. Reject wide, admit
narrow.

Both doors now delete the declaration FIRST. The declaration is what
Resolve reads: on the user path an orphan poisons every live session's
FetchSecrets, and on the server path it fails the forge lanes at request
time and hard-fails the next boot at validateForgeSecret. A provider
failure after the row is gone is CodeUnavailable and deliberately not
rolled back — re-creating the row would point a required=true entry at a
value that may already be gone. The user-facing error now names the
recovery, since re-running the delete returns NotFound and never
re-attempts the provider.

Red-green verified against the actual prior logic, not a hypothetical.
Reverting the admin guard reds both new admin tests, one with server resolver deleted [SERVER_NEVER_DECLARED] for a never-declared name — the
destructive call on an undeclared name. Reverting the case-fold reds all
three case variants. Reverting the user guard or the ordering reds their
own tests.

Refs RIG-3436

Co-authored-by: Matt Wilkinson matt@rigel.build

@linear-code

linear-code Bot commented Sep 10, 2026

Copy link
Copy Markdown

RIG-3436

@trunk-io

trunk-io Bot commented Sep 10, 2026

Copy link
Copy Markdown

❌ This pull request could not start testing because there was a merge conflict. See more details here.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-3436-prov.compass-eng-docs.pages.dev

Deployed from compass-server/rig-3436-provider-hard-delete at 0a1fefe.

`SpecResolver.Delete` was a validate-only no-op, documented as an upstream
gap. The `secretspec delete` verb has existed since 0.18 and the pin is
0.20.0, so an operator delete left the provider value in the backing
store — a data-retention gap, not a correctness bug, since an undeclared
name never resolves.

Delete now shells the real verb, mirroring the Set path: a manifest
declaring just this name, argv from a pure `deleteArgs` builder, stderr
captured, and an error wrapping name/cli/stderr but never a value. The
audit `reason` is threaded through as Set already threads it.

Arming that method arms it for BOTH callers, so both doors needed work.

The F1 guard now covers each delete door. `HasServerSecretPrefix` was
enforced only at `store.DeclareSecret`, which `SetSecret` reaches via its
declare step — but neither delete path declares. On the user door a
reserved-prefix name reached the resolver unguarded, so a user deleting
`GATEWAY_CREDENTIALS_MASTER_KEY` would have destroyed the master key and
stranded every encrypted credential row. On the admin door the inverse
held: no prefix check and no declaration check, so an admin could delete
ANY name from the shared provider keyspace — including an unprefixed one
the user keyspace owns, leaving that user's required=true declaration
pointing at nothing and poisoning every live session's resolve. Both
doors now check before any store or provider call.

The reject side is case-INSENSITIVE via a new `ShadowsServerSecretPrefix`.
The name grammar admits lowercase, and a provider keyspace's case
sensitivity is provider-dependent, so on a case-insensitive provider
`gateway_credentials_master_key` reaches the real master key's value. The
byte-exact `HasServerSecretPrefix` stays the ADMIT check, because the
`server_secrets` CHECK behind it is itself case-sensitive — admitting a
variant would pass the Go door and fail in Postgres. Reject wide, admit
narrow.

Both doors now delete the declaration FIRST. The declaration is what
Resolve reads: on the user path an orphan poisons every live session's
FetchSecrets, and on the server path it fails the forge lanes at request
time and hard-fails the next boot at `validateForgeSecret`. A provider
failure after the row is gone is CodeUnavailable and deliberately not
rolled back — re-creating the row would point a required=true entry at a
value that may already be gone. The user-facing error now names the
recovery, since re-running the delete returns NotFound and never
re-attempts the provider.

Red-green verified against the actual prior logic, not a hypothetical.
Reverting the admin guard reds both new admin tests, one with `server
resolver deleted [SERVER_NEVER_DECLARED] for a never-declared name` — the
destructive call on an undeclared name. Reverting the case-fold reds all
three case variants. Reverting the user guard or the ordering reds their
own tests.

Refs RIG-3436

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-3436-provider-hard-delete branch from f1eb56f to 0a1fefe Compare September 11, 2026 02:27
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.

2 participants