← 1.0.0 backlogs ISS-11

A grant has no expiry and cannot be revoked short of rotating the server secret

Type
Bug — a credential with no lifecycle
Found while
SAPE-21
Triage
Fix in 1.0.0
Status
Open
Severity
High — the fix is a compatibility break the day after the tag

Description

A grant is a bearer credential: a list of scopes plus an HMAC over account_id ":" dbname ":" scope[,scope...], signed with a key derived from the server secret under signing.GrantLabel (internal/signing/signing.go:228, 331).

There is nothing else in the message. No expiry, no serial number, no issued-at. Verification (Server.granted, internal/server/server.go:833-841) checks the signature and the account/database binding and nothing more. So a grant is good forever, and the only way to withdraw one is to change s.options.Secret — which is the same secret that signs connection strings, so rotating it invalidates every connection string on the server at the same time.

Evidence

Triage

Does it lose data, break a promise on a public surface, or stop somebody installing and running? Yes — the middle one, and by the second-order reading rather than the first.

Say it straight, because this is the deferral-or-not that most deserves an argument. A grant that never expires does not, by itself, break a promise: the CHANGELOG says up front that it does not expire, so nobody is misled. By the narrow reading of the triage question this is Defer.

It is Fix in 1.0.0 because the surface that has to change is the signed message, and the signed message is the public surface. Adding exp means adding a field to the bytes that go into the HMAC, in the Go signer, in the Go verifier, and in the TypeScript signer, all together. Before the tag that is an edit. After the tag it is a break: every grant minted by 1.0.0 fails verification on 1.1.0, or 1.1.0 has to accept both message shapes forever, which is a permanent branch in a security check — the worst place in a codebase to keep two answers.

There is a second reason, weaker but real: the only revocation available today is rotating a secret that also invalidates every connection string. That is not a revocation mechanism, it is an outage. An operator who needs to withdraw one scope from one account has no proportionate action, which makes scopes hard to recommend for the case they exist for.

If deferred, what it costs to wait