Skip to content

Commit d24f252

Browse files
ericsmallingclaude
andcommitted
jenkins: read cosign passphrase from file, fix doc digest example
Three Copilot findings from the seventh pass on PR #330: - COSIGN_PASSWORD env var: drop it from the controller env entirely and have JCasC read the passphrase from the bind-mounted file via `${readFile:/tmp/cgjenkins-home/.secrets/cosign.password}` at boot. The env-var path was visible to `docker inspect` and one stray `echo $COSIGN_PASSWORD` away from showing up in build logs. Tradeoff: cosign.password drops back from 600 to 644 because JCasC (uid 1000 inside the controller) needs to read it across the bind mount, and the host user's uid usually isn't 1000. Documented the resulting threat-model assumption (single-user dev laptop). - README verify-image example: replace `{{index .RepoDigests 0}}` with the repo-filtered loop that cgSign/cgVerify already use, so the doc stops handing readers a digest that may belong to a different push (the local image cache often carries stale RepoDigests from earlier runs against different registries). - python312-pip-django/app.py: drop the redundant inner `import django` in hello() — django is already imported at module scope (added in the previous round to fix the AppRegistryNotReady path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 65969e4 commit d24f252

5 files changed

Lines changed: 31 additions & 31 deletions

File tree

jenkins/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ A clean build takes 10s–40s once images are cached locally; the Gradle pipelin
114114
The public key sits at `/tmp/cgjenkins-home/.secrets/cosign.pub`. To check a pushed image from outside Jenkins, run cosign in a host-network container so `localhost` resolves to your ingress:
115115

