Security
How a post to one of your agent's webhooks is checked (Webhook Plugin), beyond what every webhook's secret gets (Link's Credentials page). What your agent's bridge does around it, the address on 127.0.0.1 and the ids it keeps, is in Security of agents.
A post
Each post is checked in this order, and refused at the first check it fails.
Scheme
A webhook takes its own scheme only, and sift checks it through
Link's one way in (link_secret_value::inbound), each signature
compared in constant time.
Link's
A webhook added without --sender takes Link's: an HMAC-SHA256 with the
webhook's secret over link webhook, the time (link-time), the
post's id (link-id) and the body, each after a NUL byte, sent as
link-signature: v1=<hex> (Scheme::LINK).
GitHub's
A webhook added --sender github takes GitHub's: an HMAC-SHA256 over the
body alone, sent as x-hub-signature-256: sha256=<hex>
(Scheme::GITHUB).
Cap
A post is at most 1 MiB (UPDATE_MAX). The bridge answers 413 to a body
over it, before reading it whole, so sift is never handed one: sift
refuses a line over the cap as one it can't read, 400, and checks the
cap again itself.
Its type
A post signed right must carry JSON (application/json, or a +json
type, whose body is JSON) or text (text/*), and is answered 415 for
anything else, a form among them. Its type is looked at only once the
signature holds.
Window
Link's scheme takes a post within 5 minutes of its link-time, either
side. GitHub signs no time, so its posts have none: their id stops a
repeat.
Keep
A post's dedupe is the id its scheme signs, never one it doesn't:
Link's link-id, or the SHA-256 of GitHub's body. GitHub's
x-github-delivery isn't signed, so a captured body sent again under a
new one is the same post, refused. The bridge keeps the id 30 days once
your agent has the post, and refuses it again within them; sift keeps
nothing.
Answers
sift answers 400 without the time or the id Link's scheme signs, or
for JSON that isn't; 401 without a signature, with a wrong one, or
outside the window; 413 and 415 as above; and 503 for a webhook with no
secret, or that the bridge describes wrongly, its sender among what it
must say, closed, so its sender tries again.
Rolling its secret
lnk agent webhook rotate <name> makes a new secret, printed once. For
24 hours the bridge hands sift the previous one beside it
(previous), and a post signed with either is taken, so its sender
switches whenever it likes.
Dropping the previous one
roll --done drops the previous secret at once, and from then a post
signed with it is refused. A previous secret alone takes nothing. No
restart: each post is checked with the secrets it comes with.
What it holds
sift is handed one webhook's secrets with each post, on its stdin,
never on its command line or in its environment, and keeps none. It
reads nothing else, opens no connection, and never holds your agent's
endpoint or its key: it can refuse a post, never make one yours, since
the bridge says who a post it took is from.