docs: record the rotation seam and the exclusion evidence
Adds doc/rotation.md: what is already in place for optional rotation (RotationHint, Status::Rotated, fingerprint-as-unit-of-work), what it would take, and the four constraints a future executor does not get to relax — chiefly that nanny holding provisioning credentials is the real cost of the feature and must be opt-in per service. Broadens the noise exclusions after a first sweep of a real ~/git produced 1030 working-tree findings, 273 of them from one crate's vendored target/container/cargo/registry. Excluding all of target/ rather than just debug/ and release/, plus vendored dependencies, build output, tool caches and minified assets. .git/config stays readable: a password in a remote URL is a genuine spill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XAqHWfdMAsYu1o36tgeima
This commit is contained in:
@@ -281,7 +281,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn severity_ranks_critical_first() {
|
||||
let mut v = vec![Severity::Low, Severity::Critical, Severity::Medium];
|
||||
let mut v = [Severity::Low, Severity::Critical, Severity::Medium];
|
||||
v.sort_by_key(|s| s.rank());
|
||||
assert_eq!(v[0], Severity::Critical);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Rotation
|
||||
|
||||
**Status: design note. Not implemented.** nanny records and alerts; the operator
|
||||
decides what to do. This document exists so that when optional rotation is
|
||||
built, it lands on a seam that is already in the right place rather than being
|
||||
retrofitted through the middle of the detection path.
|
||||
|
||||
## Why it is not built yet
|
||||
|
||||
The expensive mistake would be to build rotation before the detection is
|
||||
trusted. A tool that revokes a credential on a false positive costs more than
|
||||
one that misses a real leak, because the false positive breaks something that
|
||||
was working and the operator learns to distrust the tool. Detection quality has
|
||||
to be demonstrated on a real corpus first — and the first sweep of a real
|
||||
workstation produced 1538 findings, most of them fixtures in vendored
|
||||
dependencies, which is exactly the state in which automated revocation would
|
||||
have been a disaster.
|
||||
|
||||
So the order is: detect, tune until the findings are believable, then automate
|
||||
the response.
|
||||
|
||||
## What is already in place
|
||||
|
||||
**`RotationHint`**, carried by a rule rather than by a finding, because what you
|
||||
do about a leaked forge token does not depend on which transcript it turned up
|
||||
in:
|
||||
|
||||
```rust
|
||||
pub struct RotationHint {
|
||||
pub service: String, // "anthropic", "gitea", "aws" — the dispatch key
|
||||
pub instructions: String, // what a human does, right now
|
||||
pub docs: Option<String>,
|
||||
pub rotation_suffices: bool, // false ⇒ incident response, not rotation
|
||||
}
|
||||
```
|
||||
|
||||
`rotation_suffices` is the field that will matter most. A leaked API key is
|
||||
undone by revoking it. A leaked private key is not: the key has to be replaced
|
||||
*and* the old public key removed from every trust store it reached, and no
|
||||
amount of automation makes that a one-button operation. Rules where this is
|
||||
`false` must never be handed to an automatic executor.
|
||||
|
||||
**`Status::Rotated`**, distinct from `Status::Acknowledged`. Acknowledged means
|
||||
the operator has seen it and decided; rotated means the leaked value is now
|
||||
worthless. Only the second closes the exposure, and the UI keeps them apart so
|
||||
that a triaged backlog is never mistaken for a clean one. An executor sets
|
||||
`Rotated`; nothing else should.
|
||||
|
||||
**The `Fingerprint`.** Because the same secret in five places shares one
|
||||
fingerprint, a single rotation can resolve every finding carrying it. That is
|
||||
the unit of work an executor should operate on — a secret, not a location.
|
||||
|
||||
## What it would take
|
||||
|
||||
An `Remediator` port in `nanny-core`, alongside `Alerter`, dispatching on
|
||||
`RotationHint::service` to per-service implementations. Sketch:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Remediator: Send + Sync {
|
||||
fn service(&self) -> &str;
|
||||
/// What this would do, without doing it.
|
||||
async fn plan(&self, finding: &Finding) -> Result<RemediationPlan>;
|
||||
/// Do it. Only ever reached after explicit operator confirmation.
|
||||
async fn execute(&self, plan: &RemediationPlan) -> Result<Remediated>;
|
||||
}
|
||||
```
|
||||
|
||||
Four constraints that follow from the rest of the design, and that a future
|
||||
implementation does not get to relax:
|
||||
|
||||
1. **An executor cannot read the leaked value, because nothing stores it.** It
|
||||
therefore cannot revoke "the credential that leaked" by presenting it. It
|
||||
revokes *by identity at the service* — the key with this prefix, the token
|
||||
named for this consumer — which is why `agent-credentials.md` §5's practice
|
||||
of naming issued credentials after their consumer matters here.
|
||||
2. **Rotation needs credentials, and nanny currently has none.** This is the
|
||||
real cost of the feature: nanny becomes a process holding provisioning
|
||||
credentials, which is the thing the patterns-only detection design exists to
|
||||
avoid. The mitigation is the mint-don't-share pattern of
|
||||
`agent-credentials.md` §2 — a narrow provisioning credential per service that
|
||||
can revoke and reissue and nothing else — and it should be opt-in per
|
||||
service, off by default, never a blanket capability.
|
||||
3. **Never automatic.** `plan` then confirm then `execute`. A monitor that
|
||||
revokes credentials on its own judgement is a denial-of-service with good
|
||||
intentions.
|
||||
4. **Still never edits a harness's files.** Rotating makes the leaked copy
|
||||
inert, which is the correct remedy. Rewriting a live session transcript to
|
||||
scrub it risks breaking the harness that is writing it, and leaves the
|
||||
operator believing a copy is gone when backups, the model provider's logs and
|
||||
`file-history/` still hold it.
|
||||
|
||||
## The thing rotation does not fix
|
||||
|
||||
A credential that reached a model provider's servers is gone, whatever happens
|
||||
locally afterwards. Rotation is the only remedy that works, which is the whole
|
||||
argument for detecting the spill in the first place — and for `nanny scan`
|
||||
exiting non-zero, so it can gate something before the transcript is uploaded
|
||||
rather than after.
|
||||
Reference in New Issue
Block a user