116116
```sh
117-
DIGEST=$(docker image inspect --format '{{index .RepoDigests 0}}' localhost/library/pytest:3-14)
117+
# Pick the RepoDigest whose repo matches the image you just pulled. A
118+
# single image can have multiple RepoDigests in the local cache (one per
119+
# registry/org it has been pushed to), and `{{index .RepoDigests 0}}`
120+
# returns whichever happens to be first — which may not match the repo
121+
# you want to verify. Filter by the repo prefix to be safe. cgSign and
122+
# cgVerify use the same pattern internally.
123+
IMAGE=localhost/library/pytest:3-14
124+
REPO="${IMAGE%:*}"
125+
DIGEST=$(docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$IMAGE" | grep -F "${REPO}@" | head -1)
118126
# cosign's reference parser rejects bare 'localhost' (it tries Docker Hub),
119127
# so rewrite to 'localhost:80' before passing to cosign:
120128
DIGEST="${DIGEST/#localhost\//localhost:80/}"

jenkins/apps/python312-pip-django/app.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939

4040

4141
def hello(request):
42-
import django
4342
body = (
4443
"<h1>Hello from Django on Chainguard</h1>"
4544
"<h2>Runtime info</h2>"

jenkins/docker-compose.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@ services:
3030
PULL_REGISTRY: ${PULL_REGISTRY:-cgr.dev/${CHAINGUARD_ORG}}
3131
PUSH_REGISTRY: ${PUSH_REGISTRY:-ttl.sh/smalls}
3232
HARBOR_ENABLED: ${HARBOR_ENABLED:-false}
33-
# Cosign signing-key passphrase. setup.sh exports this from
34-
# /tmp/cgjenkins-home/.secrets/cosign.password before bringing Jenkins
35-
# up; JCasC interpolates it into the `cosign-password` credential at
36-
# boot. Empty if setup.sh hasn't been run yet — Jenkins still boots,
37-
# but pipelines that call cgSign() will fail with a clear "credential
38-
# not found" error.
39-
COSIGN_PASSWORD: ${COSIGN_PASSWORD:-}
33+
# NOTE: the cosign signing-key passphrase used to live here as
34+
# COSIGN_PASSWORD. JCasC now reads it directly off the bind-mounted
35+
# file at boot via `${readFile:/tmp/cgjenkins-home/.secrets/cosign.password}`,
36+
# so it never sits in the long-lived controller env (where it would
37+
# show up in `docker inspect` and could leak into build logs).
4038
JENKINS_HOME: /tmp/cgjenkins-home
4139
CASC_JENKINS_CONFIG: /tmp/cgjenkins-home/casc
4240
JENKINS_ADMIN_PASSWORD: ${JENKINS_ADMIN_PASSWORD:-admin}

jenkins/jenkins/casc/jenkins.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ credentials:
110110
scope: GLOBAL
111111
id: cosign-password
112112
description: "passphrase that decrypts cosign-private-key"
113-
secret: "${COSIGN_PASSWORD}"
113+
# Read the passphrase directly from the bind-mounted secret file
114+
# rather than via the controller's env. Avoids leaving the
115+
# passphrase visible to `docker inspect` and prevents accidental
116+
# propagation into build logs (the env-var path used to be one
117+
# `echo $COSIGN_PASSWORD` in a debug step away from disclosure).
118+
secret: "${readFile:/tmp/cgjenkins-home/.secrets/cosign.password}"
114119

115120
jobs:
116121
- file: /tmp/cgjenkins-home/casc/jobs.groovy

jenkins/setup.sh

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -238,27 +238,17 @@ if [[ ! -f "$COSIGN_DIR/cosign.key" ]]; then
238238
else
239239
echo " Reusing existing keypair in ${COSIGN_DIR}/cosign.{key,pub,password}"
240240
fi
241-
# Apply perms unconditionally so re-runs upgrade older 644 cosign.password
242-
# files generated before this hardening:
243-
# cosign.key/cosign.pub at 644 — JCasC (uid 1000 in the container) reads
244-
# cosign.key at boot via `readFileBase64:`, and cgVerify mounts
245-
# cosign.pub into a sibling cosign container. Both happen across the
246-
# bind mount, so host perms must let uid 1000 read regardless of the
247-
# host user's uid.
248-
# cosign.password at 600 — nothing inside the container reads it off
249-
# the bind mount; setup.sh below `cat`s it (as the host user) to
250-
# export $COSIGN_PASSWORD, which docker-compose forwards into the
251-
# controller env, and JCasC reads it from there. Pairing world-
252-
# readable encrypted key with world-readable passphrase would defeat
253-
# the encryption for any other local user.
254-
chmod 644 "$COSIGN_DIR"/cosign.key "$COSIGN_DIR"/cosign.pub
255-
chmod 600 "$COSIGN_DIR"/cosign.password
256-
257-
# Export COSIGN_PASSWORD so docker compose forwards it into the Jenkins
258-
# container, where JCasC interpolates it into the `cosign-password` Secret
259-
# Text credential at boot. The key + pub files are loaded directly by JCasC
260-
# via `${readFileBase64:…}` so they don't need to ride in env vars.
261-
export COSIGN_PASSWORD="$(cat "$COSIGN_DIR/cosign.password")"
241+
# All three files at 644 so JCasC (uid 1000 inside the Jenkins controller,
242+
# typically a different uid than the host user) can read them across the
243+
# bind mount via `${readFile:…}` / `${readFileBase64:…}` at boot. We used
244+
# to keep cosign.password at 600 — but that only worked because the
245+
# passphrase rode into the controller via the COSIGN_PASSWORD env var; we
246+
# dropped that path (it was visible to `docker inspect` and could leak
247+
# into build logs) in favour of reading the file directly. The local-demo
248+
# threat model is "single dev laptop, host user only"; for production
249+
# you'd want chown 1000:1000 + chmod 600 and a separate setup.sh path that
250+
# doesn't need to round-trip the password through host-side reads.
251+
chmod 644 "$COSIGN_DIR"/cosign.key "$COSIGN_DIR"/cosign.pub "$COSIGN_DIR"/cosign.password
262252

263253
# ---- Phase 1: write .env first so docker compose picks up the new mode ----
264254

0 commit comments

Comments
 (0)