A file can come back out, and a script cannot come back forever
Two halves of the same evening.
Uploads were one-way. A `type: file` field put an object in the bucket
and wrote its key into the record, and nothing ever read it back - a
desk showed `0aba5734-...-drawing.pdf` as a string with no link, so a
builder could receive drawings and never open one.
`GET /attachment/{bucket}/{record}/{field}` streams it. The key is not
the credential and is not taken from the caller: it is read out of the
record named, so a file is reachable only through a record somebody is
allowed to see, and a key that leaks off a desk is a link to nothing.
And there was no rate limiting anywhere. A burst of 30 per caller IP,
refilled one every two seconds, outside the session layer so anyone
over it is turned away before a handler is entered. Well above a
person filling in a form - a page load fetches several resources at
once - and well below being a nuisance. PORTAL_RATE_BURST and
PORTAL_RATE_SECONDS retune it; either 0 turns it off for an instance
behind something that already does this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8d6f29011c
commit
d5de25e8c6
@@ -75,6 +75,52 @@ values.
|
||||
| `AUTOMATION_READ_TOKEN` | the automation KV read endpoint |
|
||||
| `GARAGE_*` | uploads |
|
||||
| `MAPBOX_TOKEN` | map tiles, proxied; unset, maps are off |
|
||||
| `GARAGE_*` (4) | attachments; see below. Unset, upload fields fail closed |
|
||||
| `PORTAL_RATE_BURST`, `PORTAL_RATE_SECONDS` | the rate limit; see below |
|
||||
|
||||
Mail is delivered by [gdo](https://project.uhhm.no/uhhm/gdo), a
|
||||
separate unit on the same host reading the same NATS.
|
||||
|
||||
## Rate limiting
|
||||
|
||||
Every request passes a per-caller-IP limit before a session is loaded
|
||||
or a handler is entered: a burst of 30, refilled one every two
|
||||
seconds. A visitor filling in a form never reaches it (a page load
|
||||
fetches several resources at once); a script does.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `PORTAL_RATE_BURST` | how many requests at once, default 30 |
|
||||
| `PORTAL_RATE_SECONDS` | seconds per refilled request, default 2 |
|
||||
|
||||
Either set to `0` turns it off, for an instance behind something that
|
||||
already does this. The caller is read from `X-Forwarded-For` or
|
||||
`X-Real-IP`, falling back to the socket — all of these instances are
|
||||
behind Caddy.
|
||||
|
||||
This is a fence, not an accounting system. What keeps uploads honest
|
||||
is the policy check on the route itself.
|
||||
|
||||
## Attachments
|
||||
|
||||
A `type: file` field stores an object key in the record. The file
|
||||
comes back out at:
|
||||
|
||||
GET /attachment/{bucket}/{record}/{field}
|
||||
|
||||
which reads the key out of that record rather than taking one from the
|
||||
caller, so a file can only be fetched through a record somebody is
|
||||
allowed to see, and a leaked key is a link to nothing. Reading it
|
||||
needs the same policy permission as reading the bucket on a desk.
|
||||
|
||||
Uploads need a bucket and a key in the Garage on the host:
|
||||
|
||||
```sh
|
||||
garage bucket create <site>-attachments
|
||||
garage key create <site>-portal-uploads
|
||||
garage bucket allow --read --write <site>-attachments --key <site>-portal-uploads
|
||||
```
|
||||
|
||||
then `GARAGE_S3_ENDPOINT`, `GARAGE_ACCESS_KEY`, `GARAGE_SECRET_KEY` and
|
||||
`GARAGE_UPLOADS_BUCKET` in that instance's env. Unset, an upload field
|
||||
fails closed rather than appearing to work and storing nowhere.
|
||||
|
||||
Reference in New Issue
Block a user