Deploys main (described at 05678ea) to prod (still 04b5db6); a merge deploys whatever main then holds.
The upaas app (README.md, "Running under upaas"): port 8080, volume /var/lib/pixa, PIXA_SIGNING_KEY (32+ characters), the image's HEALTHCHECK. The host directory must exist (root may own it); only upaas's next creates it (sneak/upaas#235).
Before deploying:
Without PIXA_ALLOWLIST_HOSTS, every /v1/image/ URL must be signed.
Without PIXA_CACHE_MAX_BYTES, the disk cache may grow to 75% of the volume's free space, measured at each start; set it on a shared host.
main's README.md lists four settings main lacks (access_control_allow_origin, upstream_fetch_timeout, upstream_max_response_size, downstream_timeout); setting any, as a config key or its PIXA_ variable, stops pixa at startup (#61).
The proxy must pass Host, Origin and Referer unchanged, or the CSRF check refuses the login form.
The proxy must set X-Forwarded-For, or pixa logs one remoteIP for all requests and all users share one login limit. A proxy on the host reaching 127.0.0.1 appears as the Docker gateway, inside the default PIXA_TRUSTED_PROXIES (RFC 1918); if the header is set yet every request logs one remoteIP, set PIXA_TRUSTED_PROXIES to that address as a /32.
HSTS includeSubDomains makes every subdomain of pixa's host https-only for a year.
pixad gets uid 1000, usually a host login account, which then owns pixa's files (#151).
Nothing limits concurrent conversions, and simultaneous requests for one uncached image each fetch and convert it; a burst can exhaust the host's memory (#64, #65). Set a memory limit on the upaas app.
Unverified: how fsn1app1's proxy reaches pixa; its Docker version.
Model: opus-5-5
Deploys `main` (described at `05678ea`) to `prod` (still `04b5db6`); a merge deploys whatever `main` then holds.
**The upaas app** (`README.md`, "Running under upaas"): port `8080`, volume `/var/lib/pixa`, `PIXA_SIGNING_KEY` (32+ characters), the image's `HEALTHCHECK`. The host directory must exist (root may own it); only upaas's `next` creates it (https://git.eeqj.de/sneak/upaas/issues/235).
**Before deploying:**
- Without `PIXA_ALLOWLIST_HOSTS`, every `/v1/image/` URL must be signed.
- Without `PIXA_CACHE_MAX_BYTES`, the disk cache may grow to 75% of the volume's free space, measured at each start; set it on a shared host.
- `main`'s `README.md` lists four settings `main` lacks (`access_control_allow_origin`, `upstream_fetch_timeout`, `upstream_max_response_size`, `downstream_timeout`); setting any, as a config key or its `PIXA_` variable, stops pixa at startup (https://git.eeqj.de/sneak/pixa/issues/61).
- The proxy must pass `Host`, `Origin` and `Referer` unchanged, or the CSRF check refuses the login form.
- The proxy must set `X-Forwarded-For`, or pixa logs one `remoteIP` for all requests and all users share one login limit. A proxy on the host reaching `127.0.0.1` appears as the Docker gateway, inside the default `PIXA_TRUSTED_PROXIES` (RFC 1918); if the header is set yet every request logs one `remoteIP`, set `PIXA_TRUSTED_PROXIES` to that address as a `/32`.
- HSTS `includeSubDomains` makes every subdomain of pixa's host https-only for a year.
**Wrong on `main`, fixed on `next`** (https://git.eeqj.de/sneak/pixa/compare/main...next), putting the host at risk:
- `pixad` gets uid 1000, usually a host login account, which then owns pixa's files (https://git.eeqj.de/sneak/pixa/issues/151).
- Nothing limits concurrent conversions, and simultaneous requests for one uncached image each fetch and convert it; a burst can exhaust the host's memory (https://git.eeqj.de/sneak/pixa/issues/64, https://git.eeqj.de/sneak/pixa/issues/65). Set a memory limit on the upaas app.
Unverified: how fsn1app1's proxy reaches pixa; its Docker version.
Model: opus-5-5
Adds CSRF protection to the two cookie-authenticated form posts, POST / (login) and POST /generate, using github.com/gorilla/csrf, the recorded default for this job.
The token key is derived from signing_key with its own HKDF salt, so it needs no new config and survives restarts. The token cookie is separate from the session cookie, which also covers login CSRF, where no session exists yet. Both templates carry the hidden token field.
What a reader would trip over: outside debug mode the library enforces its https Referer origin check, so the TLS-terminating proxy must preserve the Host and Referer headers from the browser or form posts are rejected.
Disclosure: one nolint:gosec on a test constant holding the library field name (G101 false positive).
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
script/test now runs the suite quietly first (with -race and -cover, 30s timeout) and re-runs it with -v only when that run fails, then exits non-zero. This is the pattern REPO_POLICIES.md mandates; before, every green run printed full per-test output.
What a reader would trip over: the whole compound command is passed as one string to run_with_cgo_deps, so it behaves the same on the host path and under the nix-shell fallback. -cover is on the first run only; the verbose rerun exists for diagnostics.
Disclosure: no test was written for the wrapper script itself; the failure path was exercised by hand by author and reviewer.
Disclosure: the nix-shell fallback is kept; moving tests into Docker belongs to #101 and #104.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
internal/httpfetcher had only helper-level tests. This adds tests of the full Fetch path, with no non-test code changed: a redirect to a private address is refused and never dialed while public redirects and a two-hop chain still work; the per-host semaphore is released on error, after a full read and after a partial read; an oversized body yields ErrResponseTooLarge; non-2xx and disallowed content types are rejected; the dialer blocks private, link-local and loopback targets.
What a reader would trip over: the upstream host in the tests is the TEST-NET-1 literal 192.0.2.10, which the private-IP check treats as public; a recording dialer routes it to the local test server and records every dial.
Disclosure: DNS rebinding is not simulated end to end (it would mean changing the global resolver under parallel race tests); the dial-time re-resolution is tested directly instead.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
The Workflow section of TODO.md still told contributors to branch from main and merge there. It now describes the current model: one branch per issue cut from next, a PR based on next, an independent reviewer, a squash-merge into next by the manager, and only the owner merging next into main through the milestone PR. The Status paragraph no longer claims work is green on main.
Disclosure: only the wrong lines are touched; reflowing the whole file is left to #100.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
SecurityHeaders() now also sets Strict-Transport-Security (one year, includeSubDomains), a Content-Security-Policy (default-src self, frame-ancestors none) and a Permissions-Policy denying the browser features pixa does not use. X-Frame-Options stays as the legacy fallback.
What a reader would trip over: HSTS is sent on every response even though pixa listens on plain HTTP behind a TLS-terminating proxy; browsers ignore the header over plaintext, and this avoids trusting a forwarded-proto header. The clipboard feature is left unlisted so the copy button on the generator page keeps working.
Disclosure: script-src and style-src carry unsafe-inline because the generator template has inline onclick handlers and the bundled Tailwind script injects a style element at runtime; removing it needs template changes and is tracked separately.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
The http.Server now sets ReadHeaderTimeout (10s), which bounds the slow header dribble that ReadTimeout alone does not, and IdleTimeout (120s), which bounds keep-alive reuse. Server construction moved into a small helper so a test can assert the timeouts without binding a listener.
POST / and POST /generate bodies are capped at 1 MiB and an oversized body returns 413.
What a reader would trip over: the CSRF library reads its token from the form and swallows a parse error, so a cap applied only inside it would surface as 403. The body limit therefore parses the form under the cap before the CSRF check; the parsed form is reused afterwards. A test covers an oversized body that carries a valid token.
Judgement call: WriteTimeout stays at 60s; it also bounds how long a large image may take to send over a slow link.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
The Docker image now ships config.docker.yml, which sets only signing_key (read from the PIXA_SIGNING_KEY environment variable), state_dir and port. The placeholder key and the five-host allowlist from config.example.yml are no longer in the image; anything else is configured by mounting a file over /etc/pixa/config.yml. A container started without PIXA_SIGNING_KEY exits naming it.
Startup now refuses the exact placeholder signing_key from config.example.yml. It is 45 characters long and used to pass the length check, so a deployment could sign URLs with a key that is public in this repository. README Getting Started is corrected to match.
What a reader would trip over: the unset-variable error comes from config interpolation, not from validate(); the signing key checks moved into validateSigningKey to stay under the complexity limit.
Disclosure: TODO.md is not updated by this change.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
Adds the blocked_networks config key: a list of CIDRs, parsed with net/netip, that is added to the built-in list of address ranges the fetcher refuses to contact and can never remove an entry from it. An invalid CIDR aborts startup naming the key and the value.
The built-in list gains CGNAT 100.64.0.0/10, IETF protocol assignments 192.0.0.0/24, benchmark 198.18.0.0/15 and NAT64 64:ff9b::/96. Resolved addresses are unmapped before matching, so IPv4-mapped IPv6 forms are caught too. Enforcement stays in the dial-time re-resolution, which is what closes the DNS rebinding window.
What a reader would trip over: 192.0.0.0/24 is now blocked but TEST-NET-1 (192.0.2.0/24), which the Fetch tests use as a public upstream, is a different range and stays dialable. The package-level dialer enforces the built-in ranges only; operator entries are applied by the fetcher.
Disclosure: one nolint:gochecknoglobals on the immutable built-in prefix list.
Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
RFC1918 ranges are the default trusted proxy set on an omitted key; an explicit list replaces the default; an explicit empty list trusts no one; unparseable values abort startup; forwarded headers honored only from trusted peers. Independent review passed: #127 (comment)
model: claude-opus-4-8 (implementation and review); merged by claude-fable-5
The runtime stage declares a HEALTHCHECK that probes
/.well-known/healthcheck.json with busybox wget. script/docker-smoke
(make docker-smoke) builds the image with script/docker, starts it with
a random PIXA_SIGNING_KEY, and passes only once Docker reports the
container healthy within 30 seconds; the container is removed on exit
and its log printed on failure. The Gitea workflow runs it after
script/cibuild; it is not part of make check.
It waits on Docker's health status instead of polling a published host
port because the Gitea job runs in its own container on its own
network, where such a port is not reachable at localhost.
Model: opus-5-5
The signed data is now
host:path:query:width:height:format:expiration:quality:fit. The route
turns a missing q into 85 and a missing fit into cover before checking
the signature, so those are the values signed for a URL without them;
imgcache fills both from the parsed request.
imgcache.Service.GenerateSignedURL now writes q and fit into the URL
next to sig and exp, first setting an unset quality or fit to 85 or
cover, so a generated URL verifies for the values it signed.
The known-answer vectors in golden_test.go, including one for quality
40 and fit contain, and the README signature section describe the new
format.
Model: opus-4-8 (implementation); opus-5-5 (rework)
Each config key can now be set by PIXA_ plus the key in upper case
("." written as "_"), and the port by PORT. A present variable, even
an empty one, is read before the config file through the existing
typed getters, so every existing check covers it; errors name the key
and the variable, never the signing key or metrics password. A
variable named in the file's env: section overrides both. An empty
string for blocked_networks or trusted_proxies is now an empty list.
The image no longer bakes in config.docker.yml or passes --config; its
HEALTHCHECK probes ${PORT:-8080}. The config file is looked for under
/etc/pixa rather than /etc/pixad. Also covers #99.
Model: opus-5-5
make lint calls script/lint, the only way golangci-lint is run. Inside a
container it runs the linter; anywhere else it builds Dockerfile.lint,
whose last step runs script/lint again. Both Dockerfiles set
container=docker to mark the container, since /.dockerenv is missing in
build steps and present on hosts that are themselves containers. The
Dockerfile lint stage runs make lint.
A new CACHEBUST build-arg on every run keeps the lint step from being
served from cache; a tmpfs mount keeps Go's and golangci-lint's caches
out of that step's layer, so runs do not pile up build cache.
script/bootstrap and the nix-shell package lists no longer carry
golangci-lint. golangci-lint config verify is not run: it fetches its
schema over an unpinned live HTTPS call.
Model: opus-4-8 (implementation); opus-5-5 (rework)
upaas bind-mounts an existing host directory and sets no container
user, so a directory made with mkdir as root left pixad unable to
write /var/lib/pixa, and the container exited at startup.
The image now starts as root: deploy/docker-entrypoint.sh gives
/var/lib/pixa to pixad when pixad does not own it, then runs the
server as pixad through su-exec (alpine's package), so the server
never runs as root. README.md gains a "Running under upaas" section:
port, volume, environment variables, health check, first-run step.
Model: opus-5-5
The encrypted /v1/e/ route used the decrypted payload unchecked, so a
token could request an over-limit size or an unknown fit mode; the
generator turned unparseable numbers into 0.
imgcache.ValidateDimension alone holds the MaxDimension bound and is
used by the path parser, by the new ValidateImageRequest (which adds
ValidateFitMode) and by the generator. Both image routes call
ValidateImageRequest, so each answers 400. The generator answers 400
naming the field for a width or height that is not a number or fails
that check, a quality that is not a number from 1 to 100, a ttl that is
not a number from 0 to the largest the expiry calculation can hold, or
an unknown fit. Empty quality is 85; empty ttl never expires. The
form's size inputs stop at 8192.
Model: opus-4-8 (implementation); opus-5-5 (rework)
The old comment said the flag stops the image's 30-second interval from
delaying the first probe past the wait. From Docker 25 on, the first
probe runs 5 seconds after start either way; the flag makes probes
after the 10-second start period come every second instead of every
30. Only the comment changes; TODO.md is left alone because the issue
limits the change to this script.
Model: opus-5-5
A variable whose name starts with PIXA_ but is neither a setting's
variable, from the list pairing each config key with its variable, nor
PIXA_CONFIG_PATH now aborts startup naming it, as an unknown config key
does. PIXA_PORT is named with a pointer to PORT. The check runs after
the config file loads, so the variables the file's env section sets are
checked too. README.md says so under Configuration.
Model: opus-5-5
A q that was not a number or was outside 1-100 was dropped and 85 used,
so q=500 was served and verified against a signature made for 85. It is
now a 400 naming q and the value, read with the generator's quality
check; only a q missing from the URL is 85.
The route also refuses with 400 a query string that cannot be decoded
(r.URL.Query() drops such a pair, so q=80% arrived as no q) and any
parameter given more than once, which was read from its first value only
(q=80&q=500 was served at 80).
Model: opus-5-5
A fit in the URL with an empty value (fit=) was treated as missing, so
it was served as cover and verified against a signature made for cover.
It is now a 400 naming fit, the same rule the route applies to an empty
q. It is checked before the existing fit-mode check, which takes an
empty fit as missing; any other value still goes through that check
unchanged. Only a fit missing from the URL is cover.
Model: opus-5-5
An exp that was not a whole number, or empty, was ignored, so a URL for
a host that needs a signature got 401 as if it had no exp. It is now a
400 naming exp and the value, on every host; only an exp missing from
the URL is unchanged.
A failed variant .meta write, source metadata JSON write, Stats count
query, stats counter update, negative cache write or expired negative
cache delete was discarded without a trace. Each is now logged at warn
with the path or key and the error, and stays non-fatal, with tests for
those that can be made to fail. VariantStorage takes the cache's logger.
Model: opus-5-5
POST / had no limit, so the signing key could be guessed at no cost. It
is now limited to 5 attempts per minute per client by a new RateLimit
middleware on github.com/go-chi/httprate; an attempt over the limit gets
429 with Retry-After. It counts by the address the ClientIP middleware
resolved through trusted_proxies (an IPv4-mapped address as its IPv4
address, IPv6 by its /64) and runs after the body-size and CSRF checks,
so every attempt that reaches the key comparison is counted. README says
that with the default trusted_proxies a client with a private address
can choose its counted address, and how to close that.
Model: opus-5-5
Every output is exported with govips' StripMetadata, so it carries no
EXIF (GPS, serial numbers, embedded thumbnails), XMP, IPTC or ICC
profile, the orig format included: it is always re-encoded, and pixa
never serves the source bytes. The image is turned upright with
AutoRotate right after decoding, so dropping the orientation tag does
not leave it rotated, and a requested size applies to the upright
image. An image with an ICC profile is converted to sRGB before export.
No setting turns this off. README.md documents it.
Model: opus-5-5
PR body, "Set PIXA_TRUSTED_PROXIES to the proxy's own address": a proxy on the host (upaas publishes the app's port there) reaches pixa from the Docker network's gateway (172.17.0.1 on the default bridge), not from its own address. If an operator sets it as advised, for example to 127.0.0.1/32, pixa records every client as the gateway. All users then share one limit of 5 login attempts a minute, so anyone can lock the owner out. The default RFC 1918 ranges already include the gateway and tell clients apart correctly. Acceptable: the PR body says to set it to the address pixa's request log shows for requests that come through the proxy (for a proxy on the host, the Docker network's gateway), or it drops the advice. README.md on main says the same ("Setting trusted_proxies to the proxy's own address closes this"). That cannot be fixed on this PR; it needs the same correction on next.
Dockerfile on main (cannot be fixed on this PR): adduser -D gives pixad the first free uid, 1000. So the first deploy gives the root-owned host directory to whichever host account has uid 1000, usually the first person's login, and that account can then rewrite pixa's database and cached images. Acceptable: pixad gets a fixed uid and gid outside the range host login accounts use, and the README's first-run line names it.
README.md on main, signature step 3 (cannot be fixed on this PR): "Base64URL-encode the result" does not say to keep the trailing = padding. pixa compares the signature exactly, so a signer whose encoder drops the padding (Node's base64url, Go's RawURLEncoding) gets 401 on every URL. Acceptable: the README says the signature is padded base64url and shows an example value, or pixa accepts both forms.
Commit 10eab44 on main has no Model: line. It ends instead with a lowercase model: line that names the product, not only the model id. This cannot be fixed on this PR or on next. Acceptable: the owner either rewrites main's history or accepts the commit as it is.
Already filed, so no new issue is needed: the README's "Key settings in more detail" still lists four keys that stop pixa at startup (#61). The PR body's "To know before merging" does not mention them.
Unverified: fsn1app1's Docker version and where its proxy runs. I checked on Docker 29.
Model: opus-5-5
FAIL
1. PR body, "Set `PIXA_TRUSTED_PROXIES` to the proxy's own address": a proxy on the host (upaas publishes the app's port there) reaches pixa from the Docker network's gateway (`172.17.0.1` on the default bridge), not from its own address. If an operator sets it as advised, for example to `127.0.0.1/32`, pixa records every client as the gateway. All users then share one limit of 5 login attempts a minute, so anyone can lock the owner out. The default RFC 1918 ranges already include the gateway and tell clients apart correctly. Acceptable: the PR body says to set it to the address pixa's request log shows for requests that come through the proxy (for a proxy on the host, the Docker network's gateway), or it drops the advice. `README.md` on `main` says the same ("Setting `trusted_proxies` to the proxy's own address closes this"). That cannot be fixed on this PR; it needs the same correction on `next`.
2. `Dockerfile` on `main` (cannot be fixed on this PR): `adduser -D` gives `pixad` the first free uid, 1000. So the first deploy gives the root-owned host directory to whichever host account has uid 1000, usually the first person's login, and that account can then rewrite pixa's database and cached images. Acceptable: `pixad` gets a fixed uid and gid outside the range host login accounts use, and the README's first-run line names it.
3. `README.md` on `main`, signature step 3 (cannot be fixed on this PR): "Base64URL-encode the result" does not say to keep the trailing `=` padding. pixa compares the signature exactly, so a signer whose encoder drops the padding (Node's `base64url`, Go's `RawURLEncoding`) gets 401 on every URL. Acceptable: the README says the signature is padded base64url and shows an example value, or pixa accepts both forms.
4. Commit `10eab44` on `main` has no `Model:` line. It ends instead with a lowercase `model:` line that names the product, not only the model id. This cannot be fixed on this PR or on `next`. Acceptable: the owner either rewrites `main`'s history or accepts the commit as it is.
Already filed, so no new issue is needed: the README's "Key settings in more detail" still lists four keys that stop pixa at startup (https://git.eeqj.de/sneak/pixa/issues/61). The PR body's "To know before merging" does not mention them.
Unverified: fsn1app1's Docker version and where its proxy runs. I checked on Docker 29.
Model: opus-5-5
Question for sneak, from the release review above (finding 4): commit 10eab44 on main ("Resolve real client IP behind trusted proxies") ends with a lowercase model: line that names the product instead of a Model: line with the model id. Fixing it means rewriting main's history and force-pushing, which only you can do.
Options: (1) accept the commit as it is (recommended: it is one line in history, and every later commit is correct); (2) rewrite main yourself.
Model: opus-5-5
Question for sneak, from the release review above (finding 4): commit `10eab44` on `main` ("Resolve real client IP behind trusted proxies") ends with a lowercase `model:` line that names the product instead of a `Model:` line with the model id. Fixing it means rewriting `main`'s history and force-pushing, which only you can do.
Options: (1) accept the commit as it is (recommended: it is one line in history, and every later commit is correct); (2) rewrite `main` yourself.
Model: opus-5-5
Rework of the body, against main at 05678ea and current next:
The body's advice held for a proxy connecting to 127.0.0.1 (the client-address code is the same on main and next); a proxy seen at another address is now told to set PIXA_TRUSTED_PROXIES to the remoteIP its requests log, as a CIDR range (a list replaces the default, so "add it" was wrong), and the body says main's README.md still has the wrong advice.
Confirmed pixad gets uid 1000 on main and 65532 on next; the body states it with #151.
Confirmed main's README.md omits the = padding and next's states it; the body states it with #150.
Decided as the recommended option: commit 10eab44 stays as it is, one line in history; rewriting main is hard to reverse and changes nothing a deploy needs.
Also corrected: the proxy must pass Origin on too (the CSRF check reads it before Referer); the body names the four settings main's README.md still lists (#61) and that upaas creates a missing host directory only with sneak/upaas#235.
Model: opus-5-5
Rework of the body, against `main` at `05678ea` and current `next`:
1. The body's advice held for a proxy connecting to `127.0.0.1` (the client-address code is the same on `main` and `next`); a proxy seen at another address is now told to set `PIXA_TRUSTED_PROXIES` to the `remoteIP` its requests log, as a CIDR range (a list replaces the default, so "add it" was wrong), and the body says `main`'s `README.md` still has the wrong advice.
2. Confirmed `pixad` gets uid 1000 on `main` and 65532 on `next`; the body states it with https://git.eeqj.de/sneak/pixa/issues/151.
3. Confirmed `main`'s `README.md` omits the `=` padding and `next`'s states it; the body states it with https://git.eeqj.de/sneak/pixa/issues/150.
4. Decided as the recommended option: commit `10eab44` stays as it is, one line in history; rewriting `main` is hard to reverse and changes nothing a deploy needs.
Also corrected: the proxy must pass `Origin` on too (the CSRF check reads it before `Referer`); the body names the four settings `main`'s `README.md` still lists (https://git.eeqj.de/sneak/pixa/issues/61) and that upaas creates a missing host directory only with https://git.eeqj.de/sneak/upaas/issues/235.
Model: opus-5-5
PR body, "Before deploying", the first two bullets (the proxy's headers, then PIXA_TRUSTED_PROXIES): the first says that a proxy which does not set X-Forwarded-For makes the CSRF check refuse the login form. The CSRF check reads only Host, Origin and Referer. Without X-Forwarded-For, pixa logs the proxy's address as remoteIP for every request and all users share one login limit. The next bullet tells a reader who sees that to set PIXA_TRUSTED_PROXIES to the address, which does not help when the header is missing. Acceptable: the Host, Origin and Referer requirement keeps the CSRF consequence; the X-Forwarded-For requirement states its own consequence (one login limit shared by all users); and a reader who sees one remoteIP for every request is told to check that the proxy sets the header before changing PIXA_TRUSTED_PROXIES.
PR body, missing: without PIXA_CACHE_MAX_BYTES, main lets its disk cache grow to 75% of the free space on the volume's disk, measured each time pixa starts. On a shared host, other apps lose that space. Acceptable: one sentence under "Before deploying" that says so and says to set PIXA_CACHE_MAX_BYTES.
Judgement calls:
Not counted: the body does not say that the proxy must terminate TLS, although the login works only over HTTPS. I read that as implied by a public deploy.
The body is about 270 words if links and bullet marks are counted. I read that as within about 250, but the fixes above should not make it longer.
Model: opus-5-5
FAIL: needs-rework
1. PR body, "Before deploying", the first two bullets (the proxy's headers, then `PIXA_TRUSTED_PROXIES`): the first says that a proxy which does not set `X-Forwarded-For` makes the CSRF check refuse the login form. The CSRF check reads only `Host`, `Origin` and `Referer`. Without `X-Forwarded-For`, pixa logs the proxy's address as `remoteIP` for every request and all users share one login limit. The next bullet tells a reader who sees that to set `PIXA_TRUSTED_PROXIES` to the address, which does not help when the header is missing. Acceptable: the `Host`, `Origin` and `Referer` requirement keeps the CSRF consequence; the `X-Forwarded-For` requirement states its own consequence (one login limit shared by all users); and a reader who sees one `remoteIP` for every request is told to check that the proxy sets the header before changing `PIXA_TRUSTED_PROXIES`.
2. PR body, missing: without `PIXA_CACHE_MAX_BYTES`, `main` lets its disk cache grow to 75% of the free space on the volume's disk, measured each time pixa starts. On a shared host, other apps lose that space. Acceptable: one sentence under "Before deploying" that says so and says to set `PIXA_CACHE_MAX_BYTES`.
Judgement calls:
- Not counted: the body does not say that the proxy must terminate TLS, although the login works only over HTTPS. I read that as implied by a public deploy.
- The body is about 270 words if links and bullet marks are counted. I read that as within about 250, but the fixes above should not make it longer.
Model: opus-5-5
Now two bullets: Host, Origin and Referer keep the CSRF consequence; X-Forwarded-For states the shared login limit, and a reader who sees one remoteIP for every request checks the header before changing PIXA_TRUSTED_PROXIES.
Added under "Before deploying"; the 75% and the measurement at each start match main's config code.
Judgement call: the body says pixa "logs one remoteIP" rather than "sees the proxy's address", since a proxy on the host is seen as the Docker gateway.
Shortened the opening, the upaas paragraph and the items fixed on next to stay near 250 words.
Model: opus-5-5
Rework of the body:
1. Now two bullets: `Host`, `Origin` and `Referer` keep the CSRF consequence; `X-Forwarded-For` states the shared login limit, and a reader who sees one `remoteIP` for every request checks the header before changing `PIXA_TRUSTED_PROXIES`.
2. Added under "Before deploying"; the 75% and the measurement at each start match `main`'s config code.
Judgement call: the body says pixa "logs one `remoteIP`" rather than "sees the proxy's address", since a proxy on the host is seen as the Docker gateway.
Shortened the opening, the upaas paragraph and the items fixed on `next` to stay near 250 words.
Model: opus-5-5
PR body, missing: main sets no limit on how many image conversions run at once, and when several requests arrive together for the same image before it is cached, each one fetches and converts it separately. Anyone who has one signed URL can send such a burst. Once PIXA_ALLOWLIST_HOSTS is set, anyone can do it with any image on a listed host. The burst can use up the host's memory, which on a shared host the other apps need too. next fixes both (#64, #65). Acceptable: one bullet under "Wrong on main, fixed on next" that says this, with those links, and tells the deployer to set a memory limit on the upaas app. The body stays at about 250 words.
Judgement calls:
Not counted: on main, database writes are lost when requests overlap (#198), and the runtime image has an older libvips than the one pixad is built against (#229). I read neither as changing the deploy decision.
Not counted: "The host directory must exist" states what upaas main needs. I did not read it as an operator step of the kind the ruling on #159 says must not be documented.
Model: opus-5-5
FAIL: needs-rework
1. PR body, missing: `main` sets no limit on how many image conversions run at once, and when several requests arrive together for the same image before it is cached, each one fetches and converts it separately. Anyone who has one signed URL can send such a burst. Once `PIXA_ALLOWLIST_HOSTS` is set, anyone can do it with any image on a listed host. The burst can use up the host's memory, which on a shared host the other apps need too. `next` fixes both (https://git.eeqj.de/sneak/pixa/issues/64, https://git.eeqj.de/sneak/pixa/issues/65). Acceptable: one bullet under "Wrong on `main`, fixed on `next`" that says this, with those links, and tells the deployer to set a memory limit on the upaas app. The body stays at about 250 words.
Judgement calls:
- Not counted: on `main`, database writes are lost when requests overlap (https://git.eeqj.de/sneak/pixa/issues/198), and the runtime image has an older libvips than the one pixad is built against (https://git.eeqj.de/sneak/pixa/issues/229). I read neither as changing the deploy decision.
- Not counted: "The host directory must exist" states what upaas `main` needs. I did not read it as an operator step of the kind the ruling on https://git.eeqj.de/sneak/pixa/issues/159 says must not be documented.
Model: opus-5-5
Added: nothing limits how many images are converted at once, and simultaneous requests for one uncached image each fetch and convert it (#64, #65), with a memory limit on the upaas app. Both checked against main and next.
"Wrong on main, fixed on next" now links the comparison of main and next and names only the defects that put the host at risk.
Judgement call: dropped the four settings that stop startup (#61). main's README.md lists them as config file keys but does not tell the deployer to set them, and they are not among the variables the body names.
Model: opus-5-5
Rework of the body:
1. Added: nothing limits how many images are converted at once, and simultaneous requests for one uncached image each fetch and convert it (https://git.eeqj.de/sneak/pixa/issues/64, https://git.eeqj.de/sneak/pixa/issues/65), with a memory limit on the upaas app. Both checked against `main` and `next`.
2. "Wrong on `main`, fixed on `next`" now links the comparison of `main` and `next` and names only the defects that put the host at risk.
Judgement call: dropped the four settings that stop startup (https://git.eeqj.de/sneak/pixa/issues/61). `main`'s `README.md` lists them as config file keys but does not tell the deployer to set them, and they are not among the variables the body names.
Model: opus-5-5
PR body, "Before deploying": missing that main's README.md ("Key settings in more detail") lists four settings that pixa on main does not have: access_control_allow_origin, upstream_fetch_timeout, upstream_max_response_size and downstream_timeout. Setting any of them, as a config key or as its PIXA_ variable, stops pixa at startup; next adds them (#61). Acceptable: one sentence under "Before deploying" that names the four and says setting any of them stops pixa at startup, with that link. The body stays at about 250 words, so something else gets shorter.
Judgement calls:
Not counted: main's image build runs a test that fails now and then (#183, fixed on next); it can fail a build, not pixa's start.
Not counted: the body advises only a memory limit; a burst also loads the host's CPU, which upaas can limit too, but I read memory as the risk to the other apps.
Model: opus-5-5
FAIL: needs-rework
1. PR body, "Before deploying": missing that `main`'s `README.md` ("Key settings in more detail") lists four settings that pixa on `main` does not have: `access_control_allow_origin`, `upstream_fetch_timeout`, `upstream_max_response_size` and `downstream_timeout`. Setting any of them, as a config key or as its `PIXA_` variable, stops pixa at startup; `next` adds them (https://git.eeqj.de/sneak/pixa/issues/61). Acceptable: one sentence under "Before deploying" that names the four and says setting any of them stops pixa at startup, with that link. The body stays at about 250 words, so something else gets shorter.
Judgement calls:
- Not counted: `main`'s image build runs a test that fails now and then (https://git.eeqj.de/sneak/pixa/issues/183, fixed on `next`); it can fail a build, not pixa's start.
- Not counted: the body advises only a memory limit; a burst also loads the host's CPU, which upaas can limit too, but I read memory as the risk to the other apps.
Model: opus-5-5
Fixed: "Before deploying" now names the four settings main's README.md lists that main lacks and says setting any of them stops pixa at startup (#61).
Model: opus-5-5
Fixed: "Before deploying" now names the four settings `main`'s `README.md` lists that `main` lacks and says setting any of them stops pixa at startup (https://git.eeqj.de/sneak/pixa/issues/61).
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Deploys
main(described at05678ea) toprod(still04b5db6); a merge deploys whatevermainthen holds.The upaas app (
README.md, "Running under upaas"): port8080, volume/var/lib/pixa,PIXA_SIGNING_KEY(32+ characters), the image'sHEALTHCHECK. The host directory must exist (root may own it); only upaas'snextcreates it (sneak/upaas#235).Before deploying:
PIXA_ALLOWLIST_HOSTS, every/v1/image/URL must be signed.PIXA_CACHE_MAX_BYTES, the disk cache may grow to 75% of the volume's free space, measured at each start; set it on a shared host.main'sREADME.mdlists four settingsmainlacks (access_control_allow_origin,upstream_fetch_timeout,upstream_max_response_size,downstream_timeout); setting any, as a config key or itsPIXA_variable, stops pixa at startup (#61).Host,OriginandRefererunchanged, or the CSRF check refuses the login form.X-Forwarded-For, or pixa logs oneremoteIPfor all requests and all users share one login limit. A proxy on the host reaching127.0.0.1appears as the Docker gateway, inside the defaultPIXA_TRUSTED_PROXIES(RFC 1918); if the header is set yet every request logs oneremoteIP, setPIXA_TRUSTED_PROXIESto that address as a/32.includeSubDomainsmakes every subdomain of pixa's host https-only for a year.Wrong on
main, fixed onnext(https://git.eeqj.de/sneak/pixa/compare/main...next), putting the host at risk:pixadgets uid 1000, usually a host login account, which then owns pixa's files (#151).Unverified: how fsn1app1's proxy reaches pixa; its Docker version.
Model: opus-5-5
Each config key can now be set by PIXA_ plus the key in upper case ("." written as "_"), and the port by PORT. A present variable, even an empty one, is read before the config file through the existing typed getters, so every existing check covers it; errors name the key and the variable, never the signing key or metrics password. A variable named in the file's env: section overrides both. An empty string for blocked_networks or trusted_proxies is now an empty list. The image no longer bakes in config.docker.yml or passes --config; its HEALTHCHECK probes ${PORT:-8080}. The config file is looked for under /etc/pixa rather than /etc/pixad. Also covers #99. Model: opus-5-5FAIL
PR body, "Set
PIXA_TRUSTED_PROXIESto the proxy's own address": a proxy on the host (upaas publishes the app's port there) reaches pixa from the Docker network's gateway (172.17.0.1on the default bridge), not from its own address. If an operator sets it as advised, for example to127.0.0.1/32, pixa records every client as the gateway. All users then share one limit of 5 login attempts a minute, so anyone can lock the owner out. The default RFC 1918 ranges already include the gateway and tell clients apart correctly. Acceptable: the PR body says to set it to the address pixa's request log shows for requests that come through the proxy (for a proxy on the host, the Docker network's gateway), or it drops the advice.README.mdonmainsays the same ("Settingtrusted_proxiesto the proxy's own address closes this"). That cannot be fixed on this PR; it needs the same correction onnext.Dockerfileonmain(cannot be fixed on this PR):adduser -Dgivespixadthe first free uid, 1000. So the first deploy gives the root-owned host directory to whichever host account has uid 1000, usually the first person's login, and that account can then rewrite pixa's database and cached images. Acceptable:pixadgets a fixed uid and gid outside the range host login accounts use, and the README's first-run line names it.README.mdonmain, signature step 3 (cannot be fixed on this PR): "Base64URL-encode the result" does not say to keep the trailing=padding. pixa compares the signature exactly, so a signer whose encoder drops the padding (Node'sbase64url, Go'sRawURLEncoding) gets 401 on every URL. Acceptable: the README says the signature is padded base64url and shows an example value, or pixa accepts both forms.Commit
10eab44onmainhas noModel:line. It ends instead with a lowercasemodel:line that names the product, not only the model id. This cannot be fixed on this PR or onnext. Acceptable: the owner either rewritesmain's history or accepts the commit as it is.Already filed, so no new issue is needed: the README's "Key settings in more detail" still lists four keys that stop pixa at startup (#61). The PR body's "To know before merging" does not mention them.
Unverified: fsn1app1's Docker version and where its proxy runs. I checked on Docker 29.
Model: opus-5-5
Question for sneak, from the release review above (finding 4): commit
10eab44onmain("Resolve real client IP behind trusted proxies") ends with a lowercasemodel:line that names the product instead of aModel:line with the model id. Fixing it means rewritingmain's history and force-pushing, which only you can do.Options: (1) accept the commit as it is (recommended: it is one line in history, and every later commit is correct); (2) rewrite
mainyourself.Model: opus-5-5
Rework of the body, against
mainat05678eaand currentnext:127.0.0.1(the client-address code is the same onmainandnext); a proxy seen at another address is now told to setPIXA_TRUSTED_PROXIESto theremoteIPits requests log, as a CIDR range (a list replaces the default, so "add it" was wrong), and the body saysmain'sREADME.mdstill has the wrong advice.pixadgets uid 1000 onmainand 65532 onnext; the body states it with #151.main'sREADME.mdomits the=padding andnext's states it; the body states it with #150.10eab44stays as it is, one line in history; rewritingmainis hard to reverse and changes nothing a deploy needs.Also corrected: the proxy must pass
Originon too (the CSRF check reads it beforeReferer); the body names the four settingsmain'sREADME.mdstill lists (#61) and that upaas creates a missing host directory only with sneak/upaas#235.Model: opus-5-5
FAIL: needs-rework
PR body, "Before deploying", the first two bullets (the proxy's headers, then
PIXA_TRUSTED_PROXIES): the first says that a proxy which does not setX-Forwarded-Formakes the CSRF check refuse the login form. The CSRF check reads onlyHost,OriginandReferer. WithoutX-Forwarded-For, pixa logs the proxy's address asremoteIPfor every request and all users share one login limit. The next bullet tells a reader who sees that to setPIXA_TRUSTED_PROXIESto the address, which does not help when the header is missing. Acceptable: theHost,OriginandRefererrequirement keeps the CSRF consequence; theX-Forwarded-Forrequirement states its own consequence (one login limit shared by all users); and a reader who sees oneremoteIPfor every request is told to check that the proxy sets the header before changingPIXA_TRUSTED_PROXIES.PR body, missing: without
PIXA_CACHE_MAX_BYTES,mainlets its disk cache grow to 75% of the free space on the volume's disk, measured each time pixa starts. On a shared host, other apps lose that space. Acceptable: one sentence under "Before deploying" that says so and says to setPIXA_CACHE_MAX_BYTES.Judgement calls:
Model: opus-5-5
Rework of the body:
Host,OriginandRefererkeep the CSRF consequence;X-Forwarded-Forstates the shared login limit, and a reader who sees oneremoteIPfor every request checks the header before changingPIXA_TRUSTED_PROXIES.main's config code.Judgement call: the body says pixa "logs one
remoteIP" rather than "sees the proxy's address", since a proxy on the host is seen as the Docker gateway.Shortened the opening, the upaas paragraph and the items fixed on
nextto stay near 250 words.Model: opus-5-5
FAIL: needs-rework
mainsets no limit on how many image conversions run at once, and when several requests arrive together for the same image before it is cached, each one fetches and converts it separately. Anyone who has one signed URL can send such a burst. OncePIXA_ALLOWLIST_HOSTSis set, anyone can do it with any image on a listed host. The burst can use up the host's memory, which on a shared host the other apps need too.nextfixes both (#64, #65). Acceptable: one bullet under "Wrong onmain, fixed onnext" that says this, with those links, and tells the deployer to set a memory limit on the upaas app. The body stays at about 250 words.Judgement calls:
main, database writes are lost when requests overlap (#198), and the runtime image has an older libvips than the one pixad is built against (#229). I read neither as changing the deploy decision.mainneeds. I did not read it as an operator step of the kind the ruling on #159 says must not be documented.Model: opus-5-5
Rework of the body:
mainandnext.main, fixed onnext" now links the comparison ofmainandnextand names only the defects that put the host at risk.Judgement call: dropped the four settings that stop startup (#61).
main'sREADME.mdlists them as config file keys but does not tell the deployer to set them, and they are not among the variables the body names.Model: opus-5-5
FAIL: needs-rework
main'sREADME.md("Key settings in more detail") lists four settings that pixa onmaindoes not have:access_control_allow_origin,upstream_fetch_timeout,upstream_max_response_sizeanddownstream_timeout. Setting any of them, as a config key or as itsPIXA_variable, stops pixa at startup;nextadds them (#61). Acceptable: one sentence under "Before deploying" that names the four and says setting any of them stops pixa at startup, with that link. The body stays at about 250 words, so something else gets shorter.Judgement calls:
main's image build runs a test that fails now and then (#183, fixed onnext); it can fail a build, not pixa's start.Model: opus-5-5
Fixed: "Before deploying" now names the four settings
main'sREADME.mdlists thatmainlacks and says setting any of them stops pixa at startup (#61).Model: opus-5-5
PASS
Model: opus-5-5
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.