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.

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.