Canary tokens
Decoy secrets that deny outbound calls at 9.5 - and the persistence gap you have to work around to register one.
A canary is a fake secret that has no legitimate use. Nothing should ever read it, and nothing should ever send it anywhere. If one appears in an outbound call, that is not a risk estimate - it is proof that something is exfiltrating.
grith scores a canary match at 9.5, deliberately above the 8.0 deny line, so it always denies without a prompt.
⚠️Read this before you plan around canaries
The canary registry is in-memory. grith canary add does not persist, and the CLI does
not even reach the running daemon - it mutates its own throwaway registry and exits. The
only durable way to register a canary on v0.3.1 is to edit
config/filters/canary.toml and restart the daemon.
The persistence gap, reproduced
$ grith canary add --label probe-trap --generate
Added canary:
id: 0185dce4-df83-4a6c-bd42-004fb56cef7f
label: probe-trap
value: e8f2af1abcfc46e2b7be38e4e35c84f51d76f3f958c4f855$ grith canary list
No canary tokens registered.That is the real behaviour, not a bug in the example. grith canary is not one of the
thin-client commands, so nothing it does is visible to the daemon that actually evaluates
calls. A canary added through the REST API does reach the live daemon's registry, and is
lost when the daemon restarts.
Registering one that survives
Put it in the canary filter's own config file and restart the daemon.
config/filters/canary.toml
[canary]
enabled = true
[[canary.tokens]]
id = "11111111-1111-1111-1111-111111111111"
label = "aws-prod-decoy"
value = "AKIAI44QH8DHBEXAMPLE"Then plant the value somewhere an attacker would look and nothing legitimate reads: a
plausible-looking ~/.aws/credentials profile, a stale .env.production, an entry in a
config file the project no longer uses. grith never plants anything for you. Placement
is entirely yours, and it is the part that decides whether the trap is any good.
Where detection actually looks
The filter scans the extracted text and the JSON arguments of a call, but only for call types that can carry data off the machine:
| Call type | Scanned |
|---|---|
HttpRequest | Method and URL |
NetConnect | Address and port |
ShellExec, ProcessSpawn | Command and arguments |
FileWrite, FileAppend | Path only - a canary in the filename |
Every other call type returns no match immediately. In particular FileRead is not
scanned, so reading a canary file is not a canary detection. The read may well be
scored by other filters, and a canary living under ~/.ssh or ~/.aws will be caught by
path matching, but the canary filter itself fires only on outbound calls.
That follows directly from grith not inspecting content: a canary is matched in the arguments of an outbound call, not in a payload body it never sees.
What happens on a match
Score 9.5, severity critical, rule canary-secret-detected, and a message naming the
label and id. The composite is past 8.0, so the call is denied with no prompt, and the
audit record carries the full breakdown.
What does not happen: there is no canary-specific alert, no automatic rotation, and no "which canary fired" view beyond the message in the filter breakdown and the audit record. Detection is wired end to end; management and alerting are not.
Generation
--generate produces 24 random bytes as 48 hex characters, built from a UUID plus
XOR-shifted fill. It is not a cryptographic secret and does not need to be - a canary's
job is to be unique and unmistakable, not unguessable. Nothing legitimate knows the value,
so nothing legitimate can send it.