This commit is contained in:
parent
aaa7a8ce99
commit
413740af2e
4 changed files with 134 additions and 13 deletions
|
|
@ -351,6 +351,46 @@ BUNQ_API_KEY=… TRANSFER_IBAN=NL.. \
|
|||
# -> pulled 2 new credit(s) into /var/lib/catcrafts/orders.jsonl.transfer-credits.jsonl
|
||||
```
|
||||
|
||||
#### How settlement actually reaches the shop
|
||||
|
||||
Two paths, and the split is deliberate.
|
||||
|
||||
**Primary: the bunq webhook.** bunq pushes a `MUTATION` notification when money
|
||||
lands; the backend decodes the payment and appends it to the credits file, and
|
||||
the reconciler settles from there. Normally seconds end to end. Registered with
|
||||
`tools/register-bunq-webhook.sh` from a machine bunq's key permits (`--show`
|
||||
lists what is registered, with the secret redacted). **That call REPLACES the
|
||||
whole filter set**, so look before overwriting.
|
||||
|
||||
**bunq does not sign these notifications** — verified against doc.bunq.com: no
|
||||
HMAC, no server signature, and certificate pinning authenticates us to bunq
|
||||
rather than bunq to us. So the endpoint's authentication is three transport-level
|
||||
controls, all of which have to stay in place:
|
||||
|
||||
1. **Source-IP allowlist** for `185.40.108.0/22` in the Caddyfile. This is the
|
||||
strong one: forging requires completing a TLS handshake from inside that
|
||||
range. bunq warns the range may change; if it does, callbacks 403 and
|
||||
settlement stops — it fails closed.
|
||||
2. **A secret path.** Caddy matches only the non-secret prefix `/hooks/bunq/*`,
|
||||
so the secret lives solely in `BUNQ_WEBHOOK_PATH` in `payments.env` and not
|
||||
in the web server config. The backend compares it in constant time, and a
|
||||
non-POST to it falls through to an ordinary 404 rather than a distinctive
|
||||
405 — a different answer there would confirm a guessed path.
|
||||
3. **`log_skip` on that prefix.** Without it the secret would be written to the
|
||||
access log and then ingested into the GoAccess database and shown in its
|
||||
top-URLs panel, i.e. leaked to anyone who opens `/analytics/`.
|
||||
|
||||
The handler deliberately does the least it can: decode one payment, append it,
|
||||
answer 200. It makes no settlement decision, so a forged callback can at worst
|
||||
manufacture a credit line — it cannot bypass the reference match or the
|
||||
covering-amount rule, and nothing ships without a human either way.
|
||||
|
||||
**Backstop: the hourly pull.** Not optional. bunq retries a callback about six
|
||||
times and then drops it permanently, so a payment arriving while the backend
|
||||
restarts — every deploy is a restart — is lost to the webhook with no error
|
||||
anywhere. `deploy/catcrafts-credits.timer` runs `pull-and-ship-credits.sh`
|
||||
hourly to turn that from a missing donation into an hour of delay.
|
||||
|
||||
**The key is IP-restricted, and that decides the shape.** Verified the hard way
|
||||
on 2026-08-20: the bunq API key permits one address (the owner's home
|
||||
connection), and **bunq enforces that on every request, not only at device
|
||||
|
|
|
|||
Loading…
Reference in a new issue