diff --git a/README.md b/README.md index ec673d1..4102390 100644 --- a/README.md +++ b/README.md @@ -112,13 +112,19 @@ 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: a proxy on the Docker host that connects to pixa over +`127.0.0.1` is seen as the gateway of the container's Docker network, such as +`172.17.0.1` on the default bridge, and one that connects through another of the +host's addresses is seen with that address. To be sure, read it as `remoteIP` in +pixa's request log 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 +178,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 +260,15 @@ 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 connects to pixa over `127.0.0.1`, 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; a proxy that connects through another of + the host's addresses is seen with that address. To be sure which address it + is, 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 622d81f..5c0ff89 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, which the request log shows as + `remoteIP` while it is not trusted; for a proxy on the Docker host that + connects over `127.0.0.1` that is the Docker network's gateway, not the + proxy's own address; 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..f92b752 100644 --- a/config.example.yml +++ b/config.example.yml @@ -50,7 +50,13 @@ 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: a proxy on the Docker host that connects over 127.0.0.1 is seen +# as the gateway of the container's Docker network (172.17.0.1 on the +# default bridge), and one that connects through another host address is +# seen with that address. To be sure, look it up in the request log as the +# trusted_proxies entry in README.md describes. # trusted_proxies: # - 10.0.0.0/8 # - 2001:db8::/32