From 3bb992fc1b7a88385f6d83d9e291c44d00a96382 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:03:21 +0000 Subject: [PATCH 1/6] test: reject the config.example.yml placeholder signing_key The placeholder is 45 characters, so it passes the length check and a deployment could unknowingly sign URLs with a key that is public in this repository. Add a failing validation case (and the placeholder constant it references); the rejection follows. Model: opus-4-8 --- internal/config/config.go | 6 ++++++ internal/config/config_validation_internal_test.go | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index fb6e02d..9725a66 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -44,6 +44,12 @@ const ( keyCacheMaxBytes = "cache_max_bytes" ) +// placeholderSigningKey is the dummy signing_key shipped in +// config.example.yml. It is 45 characters, so it passes the length +// check, but it is public in this repository and must be rejected at +// startup so no deployment ever signs URLs with it. +const placeholderSigningKey = "CHANGE_ME_generate_with_openssl_rand_base64_32" + // Static validation errors. Each use site attaches the offending key // and value by wrapping these with fmt.Errorf and %w. var ( diff --git a/internal/config/config_validation_internal_test.go b/internal/config/config_validation_internal_test.go index ca1573e..c12d27d 100644 --- a/internal/config/config_validation_internal_test.go +++ b/internal/config/config_validation_internal_test.go @@ -303,6 +303,11 @@ func invalidHostAndCredentialCases() []abortCase { yaml: "signing_key: short\n", wantErrSubstrings: []string{keySigningKey}, }, + { + name: "signing_key is the documented placeholder", + yaml: "signing_key: " + placeholderSigningKey + "\n", + wantErrSubstrings: []string{keySigningKey}, + }, { name: "signing_key missing", yaml: "port: 8080\n", -- 2.54.0 From 8ed7f7e3c70470d12d37d3ec5668ac4cae0e6080 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:03:32 +0000 Subject: [PATCH 2/6] fix: refuse the example placeholder signing_key at startup Reject the exact config.example.yml placeholder in validate() with an error naming signing_key, so a container or file-based deployment that never changed it fails fast instead of signing URLs with a public key. The signing-key checks move into a validateSigningKey helper, keeping validate() within the cyclomatic-complexity limit. Model: opus-4-8 --- internal/config/config.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 9725a66..e3ab570 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -67,6 +67,9 @@ var ( errPortOutOfRange = errors.New("outside the valid port range") errTooFewConnections = errors.New("must be at least 1") errValueTooShort = errors.New("value too short") + errPlaceholderKey = errors.New( + "is the placeholder from config.example.yml; " + + "generate a real key with: openssl rand -base64 32") errMustBeSetTogether = errors.New("must be set together") errMustNotBeNegative = errors.New("must not be negative") errOverflowsInt64 = errors.New("overflows a 64-bit integer") @@ -347,10 +350,10 @@ func (c *Config) ensureStateDirWritable() error { return nil } -// validate checks that all required configuration values are set and -// that every value is within its valid range. -func (c *Config) validate() error { - // The signing key value is never echoed in error messages. +// validateSigningKey checks that the signing key is present, long +// enough, and not the public placeholder from config.example.yml. The +// key value itself is never echoed in error messages. +func (c *Config) validateSigningKey() error { if c.SigningKey == "" { return fmt.Errorf("config key %q: %w", keySigningKey, errValueRequired) } @@ -362,6 +365,21 @@ func (c *Config) validate() error { keySigningKey, errValueTooShort, minKeyLength, len(c.SigningKey)) } + if c.SigningKey == placeholderSigningKey { + return fmt.Errorf("config key %q: %w", keySigningKey, errPlaceholderKey) + } + + return nil +} + +// validate checks that all required configuration values are set and +// that every value is within its valid range. +func (c *Config) validate() error { + err := c.validateSigningKey() + if err != nil { + return err + } + const maxPort = 65535 if c.Port < 1 || c.Port > maxPort { return fmt.Errorf("config key %q: value %d is %w 1-%d", -- 2.54.0 From 30e4e3d968377e5375214efd8d464993011f0b38 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:03:46 +0000 Subject: [PATCH 3/6] feat: ship image config that reads signing_key from the environment The runtime stage now copies config.docker.yml, which sets only signing_key (from PIXA_SIGNING_KEY), state_dir, and port. This drops the public placeholder key and the baked-in allowlist from the image, matching how upaas configures apps: environment variables and mounts, no injected config file. Model: opus-4-8 --- Dockerfile | 5 +++-- config.docker.yml | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 config.docker.yml diff --git a/Dockerfile b/Dockerfile index 3eb5782..27ab84b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,8 +67,9 @@ RUN adduser -D -H -s /sbin/nologin pixad && \ mkdir -p /var/lib/pixa /etc/pixa && \ chown pixad:pixad /var/lib/pixa -# Copy default config (edit signing_key before use) -COPY config.example.yml /etc/pixa/config.yml +# Copy the image config; signing_key comes from PIXA_SIGNING_KEY. +# Mount a file over /etc/pixa/config.yml to override anything else. +COPY config.docker.yml /etc/pixa/config.yml USER pixad WORKDIR /var/lib/pixa diff --git a/config.docker.yml b/config.docker.yml new file mode 100644 index 0000000..548b45a --- /dev/null +++ b/config.docker.yml @@ -0,0 +1,11 @@ +# Pixa configuration baked into the Docker image. +# +# The signing key is read from the PIXA_SIGNING_KEY environment +# variable; startup aborts naming it when it is unset. Every other key +# is omitted so its default applies. Operators who need more (an +# allowlist, metrics, and so on) mount their own file over +# /etc/pixa/config.yml. + +signing_key: "${ENV:PIXA_SIGNING_KEY}" +state_dir: /var/lib/pixa +port: 8080 -- 2.54.0 From 37d0e24cfc67040ffb10b27223fcc05f74c29241 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:04:19 +0000 Subject: [PATCH 4/6] docs: document the two container config paths (closes #110) Getting Started now shows the docker run with PIXA_SIGNING_KEY and a short paragraph: the key comes from that environment variable, and any other setting is changed by mounting a file over /etc/pixa/config.yml. Model: opus-4-8 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e36dc0..2bc9385 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,16 @@ make build # or build and run via Docker make docker -docker run -p 8080:8080 pixad:latest +docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixad:latest ``` +A container is configured two ways. The signing key comes from the +`PIXA_SIGNING_KEY` environment variable, which the baked-in config +reads; if it is unset the container exits at startup naming the +variable. Everything else uses built-in defaults, so to change any +other setting mount your own file over `/etc/pixa/config.yml` (see +`config.example.yml` for the full set of keys). + ## Rationale Image-heavy web applications need a fast, caching reverse proxy that -- 2.54.0 From c32fdefc3b47d64d57647b58501a58d5ddfdf0bf Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 18:45:26 +0000 Subject: [PATCH 5/6] docs: fix Getting Started local-run example for the refused placeholder The startup now rejects the config.example.yml placeholder signing_key, so the documented `pixad --config config.example.yml` command aborts on first run. Show copying the example to config.yml and setting a real signing_key before running, matching the code. Model: opus-4-8 --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2bc9385..b3b5530 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,12 @@ git clone https://git.eeqj.de/sneak/pixa.git cd pixa make build -# run with a config file -./bin/pixad --config config.example.yml +# run with a config file: copy the example and set a real signing key +# (the example placeholder is refused at startup), e.g. with +# openssl rand -base64 32 +cp config.example.yml config.yml +$EDITOR config.yml # replace the signing_key placeholder +./bin/pixad --config config.yml # or build and run via Docker make docker -- 2.54.0 From c9aa48227dbf363473a27490dfbbb3edebeaffc2 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 21 Sep 2026 19:28:43 +0000 Subject: [PATCH 6/6] docs: use the image name make docker builds in the Docker run example make docker tags the image pixa:latest via script/projectname, so the Getting Started run line must reference pixa:latest, not pixad:latest. Model: opus-4-8 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b3b5530..32fed7f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $EDITOR config.yml # replace the signing_key placeholder # or build and run via Docker make docker -docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixad:latest +docker run -p 8080:8080 -e PIXA_SIGNING_KEY="$(openssl rand -base64 32)" pixa:latest ``` A container is configured two ways. The signing key comes from the -- 2.54.0