4 Commits
Author SHA1 Message Date
sneak e1044e9037 docs: document the two container config paths (closes #110)
check / check (push) Failing after 0s
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
2026-09-21 18:07:22 +00:00
sneak 9d667bc4a8 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
2026-09-21 18:07:22 +00:00
sneak e5e7e81625 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
2026-09-21 18:07:22 +00:00
sneak 0c87b058aa 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
2026-09-21 18:03:21 +00:00
5 changed files with 55 additions and 7 deletions
+3 -2
View File
@@ -67,8 +67,9 @@ RUN adduser -D -H -s /sbin/nologin pixad && \
mkdir -p /var/lib/pixa /etc/pixa && \ mkdir -p /var/lib/pixa /etc/pixa && \
chown pixad:pixad /var/lib/pixa chown pixad:pixad /var/lib/pixa
# Copy default config (edit signing_key before use) # Copy the image config; signing_key comes from PIXA_SIGNING_KEY.
COPY config.example.yml /etc/pixa/config.yml # Mount a file over /etc/pixa/config.yml to override anything else.
COPY config.docker.yml /etc/pixa/config.yml
USER pixad USER pixad
WORKDIR /var/lib/pixa WORKDIR /var/lib/pixa
+8 -1
View File
@@ -20,9 +20,16 @@ make build
# or build and run via Docker # or build and run via Docker
make 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 ## Rationale
Image-heavy web applications need a fast, caching reverse proxy that Image-heavy web applications need a fast, caching reverse proxy that
+11
View File
@@ -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
+28 -4
View File
@@ -44,6 +44,12 @@ const (
keyCacheMaxBytes = "cache_max_bytes" 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 // Static validation errors. Each use site attaches the offending key
// and value by wrapping these with fmt.Errorf and %w. // and value by wrapping these with fmt.Errorf and %w.
var ( var (
@@ -61,6 +67,9 @@ var (
errPortOutOfRange = errors.New("outside the valid port range") errPortOutOfRange = errors.New("outside the valid port range")
errTooFewConnections = errors.New("must be at least 1") errTooFewConnections = errors.New("must be at least 1")
errValueTooShort = errors.New("value too short") 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") errMustBeSetTogether = errors.New("must be set together")
errMustNotBeNegative = errors.New("must not be negative") errMustNotBeNegative = errors.New("must not be negative")
errOverflowsInt64 = errors.New("overflows a 64-bit integer") errOverflowsInt64 = errors.New("overflows a 64-bit integer")
@@ -341,10 +350,10 @@ func (c *Config) ensureStateDirWritable() error {
return nil return nil
} }
// validate checks that all required configuration values are set and // validateSigningKey checks that the signing key is present, long
// that every value is within its valid range. // enough, and not the public placeholder from config.example.yml. The
func (c *Config) validate() error { // key value itself is never echoed in error messages.
// The signing key value is never echoed in error messages. func (c *Config) validateSigningKey() error {
if c.SigningKey == "" { if c.SigningKey == "" {
return fmt.Errorf("config key %q: %w", keySigningKey, errValueRequired) return fmt.Errorf("config key %q: %w", keySigningKey, errValueRequired)
} }
@@ -356,6 +365,21 @@ func (c *Config) validate() error {
keySigningKey, errValueTooShort, minKeyLength, len(c.SigningKey)) 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 const maxPort = 65535
if c.Port < 1 || c.Port > maxPort { if c.Port < 1 || c.Port > maxPort {
return fmt.Errorf("config key %q: value %d is %w 1-%d", return fmt.Errorf("config key %q: value %d is %w 1-%d",
@@ -303,6 +303,11 @@ func invalidHostAndCredentialCases() []abortCase {
yaml: "signing_key: short\n", yaml: "signing_key: short\n",
wantErrSubstrings: []string{keySigningKey}, wantErrSubstrings: []string{keySigningKey},
}, },
{
name: "signing_key is the documented placeholder",
yaml: "signing_key: " + placeholderSigningKey + "\n",
wantErrSubstrings: []string{keySigningKey},
},
{ {
name: "signing_key missing", name: "signing_key missing",
yaml: "port: 8080\n", yaml: "port: 8080\n",