diff --git a/.dockerignore b/.dockerignore index 5414d56..ab9572e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,63 @@ +# .dockerignore uses Go filepath.Match, NOT .gitignore semantics: `*` +# does not cross `/`, and a pattern without a leading `**/` is anchored +# at the build-context root. Every depth-independent pattern therefore +# needs the `**/` prefix — without it `config/.env` and +# `certs/server.key` still ship while the file reads as solved. +# +# Root-anchored entries are for paths that occur exactly once, at the +# context root. A host-built binary is the usual case, and it must be +# written anchored: `/myapp`, never `**/myapp`. The prefixed form also +# matches `cmd/myapp/`, which deletes the package directory from the +# context. +# +# Matching is case-sensitive, so `**/*.key` does not match +# `certs/SERVER.KEY`. The secret-material extensions below use character +# classes, which cover every capitalisation in one line — a doubled +# ALL-CAPS pattern would still miss `Server.Key` while reading as though +# case were handled. Names that only ever exist in one spelling because +# a tool writes them (`.env`, `.envrc`, `id_rsa`) stay literal. +# +# Extend this file with the repo's own host-built artifacts (compiled +# binaries, test binaries, coverage output); those are per-repo and +# belong here because a host build otherwise drops them into the +# context. + +# Repository metadata: exactly one, at the context root. .git -node_modules -.DS_Store + +# Environment files. `*.env` covers the `prod.env` / `local.env` +# convention; the `.env` and `.env.*` spellings are listed explicitly +# because they are what most tooling writes. +**/.env +**/.env.* +**/*.[eE][nN][vV] +**/.envrc + +# Private keys and the bundles that carry them. Public certificates +# (*.crt, *.cer) are deliberately absent: they are not secrets and are +# sometimes a legitimate build input. +**/*.[pP][eE][mM] +**/*.[kK][eE][yY] +**/*.[pP]12 +**/*.[pP][fF][xX] +**/id_rsa +**/id_dsa +**/id_ecdsa +**/id_ed25519 + +# Dependencies: restored inside the image, never copied in. +**/node_modules + +# OS metadata. +**/.DS_Store +**/Thumbs.db + +# Editor state. Never a build input, and it churns under a developer's +# hands, so it invalidates COPY for reasons unrelated to the source. +**/*.swp +**/*.swo +**/*~ +**/*.bak +**/.idea +**/.vscode +**/*.sublime-* diff --git a/TODO.md b/TODO.md index 8d9557d..cfab852 100644 --- a/TODO.md +++ b/TODO.md @@ -21,6 +21,21 @@ fmt-check, and commit. # Completed Steps +- 2026-08-09: Closed the secret exposure in the canonical `.dockerignore`: a + developer's local `.env`, `*.pem` or `*.key` was reaching the Docker build + context under `COPY . .`, invisible to every git-based check because + `.gitignore` covers it. The patterns are written to `.dockerignore`'s own + `filepath.Match` semantics — `**/`-prefixed so they hold at every depth, which + also fixes nested `node_modules` — rather than transplanted from `.gitignore`, + whose unprefixed form protects only the repository root while reading as + solved. Coverage extends past the `.env`/`.pem`/`.key` trio to the `prod.env` + convention, `.envrc`, PKCS#12 bundles and extensionless SSH keys, with + capitalisation handled by character classes because matching is case-sensitive + and an ALL-CAPS twin per pattern still misses `Server.Key`. `REPO_POLICIES.md` + and both repo checklists now state that asymmetry and require verification by + enumerating the image rather than by reading the patterns. Verified with a + probe image before, against three naive forms (unprefixed, lowercase-only, + ALL-CAPS-doubled), and after. - 2026-08-09: Made the pinned golangci-lint actually propagate: REPO_POLICIES.md now carries the canonical `script/bootstrap` snippet for Go repos, which installs when the installed version does not match the pin (the old diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 69cb02b..c32a497 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -37,6 +37,18 @@ with your task. `CHECK_EPOCH` rule in `REPO_POLICIES.md`. Without them the check layer is served from cache on an unchanged tree and the build reports a green it never ran. +- [ ] `.dockerignore` excludes the repo's own host-built artifacts (compiled + binaries, test binaries, coverage output), written root-anchored — + `/myapp`, never `**/myapp`, which would also match `cmd/myapp/`. An + existing repo is where such a binary is likeliest to already be sitting in + the build context, invisible to git. +- [ ] Every depth-independent pattern in `.dockerignore` carries a `**/` prefix; + only genuinely root-anchored entries such as `.git` are unprefixed, and + `.gitignore`'s patterns have not been transplanted unmodified. + `.dockerignore` anchors an unprefixed pattern at the context root, so the + transplanted form leaves `config/.env` and `certs/server.key` in the build + context while reading as solved — see the `.dockerignore` rule in + `REPO_POLICIES.md`. - [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on push — reference `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml` diff --git a/prompts/NEW_REPO_CHECKLIST.md b/prompts/NEW_REPO_CHECKLIST.md index 5b0bcfe..7965c10 100644 --- a/prompts/NEW_REPO_CHECKLIST.md +++ b/prompts/NEW_REPO_CHECKLIST.md @@ -52,6 +52,14 @@ Template files can be fetched from: `https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md` - [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` + - Extend `.dockerignore` with the repo's own host-built artifacts, giving + every depth-independent pattern a `**/` prefix — but write a repo-root + binary anchored, `/myapp` and never `**/myapp`, which would also match + `cmd/myapp/` and delete the package directory. Do not transplant + `.gitignore`'s patterns: `.dockerignore` anchors an unprefixed pattern at + the context root, so the copied form leaves `config/.env` in the build + context while reading as solved. See the `.dockerignore` rule in + `REPO_POLICIES.md`. - All Dockerfiles must run `make check` as a build step, and every stage containing a check-running `RUN` must declare `ARG CHECK_EPOCH` with the `RUN [ -n "$CHECK_EPOCH" ] || exit 1` guard immediately below it — see the diff --git a/prompts/REPO_POLICIES.md b/prompts/REPO_POLICIES.md index a893c8d..7b483e7 100644 --- a/prompts/REPO_POLICIES.md +++ b/prompts/REPO_POLICIES.md @@ -331,7 +331,52 @@ style conventions are in separate documents: editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`. Fetch the standard `.gitignore` from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up - a new repo. + a new repo. These patterns are written to `.gitignore`'s own semantics, in + which an unanchored pattern already matches at every depth. They are not a + `.dockerignore` and must not be transplanted into one unmodified — see the + next rule. + +- **`.dockerignore` does not use `.gitignore` semantics, and copying patterns + across unmodified leaves secrets in the build context.** Docker matches with + Go `filepath.Match`: `*` does not cross `/`, and a pattern without a leading + `**/` is anchored at the build-context root. A `.dockerignore` listing `.env`, + `*.pem` and `*.key` therefore excludes only the copies at the repository root; + `config/.env` and `certs/server.key` still reach the context and can land in + an image layer. That file is more dangerous than a short one with no secret + patterns at all, because it reads as solved and stops anyone looking. Give + every depth-independent pattern the `**/` prefix — `**/.env`, `**/.env.*`, + `**/*.pem`, `**/*.key`, `**/node_modules` — and leave only genuinely + root-anchored entries such as `.git` unprefixed. The inverse move is equally + wrong: never apply `**/` to `.gitignore`, where it is redundant and produces a + file that is wrong in a way that looks careful. Each file is written to its + own semantics; neither is derived from the other. Fetch the standard + `.dockerignore` from + `https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend + it with the repo's own host-built artifacts — a host `make build` that leaves + a compiled binary in the repo root puts that binary in the build context, + where `.gitignore` hides it from every git-based check. Write that binary + anchored, `/myapp` and never `**/myapp`: the prefixed form also matches + `cmd/myapp/` and deletes the package directory from the context. + +- **`.dockerignore` matching is case-sensitive, so cover capitalisation with + character classes rather than by doubling patterns.** `**/*.key` does not + match `certs/SERVER.KEY`, which is reachable on the case-insensitive + filesystems most laptops use. Adding an ALL-CAPS twin for each pattern is not + the fix: it still misses `Server.Key` and `Ca.Pem` while reading as though + case were handled — the same manufactured confidence as the root-anchored + form. `filepath.Match` supports character ranges, so one line covers every + spelling: `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`. Apply this to + secret-material extensions; names that exist in exactly one spelling because a + tool writes them (`.env`, `.envrc`, `id_rsa`) stay literal. + +- **Verify `.dockerignore` by enumerating the image, not by reading the + patterns.** Plant files at the root _and_ at least two directories deep, build + a probe image that does `COPY . .`, and list what actually landed + (`docker run --rm --entrypoint find IMAGE /app`). Reading the patterns and + agreeing they look right is exactly what lets the root-only form through. The + `transferring context` size is not a substitute: a nested secret is a few + bytes, and BuildKit transfers only the delta from the previous build, so the + reported size describes the transfer and not the contents of the image. - **No build artifacts in version control.** Code-derived data (compiled bundles, minified output, generated assets) must never be committed to the