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
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
# Every five minutes. Two things set the floor and the ceiling:
|
||||
# Hourly, because this is now the BACKSTOP rather than the primary path.
|
||||
#
|
||||
# * bunq rate-limits reads to roughly 3 GET per 3 seconds per method, so one
|
||||
# call per five minutes is nowhere near it. The cost of going faster is
|
||||
# nothing technical; it is just noise.
|
||||
# * a donor watching the order page wants confirmation while they still care.
|
||||
# Five minutes here plus the reconciler's own 60 s cadence means a fresh
|
||||
# order confirms within about six minutes of the money landing.
|
||||
# Since the bunq webhook landed, a payment normally settles within seconds: bunq
|
||||
# pushes it, the handler writes the credit, the reconciler picks it up. This
|
||||
# timer exists for the case the webhook cannot cover — bunq retries a callback
|
||||
# roughly six times over a few minutes and then DROPS IT FOREVER, so a payment
|
||||
# that arrives while the backend is restarting (every deploy is a restart) is
|
||||
# lost to the webhook with no error anywhere. An hourly pull turns that from a
|
||||
# silently missing donation into an hour of delay.
|
||||
#
|
||||
# It was five minutes while the pull WAS the settlement path. Hourly is the
|
||||
# right cadence for a safety net, and it keeps the noise down.
|
||||
#
|
||||
# Persistent=true so a laptop that was asleep pulls once on waking instead of
|
||||
# silently skipping every window it missed — which is exactly when a donation
|
||||
# would otherwise sit unacknowledged overnight.
|
||||
# silently skipping every window it missed.
|
||||
[Unit]
|
||||
Description=Pull bunq credits for catcrafts.net every 5 minutes
|
||||
Description=Backstop pull of bunq credits for catcrafts.net (hourly)
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
OnUnitActiveSec=1h
|
||||
Persistent=true
|
||||
AccuracySec=30s
|
||||
AccuracySec=5min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
|
|
|||
Loading…
Reference in a new issue