Keep secrets out of the Docker build context at every depth (closes #29)
The canonical .dockerignore was three lines while the canonical Dockerfile does `COPY . .`, so a local .env, *.pem or *.key shipped into the build context and could land in an image layer, invisible to every git-based check. Copying .gitignore's patterns across is not the repair: .dockerignore anchors an unprefixed pattern at the context root, so that form protects only the repository root while reading as solved. Every depth-independent pattern here carries `**/`, and secret names are character ranges because matching is case-sensitive and an ALL-CAPS twin still misses `Server.Key`. Public certificates are deliberately left in as a legitimate build input. Verified by enumerating a probe image. Model: opus-5
This commit is contained in:
@@ -1,3 +1,49 @@
|
|||||||
|
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||||
|
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||||
|
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||||
|
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||||
|
# `certs/server.key` still ship while this file reads as solved. Only
|
||||||
|
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||||
|
# into .gitignore, where `**/` is wrong.
|
||||||
|
#
|
||||||
|
# Matching is case-sensitive, so secrets use character ranges rather
|
||||||
|
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||||
|
#
|
||||||
|
# Extend with this repo's own host-built artifacts, written anchored:
|
||||||
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
|
# deletes the package directory from the context.
|
||||||
.git
|
.git
|
||||||
node_modules
|
|
||||||
.DS_Store
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Re-include a committed template with a negation if the
|
||||||
|
# build needs one: `!docs/example.env`.
|
||||||
|
**/*.[eE][nN][vV]
|
||||||
|
**/.[eE][nN][vV].*
|
||||||
|
**/.[eE][nN][vV][rR][cC]
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them. Public certificates
|
||||||
|
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||||
|
**/*.[pP][eE][mM]
|
||||||
|
**/*.[kK][eE][yY]
|
||||||
|
**/*.[pP]12
|
||||||
|
**/*.[pP][fF][xX]
|
||||||
|
**/[iI][dD]_[rR][sS][aA]
|
||||||
|
**/[iI][dD]_[dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][dD]25519
|
||||||
|
|
||||||
|
# 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 COPY.
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
**/*~
|
||||||
|
**/*.bak
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/*.sublime-*
|
||||||
|
|||||||
6
TODO.md
6
TODO.md
@@ -21,6 +21,12 @@ fmt-check, and commit.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-09-08: Closed the secret exposure in the canonical `.dockerignore`: a
|
||||||
|
local `.env`, `*.pem` or `*.key` was reaching the build context under
|
||||||
|
`COPY . .`, invisible to every git-based check. The patterns are now written
|
||||||
|
to `.dockerignore`'s own semantics — `**/`-prefixed so they hold at every
|
||||||
|
depth, case-folded with character ranges — and `REPO_POLICIES.md` requires
|
||||||
|
verifying by enumerating the image rather than by reading the file.
|
||||||
- 2026-09-08: Made a pinned tool in `script/bootstrap` actually reach the host.
|
- 2026-09-08: Made a pinned tool in `script/bootstrap` actually reach the host.
|
||||||
`REPO_POLICIES.md` now requires comparing the installed version against the
|
`REPO_POLICIES.md` now requires comparing the installed version against the
|
||||||
pin rather than testing `PATH` presence, and re-resolving the binary through
|
pin rather than testing `PATH` presence, and re-resolving the binary through
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ with your task.
|
|||||||
build step, and `script/cibuild` and `script/docker` build it with
|
build step, and `script/cibuild` and `script/docker` build it with
|
||||||
`--no-cache` — fetch `.dockerignore` from
|
`--no-cache` — fetch `.dockerignore` from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
|
||||||
|
- [ ] 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 — the
|
||||||
|
transplanted form leaves `config/.env` and `certs/server.key` in the build
|
||||||
|
context while reading as solved
|
||||||
|
- [ ] `.dockerignore` excludes the repo's own host-built artifacts (compiled
|
||||||
|
binaries, test binaries, coverage output), written root-anchored —
|
||||||
|
`/myapp`, never `**/myapp`. An existing repo is where such a binary is
|
||||||
|
likeliest to already be sitting in the build context, invisible to git.
|
||||||
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
|
- [ ] Gitea Actions workflow in `.gitea/workflows/` runs `script/cibuild` on
|
||||||
push — reference
|
push — reference
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml`
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ Template files can be fetched from:
|
|||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md`
|
||||||
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
|
- [ ] `Dockerfile` and `.dockerignore` — fetch `.dockerignore` from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore`
|
`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.
|
||||||
- All Dockerfiles must run `make check` as a build step
|
- All Dockerfiles must run `make check` as a build step
|
||||||
- Server: also builds and runs the application
|
- Server: also builds and runs the application
|
||||||
- Non-server: brings up dev environment and runs `make check`
|
- Non-server: brings up dev environment and runs `make check`
|
||||||
@@ -108,7 +115,8 @@ are thin shims calling them. Model scripts:
|
|||||||
- [ ] `make docker` succeeds
|
- [ ] `make docker` succeeds
|
||||||
- [ ] `script/cibuild` succeeds and demonstrably executed the checks — a
|
- [ ] `script/cibuild` succeeds and demonstrably executed the checks — a
|
||||||
sub-second build, or `CACHED` on a check layer, means nothing ran
|
sub-second build, or `CACHED` on a check layer, means nothing ran
|
||||||
- [ ] No secrets in repo
|
- [ ] No secrets in repo, and none in the build context: enumerate a probe image
|
||||||
|
rather than reading `.dockerignore`
|
||||||
- [ ] No mutable image/package references
|
- [ ] No mutable image/package references
|
||||||
- [ ] No unnecessary files in repo root
|
- [ ] No unnecessary files in repo root
|
||||||
- [ ] All dates written as YYYY-MM-DD
|
- [ ] All dates written as YYYY-MM-DD
|
||||||
|
|||||||
@@ -257,7 +257,38 @@ style conventions are in separate documents:
|
|||||||
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
|
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
|
||||||
Fetch the standard `.gitignore` from
|
Fetch the standard `.gitignore` from
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
`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.
|
||||||
|
|
||||||
|
- **`.dockerignore` does not use `.gitignore` semantics, and copying patterns
|
||||||
|
across unmodified leaves secrets in the build context.** Docker matches with
|
||||||
|
`moby/patternmatcher`: `filepath.Match` semantics plus a `**` extension, so
|
||||||
|
`*` 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, while `config/.env`
|
||||||
|
and `certs/server.key` still reach the context and can land in an image layer
|
||||||
|
— which is more dangerous than a short file with no secret patterns at all,
|
||||||
|
because it reads as solved and stops anyone looking. Give every
|
||||||
|
depth-independent pattern the `**/` prefix and leave only genuinely
|
||||||
|
root-anchored entries unprefixed: `.git`, and the repo's own host-built
|
||||||
|
binary, written `/myapp` and never `**/myapp`, which would also match
|
||||||
|
`cmd/myapp/` and delete the package directory from the context. Matching is
|
||||||
|
case-sensitive, and an ALL-CAPS twin per pattern still misses `Server.Key`, so
|
||||||
|
secret names use character ranges — `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`,
|
||||||
|
and likewise for `.envrc` and the extensionless SSH keys. Where such a pattern
|
||||||
|
also catches something the build needs, re-include it with a negation
|
||||||
|
(`!docs/example.env`); deleting the pattern reopens the exposure for every
|
||||||
|
other file it covers. Fetch the standard `.dockerignore` from
|
||||||
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend
|
||||||
|
it with the repo's own artifacts.
|
||||||
|
|
||||||
|
- **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`). 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.
|
||||||
|
|
||||||
- **No build artifacts in version control.** Code-derived data (compiled
|
- **No build artifacts in version control.** Code-derived data (compiled
|
||||||
bundles, minified output, generated assets) must never be committed to the
|
bundles, minified output, generated assets) must never be committed to the
|
||||||
|
|||||||
Reference in New Issue
Block a user