A gitleaks config exists that inherits the default ruleset and allowlists the known test-fixture VAPID keys + .env.example/.env.spike so they do not trip the gate
A full-history/full-tree gitleaks baseline scan has been run and committed, suppressing any pre-existing findings so the first PR-diff scan starts from a clean known state
A .dockerignore exists that keeps secrets, dev affordances, tests, and bulk out of the Docker build context WITHOUT excluding apps/api/src (the builder stage needs it)
Author the gitleaks configuration and the committed full-history baseline (D-02) and create the full `.dockerignore` (D-09 / IMG-02) — the static security-scan and image-hygiene artifacts the CI jobs in Wave 2 consume. This delivers the secret-scanning half of the D-01 security-check baseline (secret scanning + static security lint; Trivy/image CVE scanning is dropped per D-01).
Purpose: The app holds real family credentials (encryption key, OIDC secret, Fastmail app passwords), so secret scanning is core. A per-PR diff scan (wired in 16-05) needs a config that allowlists the known test-fixture VAPID keypair (apps/api/tests/fixtures/vapid.ts) and env templates, plus a one-time baseline so pre-existing findings do not block every future PR. Separately, today the entire repo root is sent to the Docker daemon as build context (no .dockerignore exists), so .env, dev seed scripts, tests, and .planning/ are all shipped to the builder. The .dockerignore must exclude secrets/dev/bulk while preserving apps/api/src (the builder stage's COPY apps/api ./apps/api needs it) and the workspace manifests.
Output: .gitleaks.toml, scripts/gitleaks-baseline.json, .dockerignore. Consumed by 16-05 (gitleaks PR scan references the config + baseline) and 16-06 (static assertion greps the .dockerignore). The baseline-scan step is a human-verify checkpoint because it requires running gitleaks against the real repo history and confirming the only findings are the known test fixtures.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-CONTEXT.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md
@.planning/phases/16-ci-dependency-audit-and-security-checks/16-PATTERNS.md
@.gitignore
Task 1: Author .gitleaks.toml with default ruleset + fixture/env allowlists
- .gitleaks.toml (file being created)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (.gitleaks.toml Config section — exact content: title, [extend] useDefault=true, allowlists blocks with paths regex for apps/api/tests/fixtures/vapid.ts, .env.example, apps/api/.env.spike; the baseline caveat about the VAPID fixture)
- apps/api/tests/fixtures/vapid.ts (the real-looking VAPID keypair that WILL be flagged unless allowlisted)
- .gitignore (env-handling conventions: .env / .env.* ignored, .env.example kept)
Create .gitleaks.toml at repo root with: a `title`, an `[extend]` section with `useDefault = true` (inherit the built-in secret ruleset), and three `allowlists` blocks each with a `description` and a `paths` regex array allowlisting (1) apps/api/tests/fixtures/vapid.ts (documented test-only VAPID values), (2) `.env.example` (intentional placeholder template), and (3) apps/api/.env.spike (dev/spike values). Use the exact structure from RESEARCH.md. Do NOT add custom detection rules — only the default set plus allowlists. Commit: `chore(16-04): add gitleaks config with fixture + env allowlists`.
grep -q "useDefault" .gitleaks.toml && grep -q "vapid" .gitleaks.toml && grep -q "env.example" .gitleaks.toml && echo OK
- .gitleaks.toml exists with `[extend] useDefault = true`
- Three `allowlists` blocks cover the VAPID fixture, .env.example, and .env.spike, each with a description
- No custom `rules` were added (default ruleset only)
The gitleaks config inherits the default ruleset and allowlists the three known-safe paths.
Task 2: Create the full .dockerignore (secrets/dev/bulk, preserving builder inputs)
- .dockerignore (file being created)
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Exact .dockerignore Line List section — the full recommended content; the critical insight that .dockerignore filters the build CONTEXT only, not COPY --from=stage; the "Items NOT excluded" list: apps/api/src, apps/pwa/src, pnpm-workspace.yaml, pnpm-lock.yaml, package.json, package.jsons, tsconfig.jsons)
- apps/api/Dockerfile (builder stage `COPY apps/api ./apps/api` at line 16 — proves apps/api/src MUST stay in context; pwa-builder `COPY apps/pwa` at line 32)
- .gitignore (section-header comment style to mirror)
Create .dockerignore at repo root mirroring the RESEARCH.md "Recommended .dockerignore" content with section-header comments (Secrets, VCS, Build artifacts, Dependencies, Tests, Playwright artifacts, Planning/docs, Editor/OS, CI config, SQL dumps). MUST exclude: .env, .env.* (with `!.env.example` un-ignore), apps/api/scripts/seed-credential.mjs, .git, **/dist/, **/node_modules/, apps/api/tests/, apps/api/test/, apps/pwa/e2e/, Playwright artifact dirs, .planning/, docs/, editor/OS files, .gitea/, and *.sql dumps. MUST NOT exclude apps/api/src, apps/pwa/src, pnpm-workspace.yaml, pnpm-lock.yaml, the package.json files, or the tsconfig.json files (the builder/pwa-builder stages need them). Add the explanatory NOTE comment from RESEARCH about migration .sql files traveling only in the builder stage. Commit: `chore(16-04): add .dockerignore (secrets/dev/bulk, preserve builder inputs)`.
set -e; for p in ".env" "node_modules" "apps/api/scripts" ".git" ".planning" "apps/api/tests" "apps/pwa/e2e"; do grep -q "$p" .dockerignore || { echo "MISSING $p"; exit 1; }; done; grep -Eq '(^|/)apps/api/src( |/|$)' .dockerignore && { echo "ERROR: apps/api/src is excluded"; exit 1; }; echo OK
- .dockerignore exists and contains all forbidden patterns the 16-06 static assertion greps for (.env, node_modules, apps/api/scripts, .git, .planning, apps/api/tests, apps/pwa/e2e)
- .dockerignore does NOT exclude apps/api/src (verify grep finds no such line)
- `!.env.example` un-ignore is present so the template survives
The .dockerignore excludes secrets/dev/bulk from the build context while preserving builder-stage inputs.
Task 3: Run + commit the gitleaks full-history baseline; confirm only known fixtures are flagged
- .planning/phases/16-ci-dependency-audit-and-security-checks/16-RESEARCH.md (Full-History Baseline Scan section — exact command `gitleaks git --config .gitleaks.toml --report-path scripts/gitleaks-baseline.json`; the VAPID fixture caveat; --baseline-path semantics)
- .gitleaks.toml (config authored in Task 1)
Tasks 1-2 created .gitleaks.toml and .dockerignore. This checkpoint runs the one-time full-history/full-tree gitleaks baseline scan locally and commits the result, so the PR-diff scan wired in 16-05 starts from a clean, reviewed known state. This must be human-verified because the scan reads the real repo history and the operator must confirm that the ONLY findings are the documented test fixtures (VAPID keys) — a real leaked credential surfacing here is a genuine security event, not noise.
Automated steps the executor performs first:
1. Install gitleaks v8.30.1 locally (single binary): download `gitleaks_8.30.1_linux_x64.tar.gz` from github.com/gitleaks/gitleaks releases, extract, chmod +x.
2. Run `gitleaks git --config .gitleaks.toml --report-path scripts/gitleaks-baseline.json` from repo root.
3. Inspect scripts/gitleaks-baseline.json — list every finding's file + rule.
1. Review the executor's listing of baseline findings.
2. Confirm EVERY finding is one of: the test-fixture VAPID keys (apps/api/tests/fixtures/vapid.ts), .env.example placeholders, or .env.spike dev values — all of which Task 1 allowlisted (so ideally the baseline is empty/near-empty after allowlisting).
3. If ANY finding is a real credential (an actual OIDC secret, encryption key, or Fastmail app password committed to history) → STOP. Do not approve. This is a genuine leak requiring rotation + history rewrite, out of scope for this plan — flag it to the operator.
4. If all findings are the known fixtures (or none), approve. The executor then commits scripts/gitleaks-baseline.json with message `chore(16-04): commit gitleaks full-history baseline`.
test -f scripts/gitleaks-baseline.json && node -e "JSON.parse(require('fs').readFileSync('scripts/gitleaks-baseline.json','utf8')); console.log('valid JSON baseline')"
Type "approved" once you confirm the baseline contains only known test fixtures (or is empty), or describe any real credential found.
<threat_model>
Trust Boundaries
Boundary
Description
developer commit → git history
A secret committed to any branch enters history; gitleaks scans the git object, not just the working tree
repo working tree → Docker build context
Everything in the context is sent to the daemon and reachable by COPY; secrets/dev files must be filtered out
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-16-11
Information Disclosure
A real OIDC secret / encryption key / Fastmail app password committed to git history
mitigate
D-02: gitleaks default ruleset + full-history baseline (Tasks 1, 3) surface any pre-existing leak; the human checkpoint blocks approval if a real credential is found
T-16-12
Information Disclosure
Test-fixture keys mis-flagged, masking real findings in noise
mitigate
.gitleaks.toml allowlists the known fixture/template paths (Task 1) so the scan signal is real leaks only
T-16-13
Information Disclosure
.env, seed-credential.mjs, .planning, or family data shipped in the Docker image
mitigate
D-09: .dockerignore (Task 2) filters secrets/dev/bulk from the build context; verified by 16-06 static assertion
T-16-14
Tampering
.dockerignore accidentally excludes apps/api/src, breaking the build
accept
Task 2 verify explicitly asserts apps/api/src is NOT excluded; build failure is loud and caught at publish, residual risk nil
</threat_model>
- `.gitleaks.toml` has `useDefault = true` + the three fixture/env allowlists
- `scripts/gitleaks-baseline.json` exists, is valid JSON, and was human-confirmed to contain only known fixtures
- `.dockerignore` contains every forbidden pattern the 16-06 assertion checks AND does not exclude apps/api/src
<success_criteria>
gitleaks config inherits the default ruleset and allowlists known-safe paths
Full-history baseline committed and confirmed free of real credentials (human checkpoint)
.dockerignore excludes secrets/dev/bulk while preserving builder-stage inputs
</success_criteria>
Create `.planning/phases/16-ci-dependency-audit-and-security-checks/16-04-SUMMARY.md` when done.