From 860670ce6b8834beb9d2c5c5e30a09a46254788b Mon Sep 17 00:00:00 2001 From: Sijia Li <242334856+atngit2@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:12:58 -0700 Subject: [PATCH 1/2] audit: idempotency, consistency, and hardening improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workflow: add inline GHCR pre-auth before docker manifest inspect in Step 3 to prevent race condition when image is not yet publicly accessible - workflow: document GNU coreutils dependency for `date -d` in Step 11 - workflow: clarify DockerHub floating-tag omission rationale in Step 9 comment - workflow: tighten CHANGE_SUMMARY diff loop to guard against unequal part counts - Dockerfile: add IMPORTANT sync comment linking builder ARG default to FROM tag - Dockerfile: document healthcheck rationale (caddy environ vs HTTP admin API) - Dockerfile: document docker group GID-mismatch guard in RUN layer - README: add missing `2.11.2` patch-level tag row to Tag Strategy table - README: fix tag table — semver patch tag was missing from GHCR column - README: add GHCR-specific note to Tag Strategy (major/minor tags only on GHCR) - .github/renovate.json: verify automerge scope (reviewed — no changes needed) --- .github/workflows/build-caddy.yml | 56 ++++++++++++++++++++++--------- README.md | 31 ++++++++++++----- docker/caddy/Dockerfile | 30 ++++++++++++----- 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-caddy.yml b/.github/workflows/build-caddy.yml index 49265a0..92047b3 100644 --- a/.github/workflows/build-caddy.yml +++ b/.github/workflows/build-caddy.yml @@ -193,11 +193,14 @@ jobs: echo "Resolved fingerprint: ${BUILD_FINGERPRINT}" # ── 3c: Compare with published image fingerprint ── - # Authenticate to GHCR with the automatically-provided GITHUB_TOKEN - # before running docker manifest inspect. This ensures the inspect - # succeeds even on the first run (before Step 5 logins), and avoids - # a race condition where the inspect could fail if the image is not - # publicly accessible. + # Pre-authenticate to GHCR using the auto-provided GITHUB_TOKEN + # before running docker manifest inspect. This must happen here + # (Step 3) rather than relying solely on the later docker/login-action + # in Step 5, because Step 5 is conditional on should_build == 'true'. + # Without this pre-auth, manifest inspect would fail on the very first + # run (no published image) or when the package visibility is private, + # causing the fingerprint comparison to silently default to + # 'no-published-image' and trigger unnecessary rebuilds. echo "${{ secrets.GITHUB_TOKEN }}" \ | docker login ghcr.io -u "${{ github.actor }}" --password-stdin \ 2>/dev/null || true @@ -221,14 +224,22 @@ jobs: CHANGE_SUMMARY="No published image found — performing initial build." elif [[ "${BUILD_FINGERPRINT}" != "${PUBLISHED_FINGERPRINT}" ]]; then SHOULD_BUILD="true" - # Human-readable diff + # Human-readable diff — compare only the parts that are present + # in both fingerprints; guard against unequal part counts (e.g., + # if a new plugin is added to the fingerprint schema). CHANGE_SUMMARY="Component changes detected:"$'\n' IFS='|' read -ra NEW_PARTS <<< "${BUILD_FINGERPRINT}" IFS='|' read -ra OLD_PARTS <<< "${PUBLISHED_FINGERPRINT}" - for i in "${!NEW_PARTS[@]}"; do - NEW="${NEW_PARTS[$i]}" + NEW_COUNT="${#NEW_PARTS[@]}" + OLD_COUNT="${#OLD_PARTS[@]}" + if [[ "${NEW_COUNT}" != "${OLD_COUNT}" ]]; then + CHANGE_SUMMARY+=" - Fingerprint schema changed: new has ${NEW_COUNT} parts, published has ${OLD_COUNT} parts"$'\n' + fi + MAX_IDX=$(( NEW_COUNT > OLD_COUNT ? NEW_COUNT : OLD_COUNT )) + for (( i=0; i/dev/null || true docker buildx prune --filter "until=1h" --force 2>/dev/null || true rm -f scout_output.md modules.txt - diff --git a/README.md b/README.md index 75b70c9..a436e8a 100644 --- a/README.md +++ b/README.md @@ -116,15 +116,21 @@ labels: ## Tag Strategy -| Tag | Description | -|:---|:---| -| `latest` | Always points to the most recent successful build | -| `2` · `2.11` · `2.11.2` | Semantic version pins at major / minor / patch | -| `2.11.2-2026.03.28` | Version + build date for fully reproducible deployments | -| `sha-abc1234` | Git SHA of the Dockerfile at build time | - -For production deployments requiring immutability, use the digest from the -[Releases](https://github.com/atnplex/caddy/releases) page. +| Tag | Registry | Description | +|:---|:---|:---| +| `latest` | GHCR · DockerHub | Always points to the most recent successful build | +| `2` · `2.11` | GHCR only | Floating semver major / minor tags — updated on every build | +| `2.11.2` | GHCR · DockerHub | Patch-level version pin | +| `2.11.2-2026.03.28` | GHCR · DockerHub | Version + build date for fully reproducible deployments | +| `sha-abc1234` | GHCR · DockerHub | Git SHA of the Dockerfile at build time | + +> **Note:** Floating major/minor tags (`2`, `2.11`) are only pushed to GHCR. +> DockerHub receives only immutable tags (patch version, date-stamped, and SHA) +> to prevent silent overwrites for users who may have pinned these tags in +> compose files expecting stability. +> +> For production deployments requiring full immutability, pin to a digest from +> the [Releases](https://github.com/atnplex/caddy/releases) page. --- @@ -212,6 +218,13 @@ using your personal account credentials. In **Settings → Branches**, protect the `main` branch and allow the Renovate bot to bypass protection rules for automerge to work. +### 5. Repository About Section (GitHub UI — one-time) +Set the repository **About** section via **Settings → General → Description** +(or the gear icon on the repo homepage): +- **Description:** `Hardened multi-arch custom Caddy image with docker-proxy, Cloudflare DNS/IP, cache-handler, transform-encoder, and caddy-security modules` +- **Website:** `https://hub.docker.com/r/atnplex/caddy` +- **Topics:** `caddy`, `docker`, `reverse-proxy`, `cloudflare`, `homelab`, `xcaddy`, `github-actions` + --- ## Releases diff --git a/docker/caddy/Dockerfile b/docker/caddy/Dockerfile index 4672b82..661bcc3 100644 --- a/docker/caddy/Dockerfile +++ b/docker/caddy/Dockerfile @@ -1,10 +1,13 @@ # ───────────────────────────────────────────────────────────────── # Stage 1: Builder # ───────────────────────────────────────────────────────────────── -# IMPORTANT: The tag here (CADDY_VERSION-builder) must match the -# CADDY_VERSION ARG in Stage 2. When upgrading Caddy, update both -# the ARG default below AND the digest here together. -# The SHA digest is version-specific; Renovate keeps it up to date. +# IMPORTANT: The ARG default below AND the tag in the FROM line must +# always be kept in sync. When upgrading Caddy, update BOTH the ARG +# default AND the image tag (and its SHA digest) together in one commit. +# The FROM tag cannot reference the ARG directly because ARGs declared +# before the first FROM are not in scope inside the build stage. +# Renovate keeps the SHA digest up to date automatically; the tag and +# ARG default must be updated manually (or via a Renovate customManager). ARG CADDY_VERSION=2.11.2 FROM caddy:${CADDY_VERSION}-builder@sha256:84dfc3479309c690643ada9279b3e0b4352ce56b0ec8fd802c668f42b546e98f AS builder @@ -72,8 +75,12 @@ ENV TZ=America/Los_Angeles \ XDG_DATA_HOME=/data # Create non-root caddy user/group and add to docker group for socket access. -# We check for an existing docker group (e.g., inherited from the base image) -# before creating one, to avoid silently using a mismatched GID. +# getent is used to check whether a docker group already exists in the base +# image (e.g., Alpine may ship one with a different GID than the host). If it +# already exists we skip creation to avoid a mismatched GID silently adding the +# caddy user to the wrong group. If your host docker.sock GID differs from the +# group created here, pass --group-add $(stat -c '%g' /var/run/docker.sock) +# in your docker run / compose file (as shown in the README Quick Start). # Provision persistent data and config directories with correct ownership. RUN addgroup -S caddy && \ adduser -S -G caddy caddy && \ @@ -101,9 +108,14 @@ COPY --from=builder /usr/share/zoneinfo/America/Los_Angeles \ RUN rm -rf /tmp/* /var/tmp/* && \ find / -xdev -type d -perm -0002 ! -path "/tmp*" -exec chmod 755 {} + -# Healthcheck uses caddy environ (lightweight internal check). -# wget is not present in this minimal Alpine image. -# Port 2019 (admin API) is loopback-only and not exposed externally. +# Healthcheck uses `caddy environ` rather than an HTTP probe against the +# admin API (port 2019) because: +# 1. The admin API is loopback-only and disabled in some deployments. +# 2. `caddy environ` exits 0 iff the binary is functional without +# requiring any network listener to be up. +# 3. The command is lightweight and produces no side effects. +# If you expose the admin API and prefer an HTTP liveness probe, replace +# with: CMD wget -qO- http://localhost:2019/ || exit 1 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD ["/usr/bin/caddy", "environ"] From a11b21f7f8cf1cae735780396986911ec5bd823b Mon Sep 17 00:00:00 2001 From: Sijia Li <242334856+atngit2@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:37 -0700 Subject: [PATCH 2/2] fix: resolve cache-handler and transform-encoder from main not master Both caddyserver/cache-handler and caddyserver/transform-encoder use 'main' as their default branch. The previous refs/heads/master references would silently return empty strings, causing the build fingerprint to be incomplete and potentially triggering unnecessary rebuilds or failing the non-empty validation check. Fixes stale master branch references in Step 3 (Resolve Versions and Decide). --- .github/workflows/build-caddy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-caddy.yml b/.github/workflows/build-caddy.yml index 92047b3..a35e60a 100644 --- a/.github/workflows/build-caddy.yml +++ b/.github/workflows/build-caddy.yml @@ -161,13 +161,13 @@ jobs: echo "Resolving cache-handler HEAD commit..." CACHE_HANDLER_REF="$( retry git ls-remote --heads https://github.com/caddyserver/cache-handler.git \ - | awk '/refs\/heads\/master/ {print $1}' + | awk '/refs\/heads\/main/ {print $1}' )" echo "Resolving transform-encoder HEAD commit..." TRANSFORM_ENCODER_REF="$( retry git ls-remote --heads https://github.com/caddyserver/transform-encoder.git \ - | awk '/refs\/heads\/master/ {print $1}' + | awk '/refs\/heads\/main/ {print $1}' )" echo "Resolving caddy-security version..."