next -> main (1.0.0 milestone) #118

Open
clawbot wants to merge 7 commits from next into main
Collaborator

next is mergeable into main at any time; whatever is on it is reviewed and gated.

On the branch now:

  • The Docker image takes signing_key from the PIXA_SIGNING_KEY environment variable and no longer ships the example config; startup refuses the placeholder key from config.example.yml (#110).
  • CSRF protection on the login and URL-generator forms (#93).
  • HSTS, Content-Security-Policy and Permissions-Policy headers on every response (#91).
  • ReadHeaderTimeout and IdleTimeout on the HTTP server, and a 1 MiB body limit on the two form posts returning 413 (#92). WriteTimeout stays at 60s, which also bounds how long a large image may take to send over a slow link.
  • script/test runs quietly and re-runs verbosely only on failure (#59).
  • Tests for the redirect-to-private-address block and semaphore release in internal/httpfetcher; no non-test code changed (#78).
  • TODO.md describes the next branching model (#106).

To know before deploying:

  • The container needs PIXA_SIGNING_KEY (32 characters or more) or it exits at startup. The image no longer carries the five example allowlist_hosts entries (hosts whose images need no signature): with no mounted config file every image URL must be signed. To exempt hosts, mount a config file over /etc/pixa/config.yml.
  • Outside debug mode the CSRF library checks the https Referer origin on form posts, so the TLS-terminating proxy in front of pixa must pass the Host and Referer headers from the browser through unchanged, or login and URL generation are rejected.
  • HSTS with includeSubDomains is now sent on every response; once a browser has seen it over https, every subdomain of the serving domain is https-only in that browser for a year.
  • The Content-Security-Policy still allows unsafe-inline; removing it is #125.

Hosted CI shows red on this branch for an infrastructure reason only: the mac-studio runner has no free docker subnets (sneak/vaultik#133). Each unit was gated with a local make check by an independent reviewer.

Waiting on you: the ruling on #60.

Model: fable-5-1

`next` is mergeable into `main` at any time; whatever is on it is reviewed and gated. On the branch now: - The Docker image takes `signing_key` from the `PIXA_SIGNING_KEY` environment variable and no longer ships the example config; startup refuses the placeholder key from `config.example.yml` (https://git.eeqj.de/sneak/pixa/issues/110). - CSRF protection on the login and URL-generator forms (https://git.eeqj.de/sneak/pixa/issues/93). - HSTS, Content-Security-Policy and Permissions-Policy headers on every response (https://git.eeqj.de/sneak/pixa/issues/91). - `ReadHeaderTimeout` and `IdleTimeout` on the HTTP server, and a 1 MiB body limit on the two form posts returning 413 (https://git.eeqj.de/sneak/pixa/issues/92). `WriteTimeout` stays at 60s, which also bounds how long a large image may take to send over a slow link. - `script/test` runs quietly and re-runs verbosely only on failure (https://git.eeqj.de/sneak/pixa/issues/59). - Tests for the redirect-to-private-address block and semaphore release in `internal/httpfetcher`; no non-test code changed (https://git.eeqj.de/sneak/pixa/issues/78). - `TODO.md` describes the `next` branching model (https://git.eeqj.de/sneak/pixa/issues/106). To know before deploying: - The container needs `PIXA_SIGNING_KEY` (32 characters or more) or it exits at startup. The image no longer carries the five example `allowlist_hosts` entries (hosts whose images need no signature): with no mounted config file every image URL must be signed. To exempt hosts, mount a config file over `/etc/pixa/config.yml`. - Outside `debug` mode the CSRF library checks the https `Referer` origin on form posts, so the TLS-terminating proxy in front of pixa must pass the `Host` and `Referer` headers from the browser through unchanged, or login and URL generation are rejected. - HSTS with `includeSubDomains` is now sent on every response; once a browser has seen it over https, every subdomain of the serving domain is https-only in that browser for a year. - The Content-Security-Policy still allows `unsafe-inline`; removing it is https://git.eeqj.de/sneak/pixa/issues/125. Hosted CI shows red on this branch for an infrastructure reason only: the `mac-studio` runner has no free docker subnets (https://git.eeqj.de/sneak/vaultik/issues/133). Each unit was gated with a local `make check` by an independent reviewer. Waiting on you: the ruling on https://git.eeqj.de/sneak/pixa/issues/60. Model: fable-5-1
clawbot added this to the 1.0.0 milestone 2026-09-21 19:26:34 +02:00
clawbot self-assigned this 2026-09-21 19:26:34 +02:00
clawbot added 1 commit 2026-09-21 19:26:34 +02:00
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)
clawbot added 1 commit 2026-09-21 19:43:23 +02:00
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)
clawbot added 1 commit 2026-09-21 19:59:29 +02:00
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)
clawbot added 1 commit 2026-09-21 19:59:52 +02:00
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)
clawbot added 1 commit 2026-09-21 20:43:15 +02:00
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)
clawbot added 1 commit 2026-09-21 20:59:27 +02:00
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)
clawbot added 1 commit 2026-09-21 21:59:26 +02:00
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)
Some checks are pending
check / check (push) Failing after 1s
Check / check (pull_request)
Required
Some required checks are missing.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin next:next
git checkout next
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#118