Author SHA1 Message Date
clawbot cb8c885061 Correct trusted_proxies advice and state signature padding (closes #150)
check / check (push) Successful in 3m46s
The README told operators to set trusted_proxies to the proxy's own
address. A proxy on the Docker host reaches pixa from the Docker
network's gateway, so that advice made pixa count every user as one
client for the login limit. The login-limit paragraph, the
trusted_proxies entry and config.example.yml now say to use the address
pixa sees for requests through the proxy, and how to read it from the
request log.

The signature section now says sig is base64url with the = padding
kept, since pixa compares it exactly, and shows the example's sig for a
stated key, computed with pixa's signer.

Model: opus-5-5
2026-09-29 02:50:45 +00:00
3 changed files with 49 additions and 17 deletions
+35 -16
View File
@@ -112,13 +112,18 @@ copy is fresh.
The login form (`POST /`) is limited to 5 attempts per minute per client The login form (`POST /`) is limited to 5 attempts per minute per client
address, counting an IPv6 client by its /64; an attempt over the limit is address, counting an IPv6 client by its /64; an attempt over the limit is
refused with 429 and a `Retry-After` header. Behind a reverse proxy the client refused with 429 and a `Retry-After` header. Behind a reverse proxy the client
address comes from `X-Forwarded-For` only when the proxy's address is in address comes from `X-Forwarded-For` only when the address pixa sees for
`trusted_proxies`; otherwise all users behind the proxy are counted as one requests that come through the proxy is in `trusted_proxies`; otherwise all
client. With the default `trusted_proxies` (the RFC 1918 ranges), a client users behind the proxy are counted as one client. That address is not always
with a private address can choose the address it is counted by through its own the proxy's own: for a proxy on the Docker host it is the gateway of the
`X-Forwarded-For`, whether it connects directly or through the proxy, because container's Docker network, such as `172.17.0.1` on the default bridge. pixa's
its own address is trusted too. Setting `trusted_proxies` to the proxy's own request log shows it as `remoteIP` while it is not in `trusted_proxies` (see
address closes this. `trusted_proxies` under Configuration). With the default `trusted_proxies`
(the RFC 1918 ranges), a client with a private address can choose the address
it is counted by through its own `X-Forwarded-For`, whether it connects
directly or through the proxy, because its own address is trusted too. Setting
`trusted_proxies` to only the address pixa sees for requests that come through
the proxy closes this.
### Image Metadata ### Image Metadata
@@ -172,19 +177,27 @@ Where:
outside), or `cover` when the URL has no `fit`; a request whose `fit` is outside), or `cover` when the URL has no `fit`; a request whose `fit` is
anything else, an empty `fit=` included, is refused with 400 anything else, an empty `fit=` included, is refused with 400
**Example:** resize `https://cdn.example.com/photos/cat.jpg` to 800x600 The URL's `sig` is the HMAC-SHA256 result in base64url (the URL-safe alphabet
WebP with expiration 1704067200, default quality and fit: of RFC 4648) with the trailing `=` padding kept, 44 characters in all. pixa
compares it exactly, so a signature encoded without padding, as Node's
`base64url` and Go's `base64.RawURLEncoding` do, is refused with 401.
**Example:** with the signing key `example-signing-key-for-documentation`,
resize `https://cdn.example.com/photos/cat.jpg` to 800x600 WebP with
expiration 1704067200, default quality and fit:
1. Build input: 1. Build input:
`cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover` `cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover`
2. Compute HMAC-SHA256 with your secret key 2. Compute HMAC-SHA256 of it with the signing key
3. Base64URL-encode the result 3. Base64URL-encode the result, keeping the `=` padding:
`-ay7KHpfqmtIGbibDGbUuBDkymi-Ymdn0NkC6j5EJag=`
4. URL: 4. URL:
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200` `/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=-ay7KHpfqmtIGbibDGbUuBDkymi-Ymdn0NkC6j5EJag=&exp=1704067200`
For the same image at quality 40 with fit `contain`, the input ends in For the same image at quality 40 with fit `contain`, the input ends in
`:40:contain` and the URL is `:40:contain`, the signature is `5IwXUx6vf7yefhaUvFzgXZvG2o0Df4RJxPTK3pKq5VU=`,
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200&q=40&fit=contain`. and the URL is
`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=5IwXUx6vf7yefhaUvFzgXZvG2o0Df4RJxPTK3pKq5VU=&exp=1704067200&q=40&fit=contain`.
**Allowlist patterns:** **Allowlist patterns:**
@@ -246,8 +259,14 @@ Key settings in more detail:
`172.16.0.0/12`, `192.168.0.0/16`), since pixa is deployed behind a `172.16.0.0/12`, `192.168.0.0/16`), since pixa is deployed behind a
proxy on a private network; an explicitly empty list (`[]`) trusts no proxy on a private network; an explicitly empty list (`[]`) trusts no
one, and an explicit list replaces the default. An invalid CIDR aborts one, and an explicit list replaces the default. An invalid CIDR aborts
startup. Set this to your proxy's address range if it is not already startup. Set this to the address pixa sees for requests that come through
covered by the defaults your proxy, such as `172.17.0.1/32`, when the defaults do not cover it, or
to trust nothing else (see the login limit under Routes). For a proxy on
the Docker host that address is the gateway of the container's Docker
network (`172.17.0.1` on the default bridge), not the proxy's own address.
To find it, set this to `[]` (or `PIXA_TRUSTED_PROXIES` to empty), send a
request through the proxy, and read `remoteIP` in pixa's request log line
for it
- `upstream_fetch_timeout` — timeout for origin requests - `upstream_fetch_timeout` — timeout for origin requests
- `upstream_max_response_size` — max origin response size - `upstream_max_response_size` — max origin response size
- `downstream_timeout` — client response timeout - `downstream_timeout` — client response timeout
+8
View File
@@ -30,6 +30,14 @@ exhaustion
# Completed Steps # Completed Steps
- 2026-09-29 `trusted_proxies` advice and signature padding in `README.md`
(closes #150): the login-limit paragraph, the `trusted_proxies` entry and
`config.example.yml` say to set `trusted_proxies` to the address pixa sees for
requests that come through the proxy, not the proxy's own address; for a proxy
on the Docker host that is the Docker network's gateway, which the request log
shows as `remoteIP` while it is not trusted; the signature section says `sig`
is base64url with the `=` padding kept, and gives the example's `sig` for a
stated signing key.
- 2026-09-29 fixed uid and gid for `pixad` (closes #151): the image creates the - 2026-09-29 fixed uid and gid for `pixad` (closes #151): the image creates the
`pixad` group with gid 65532 and the `pixad` user with uid 65532, instead of `pixad` group with gid 65532 and the `pixad` user with uid 65532, instead of
the first free uid 1000, so a bind-mounted `/var/lib/pixa` given to `pixad` the first free uid 1000, so a bind-mounted `/var/lib/pixa` given to `pixad`
+6 -1
View File
@@ -50,7 +50,12 @@ allowlist_hosts:
# 172.16.0.0/12, 192.168.0.0/16), since pixa is deployed behind a proxy on # 172.16.0.0/12, 192.168.0.0/16), since pixa is deployed behind a proxy on
# a private network. An explicitly empty list ([]) trusts no one; an # a private network. An explicitly empty list ([]) trusts no one; an
# explicit list replaces the default. An invalid CIDR aborts startup. # explicit list replaces the default. An invalid CIDR aborts startup.
# Uncomment to override the defaults with your proxy's address range. # Uncomment to override the defaults with the address pixa sees for
# requests that come through your proxy. That is not always the proxy's
# own address: for a proxy on the Docker host it is the gateway of the
# container's Docker network (172.17.0.1 on the default bridge). The
# trusted_proxies entry in README.md says how to find it in the request
# log.
# trusted_proxies: # trusted_proxies:
# - 10.0.0.0/8 # - 10.0.0.0/8
# - 2001:db8::/32 # - 2001:db8::/32