From 7bef67ef92cd37a144e3216f0e717a3bb8346f77 Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 09:45:51 +0200 Subject: [PATCH 1/5] [bugfix] Frontend: fix Caddy EPERM under hardened securityContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The goquery-ui Helm chart runs the frontend pod with a hardened securityContext (allowPrivilegeEscalation: false, readOnlyRootFilesystem, cap drop ALL, non-root). Two image-level assumptions broke under it: 1. The Caddy binary copied from caddy:2-alpine carries the cap_net_bind_service file capability. Under no_new_privs the kernel refuses to exec() a file with file caps and returns EPERM ("caddy: Operation not permitted"). We listen on :5137, so the cap is unneeded — strip it with `setcap -r`. 2. The alpine base never sets XDG_DATA_HOME/XDG_CONFIG_HOME, so Caddy wrote storage to $HOME/.local & $HOME/.config on the read-only rootfs, erroring on startup and leaving the chart's /data & /config mounts inert. Point both at /data and /config. Also add a hardened `prod` profile to docker-compose that mirrors the chart's securityContext 1:1, so these deployment-time failures surface locally (`make docker-prod`) instead of in the cluster. This is an image fix: it ships to chart users via the v4.2.12 release, which rebuilds goprobe/frontend and bumps the chart's appVersion. No Chart.yaml change here — appVersion is owned by the release process. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/goquery-ui/Dockerfile | 21 +++++++- frontend/goquery-ui/Makefile | 12 +++-- frontend/goquery-ui/docker-compose.yml | 67 ++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/frontend/goquery-ui/Dockerfile b/frontend/goquery-ui/Dockerfile index c94db669..3ed8b43b 100644 --- a/frontend/goquery-ui/Dockerfile +++ b/frontend/goquery-ui/Dockerfile @@ -16,18 +16,35 @@ FROM caddy:2-alpine AS caddy-builder # Use Alpine as the base for better security and fewer vulnerabilities FROM alpine:3.19 -# Install ca-certificates for HTTPS and gettext for envsubst (env.js generation) -RUN apk add --no-cache ca-certificates gettext +# Install ca-certificates for HTTPS, gettext for envsubst (env.js generation), +# and libcap to strip file capabilities from the Caddy binary (see below). +RUN apk add --no-cache ca-certificates gettext libcap # Copy Caddy binary from the builder image COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy +# The official Caddy binary carries cap_net_bind_service=+ep so it can bind to +# :80/:443 as non-root. BuildKit's COPY preserves that file capability. Under a +# hardened runtime (allowPrivilegeEscalation: false → no_new_privs), the kernel +# refuses to exec() a binary that carries file caps and returns EPERM +# ("caddy: Operation not permitted"). We listen on :5137 (non-privileged), so the +# capability is unnecessary — strip it to keep the binary execable when hardened. +RUN setcap -r /usr/bin/caddy + # Create a non-root user for running Caddy RUN addgroup -g 1000 caddy && \ adduser -D -u 1000 -G caddy -h /var/lib/caddy -s /sbin/nologin caddy RUN mkdir -p /var/run/env /opt/app/www /etc/caddy /data /config && \ chown -R caddy:caddy /var/run/env /opt/app/www /etc/caddy /data /config +# Point Caddy's storage at the dirs we create + back with writable volumes. +# The official caddy image sets these; our alpine base does not, so without them +# Caddy falls back to $HOME/.local/share & $HOME/.config — which are on the +# read-only rootfs in production, producing errors on startup. (The /data and +# /config volume mounts in the Helm chart / compose are inert until this is set.) +ENV XDG_DATA_HOME=/data \ + XDG_CONFIG_HOME=/config + # static files go to a read-only dir COPY --from=build --chown=caddy:caddy /app/dist /opt/app/www ## ensure protocol map is present as a static asset as well diff --git a/frontend/goquery-ui/Makefile b/frontend/goquery-ui/Makefile index 10ad9ad2..7b79e38a 100644 --- a/frontend/goquery-ui/Makefile +++ b/frontend/goquery-ui/Makefile @@ -99,15 +99,21 @@ format: install docker-dev: @echo "Starting Docker development server with hot reload..." @echo "Access the application at http://localhost:5173" - docker compose up --build + docker compose --profile dev up --build + +.PHONY: docker-prod +docker-prod: + @echo "Starting hardened prod-like Caddy image (mirrors the Helm chart securityContext)..." + @echo "Access the application at http://localhost:8080" + docker compose --profile prod up --build .PHONY: docker-stop docker-stop: - docker compose down + docker compose --profile dev --profile prod down .PHONY: docker-logs docker-logs: - docker compose logs -f + docker compose --profile dev --profile prod logs -f .PHONY: docker-build docker-build: diff --git a/frontend/goquery-ui/docker-compose.yml b/frontend/goquery-ui/docker-compose.yml index 5bc80275..927e0074 100644 --- a/frontend/goquery-ui/docker-compose.yml +++ b/frontend/goquery-ui/docker-compose.yml @@ -1,12 +1,18 @@ -# Local development only — hot reload with webpack dev server. -# Production builds use the self-contained Dockerfile directly. +# Two profiles: +# dev — hot reload via webpack-dev-server (Dockerfile.dev) +# prod — production Caddy image (Dockerfile), hardened to mirror the +# goquery-ui Helm chart's securityContext so deployment-time issues +# (e.g. the Caddy file-capability / no-new-privileges EPERM) surface +# locally instead of in the cluster. # # Usage: -# docker compose up (or: make docker-dev) -# docker compose down +# docker compose --profile dev up (or: make docker-dev) +# docker compose --profile prod up --build (or: make docker-prod) +# docker compose --profile prod down services: dev: + profiles: ["dev"] build: context: . dockerfile: Dockerfile.dev @@ -20,3 +26,56 @@ services: volumes: - ./:/app:delegated - /app/node_modules + + prod: + profiles: ["prod"] + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:5137" + # Runtime config — override via an optional .env next to this file. + environment: + GQ_API_BASE_URL: "${GQ_API_BASE_URL:-}" + HOST_RESOLVER_TYPES: "${HOST_RESOLVER_TYPES:-string}" + SSE_ON_LOAD: "${SSE_ON_LOAD:-true}" + GQ_BACKEND_HOST: "${GQ_BACKEND_HOST:-}" + + # --- Hardening: 1:1 with charts/goquery-ui/values.yaml securityContext --- + # k8s runAsNonRoot/runAsUser/runAsGroup + user: "1000:1000" + # k8s readOnlyRootFilesystem: true + read_only: true + security_opt: + # k8s allowPrivilegeEscalation: false (sets no_new_privs) + - "no-new-privileges:true" + # k8s seccompProfile.type: RuntimeDefault has no compose line — Docker + # applies its default seccomp profile automatically (omitting it == default; + # only "seccomp:unconfined" would disable it). + cap_drop: + # k8s capabilities.drop: [ALL] + - ALL + # readOnlyRootFilesystem is on, so every path the entrypoint/Caddy writes + # to needs a writable mount — mirrors the chart's four emptyDir volumes. + # mode 01777 (sticky, world-writable) lets the non-root user (uid 1000) + # write: Docker mounts tmpfs over the image's pre-created dirs as root:755, + # and there is no docker equivalent of the chart's fsGroup to fix ownership. + volumes: + - type: tmpfs + target: /var/run/env # generated env.js (served at /env.js) + tmpfs: { mode: 01777 } + - type: tmpfs + target: /tmp # entrypoint scratch (env.js template) + tmpfs: { mode: 01777 } + - type: tmpfs + target: /data # Caddy data dir + tmpfs: { mode: 01777 } + - type: tmpfs + target: /config # Caddy config dir + tmpfs: { mode: 01777 } + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:5137/"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s From 0d6ac1ebdc56ca72456ec6f17b40edc09915b08c Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 10:18:11 +0200 Subject: [PATCH 2/5] env --- frontend/goquery-ui/.env.development | 2 +- frontend/goquery-ui/.gitignore | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/goquery-ui/.env.development b/frontend/goquery-ui/.env.development index 4b1b3913..1da10333 100644 --- a/frontend/goquery-ui/.env.development +++ b/frontend/goquery-ui/.env.development @@ -1,3 +1,3 @@ GQ_API_BASE_URL=http://localhost:8145 -HOST_RESOLVER_TYPES=gethosts +HOST_RESOLVER_TYPES=strings SSE_ON_LOAD=true diff --git a/frontend/goquery-ui/.gitignore b/frontend/goquery-ui/.gitignore index bbdd6abf..7ed8c725 100644 --- a/frontend/goquery-ui/.gitignore +++ b/frontend/goquery-ui/.gitignore @@ -4,3 +4,8 @@ package-lock.json # deps and distribution node_modules/ dist/ + +# local runtime override for the prod compose profile (host-specific); +# .env.development is tracked intentionally and must stay so. +.env +!.env.development From a451ad33c744edfcb66c7e7bd0c22e815c256b6c Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 11:01:15 +0200 Subject: [PATCH 3/5] [bugfix] Frontend: collapse compose to single hardened service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review: drop the dev/prod compose profiles — they overcomplicate use. docker-compose now has one default service: the production Caddy image, hardened 1:1 with the Helm chart's securityContext. Local hot-reload development is `npm run dev` directly (the old `dev` profile only wrapped that same command in a container on the same :5173). Update Makefile (docker-prod/-stop/-logs use the default service; drop docker-dev) and README accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/goquery-ui/Makefile | 20 ++++++--------- frontend/goquery-ui/README.md | 25 ++++++------------- frontend/goquery-ui/docker-compose.yml | 34 ++++++-------------------- 3 files changed, 22 insertions(+), 57 deletions(-) diff --git a/frontend/goquery-ui/Makefile b/frontend/goquery-ui/Makefile index 7b79e38a..b5d933bc 100644 --- a/frontend/goquery-ui/Makefile +++ b/frontend/goquery-ui/Makefile @@ -29,9 +29,9 @@ help: @echo " make generate-index - create a minimal index.html if missing" @echo " make types - fetch OpenAPI $(VERSION) and generate TS types ($(GENERATED_TYPES))" @echo " make regen - install + types + build" - @echo " make docker-dev - docker compose dev server (hot reload)" - @echo " make docker-stop - stop docker compose dev server" - @echo " make docker-logs - view docker compose dev logs" + @echo " make docker-prod - run hardened prod-like image via docker compose" + @echo " make docker-stop - stop the docker compose stack" + @echo " make docker-logs - view docker compose logs" @echo " make docker-build - build production image locally" # install: re-run if package.json or chosen lockfile changes @@ -81,7 +81,7 @@ serve: build .PHONY: dev dev: build - @echo "DEPRECATED: preferably use \"make docker-dev\"" + @echo "DEPRECATED: preferably use \"npm run dev\" (hot reload on :5173)" @echo "Run these in two terminals for live development:" \ "\n Terminal 1: make watch" \ "\n Terminal 2: make serve" \ @@ -95,25 +95,19 @@ clean: format: install $(NPM) exec -- prettier --write "src/**/*.{js,jsx,ts,tsx}" -.PHONY: docker-dev -docker-dev: - @echo "Starting Docker development server with hot reload..." - @echo "Access the application at http://localhost:5173" - docker compose --profile dev up --build - .PHONY: docker-prod docker-prod: @echo "Starting hardened prod-like Caddy image (mirrors the Helm chart securityContext)..." @echo "Access the application at http://localhost:8080" - docker compose --profile prod up --build + docker compose up --build .PHONY: docker-stop docker-stop: - docker compose --profile dev --profile prod down + docker compose down .PHONY: docker-logs docker-logs: - docker compose --profile dev --profile prod logs -f + docker compose logs -f .PHONY: docker-build docker-build: diff --git a/frontend/goquery-ui/README.md b/frontend/goquery-ui/README.md index 3a435b9f..5db51ce0 100644 --- a/frontend/goquery-ui/README.md +++ b/frontend/goquery-ui/README.md @@ -15,25 +15,13 @@ npm ci ## Develop -Run with Docker Compose from this folder. - -### Dev (hot reload via webpack-dev-server) - -```bash -docker compose --profile dev up -``` - -Open - -### Without Docker - -Start webpack in watch mode and open `index.html` in a local static server (or your browser directly): +Hot reload via webpack-dev-server: ```bash npm run dev ``` -The bundle is emitted to `dist/` and `index.html` loads it. +Open ## Build @@ -45,8 +33,12 @@ npm run build ### Prod-like (Caddy serves built SPA with runtime env.js) +Runs the production image hardened to mirror the Helm chart's `securityContext` +(read-only rootfs, non-root, no-new-privileges, all caps dropped), so +deployment-time issues surface locally: + ```bash -docker compose --profile prod up --build +docker compose up --build ``` Open @@ -62,8 +54,7 @@ SSE_ON_LOAD=true Makefile shortcuts: ```bash -make docker-dev # same as compose dev profile -make docker-prod # same as compose prod profile +make docker-prod # run hardened prod-like image (docker compose up --build) make docker-build # build Caddy image locally ``` diff --git a/frontend/goquery-ui/docker-compose.yml b/frontend/goquery-ui/docker-compose.yml index 927e0074..3165f733 100644 --- a/frontend/goquery-ui/docker-compose.yml +++ b/frontend/goquery-ui/docker-compose.yml @@ -1,34 +1,14 @@ -# Two profiles: -# dev — hot reload via webpack-dev-server (Dockerfile.dev) -# prod — production Caddy image (Dockerfile), hardened to mirror the -# goquery-ui Helm chart's securityContext so deployment-time issues -# (e.g. the Caddy file-capability / no-new-privileges EPERM) surface -# locally instead of in the cluster. +# Production Caddy image (Dockerfile), hardened to mirror the goquery-ui Helm +# chart's securityContext so deployment-time issues (e.g. the Caddy +# file-capability / no-new-privileges EPERM) surface locally instead of in the +# cluster. Local hot-reload development is `npm run dev` (no container needed). # # Usage: -# docker compose --profile dev up (or: make docker-dev) -# docker compose --profile prod up --build (or: make docker-prod) -# docker compose --profile prod down +# docker compose up --build (or: make docker-prod) +# docker compose down services: - dev: - profiles: ["dev"] - build: - context: . - dockerfile: Dockerfile.dev - command: npm run dev - ports: - - "5173:5173" - env_file: - - .env.development - environment: - NODE_ENV: development - volumes: - - ./:/app:delegated - - /app/node_modules - - prod: - profiles: ["prod"] + goquery-ui: build: context: . dockerfile: Dockerfile From 0abba09760a64ed14c6991758ea6ced303a2a5f9 Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 11:03:44 +0200 Subject: [PATCH 4/5] [trivial] Frontend: remove orphaned dev container files Dockerfile.dev and .env.development were only used by the docker-compose dev profile, which was dropped in favour of `npm run dev`. Remove them and the now-moot .env.development gitignore exception (.env stays ignored for the prod compose's host-specific runtime overrides). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/goquery-ui/.env.development | 3 --- frontend/goquery-ui/.gitignore | 4 +--- frontend/goquery-ui/Dockerfile.dev | 7 ------- 3 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 frontend/goquery-ui/.env.development delete mode 100644 frontend/goquery-ui/Dockerfile.dev diff --git a/frontend/goquery-ui/.env.development b/frontend/goquery-ui/.env.development deleted file mode 100644 index 1da10333..00000000 --- a/frontend/goquery-ui/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -GQ_API_BASE_URL=http://localhost:8145 -HOST_RESOLVER_TYPES=strings -SSE_ON_LOAD=true diff --git a/frontend/goquery-ui/.gitignore b/frontend/goquery-ui/.gitignore index 7ed8c725..16a1305e 100644 --- a/frontend/goquery-ui/.gitignore +++ b/frontend/goquery-ui/.gitignore @@ -5,7 +5,5 @@ package-lock.json node_modules/ dist/ -# local runtime override for the prod compose profile (host-specific); -# .env.development is tracked intentionally and must stay so. +# local runtime override for the prod compose (host-specific) .env -!.env.development diff --git a/frontend/goquery-ui/Dockerfile.dev b/frontend/goquery-ui/Dockerfile.dev deleted file mode 100644 index 2c6b40d2..00000000 --- a/frontend/goquery-ui/Dockerfile.dev +++ /dev/null @@ -1,7 +0,0 @@ -# Dev image: pre-installs node_modules so `docker compose up` doesn't -# re-run npm ci on every start. Source is mounted at runtime. -FROM node:22-alpine -WORKDIR /app -COPY package.json package-lock.json* ./ -RUN npm ci --no-audit --no-fund -# Entrypoint is supplied by docker-compose (npm run dev) From ad550f9923ebadef7b52ad1aac5344a269a10df6 Mon Sep 17 00:00:00 2001 From: Lennart Elsen Date: Wed, 17 Jun 2026 11:47:01 +0200 Subject: [PATCH 5/5] [trivial] Frontend: rename docker-prod target to docker-up Address review: with a single compose service there's no prod/dev split, so the `-prod` suffix is redundant. Rename `make docker-prod` -> `docker-up` (consistent with docker-stop/-logs/-build) and update the compose/README references. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/goquery-ui/Makefile | 8 ++++---- frontend/goquery-ui/README.md | 2 +- frontend/goquery-ui/docker-compose.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/goquery-ui/Makefile b/frontend/goquery-ui/Makefile index b5d933bc..27b2e749 100644 --- a/frontend/goquery-ui/Makefile +++ b/frontend/goquery-ui/Makefile @@ -29,7 +29,7 @@ help: @echo " make generate-index - create a minimal index.html if missing" @echo " make types - fetch OpenAPI $(VERSION) and generate TS types ($(GENERATED_TYPES))" @echo " make regen - install + types + build" - @echo " make docker-prod - run hardened prod-like image via docker compose" + @echo " make docker-up - run the hardened image via docker compose" @echo " make docker-stop - stop the docker compose stack" @echo " make docker-logs - view docker compose logs" @echo " make docker-build - build production image locally" @@ -95,9 +95,9 @@ clean: format: install $(NPM) exec -- prettier --write "src/**/*.{js,jsx,ts,tsx}" -.PHONY: docker-prod -docker-prod: - @echo "Starting hardened prod-like Caddy image (mirrors the Helm chart securityContext)..." +.PHONY: docker-up +docker-up: + @echo "Starting the hardened Caddy image (mirrors the Helm chart securityContext)..." @echo "Access the application at http://localhost:8080" docker compose up --build diff --git a/frontend/goquery-ui/README.md b/frontend/goquery-ui/README.md index 5db51ce0..079b1555 100644 --- a/frontend/goquery-ui/README.md +++ b/frontend/goquery-ui/README.md @@ -54,7 +54,7 @@ SSE_ON_LOAD=true Makefile shortcuts: ```bash -make docker-prod # run hardened prod-like image (docker compose up --build) +make docker-up # run the hardened image (docker compose up --build) make docker-build # build Caddy image locally ``` diff --git a/frontend/goquery-ui/docker-compose.yml b/frontend/goquery-ui/docker-compose.yml index 3165f733..7da75e84 100644 --- a/frontend/goquery-ui/docker-compose.yml +++ b/frontend/goquery-ui/docker-compose.yml @@ -4,7 +4,7 @@ # cluster. Local hot-reload development is `npm run dev` (no container needed). # # Usage: -# docker compose up --build (or: make docker-prod) +# docker compose up --build (or: make docker-up) # docker compose down services: