diff --git a/README.md b/README.md index ec673d1..c158301 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,18 @@ copy is fresh. 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 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 -`trusted_proxies`; otherwise all users behind the proxy are counted as one -client. 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 the proxy's own -address closes this. +address comes from `X-Forwarded-For` only when the address pixa sees for +requests that come through the proxy is in `trusted_proxies`; otherwise all +users behind the proxy are counted as one client. That address is not always +the proxy's own: for a proxy on the Docker host it is the gateway of the +container's Docker network, such as `172.17.0.1` on the default bridge. pixa's +request log shows it as `remoteIP` while it is not in `trusted_proxies` (see +`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 @@ -172,19 +177,27 @@ Where: outside), or `cover` when the URL has no `fit`; a request whose `fit` is anything else, an empty `fit=` included, is refused with 400 -**Example:** resize `https://cdn.example.com/photos/cat.jpg` to 800x600 -WebP with expiration 1704067200, default quality and fit: +The URL's `sig` is the HMAC-SHA256 result in base64url (the URL-safe alphabet +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: `cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200:85:cover` -2. Compute HMAC-SHA256 with your secret key -3. Base64URL-encode the result +2. Compute HMAC-SHA256 of it with the signing key +3. Base64URL-encode the result, keeping the `=` padding: + `-ay7KHpfqmtIGbibDGbUuBDkymi-Ymdn0NkC6j5EJag=` 4. URL: - `/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=&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 -`:40:contain` and the URL is -`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=&exp=1704067200&q=40&fit=contain`. +`:40:contain`, the signature is `5IwXUx6vf7yefhaUvFzgXZvG2o0Df4RJxPTK3pKq5VU=`, +and the URL is +`/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=5IwXUx6vf7yefhaUvFzgXZvG2o0Df4RJxPTK3pKq5VU=&exp=1704067200&q=40&fit=contain`. **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 proxy on a private network; an explicitly empty list (`[]`) trusts no 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 - covered by the defaults + startup. Set this to the address pixa sees for requests that come through + 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_max_response_size` — max origin response size - `downstream_timeout` — client response timeout diff --git a/TODO.md b/TODO.md index a8ec7c3..8fa87ed 100644 --- a/TODO.md +++ b/TODO.md @@ -30,6 +30,14 @@ exhaustion # 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 `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` diff --git a/config.example.yml b/config.example.yml index 8df6a35..3f7381c 100644 --- a/config.example.yml +++ b/config.example.yml @@ -50,7 +50,12 @@ allowlist_hosts: # 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 # 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: # - 10.0.0.0/8 # - 2001:db8::/32