Day 81 — Security checklist

Updated

July 30, 2026

Day 81 — Security checklist

Stage VIII · ~3h
Goal: Run a structured security pass over your Go service/CLI—fix or ticket the top issues—and leave a checklist artifact suitable for the capstone evidence pack. Baseline Go 1.26.

Why this day exists

govulncheck (Day 72) is necessary but not sufficient. Most real incidents involve:

  • Broken authZ (IDOR)
  • Injection (SQL, command)
  • Secret leakage
  • Overexposed debug endpoints
  • Insecure defaults left from “dev mode”

This is a checklist day, not a crypto course. Capstone Day 89 will re-run the high-signal items. Day 80 load results should inform which timeouts and limits you tighten today.


Theory 1 — Threat-oriented checklist for this volume

Work top to bottom. Mark each item PASS / GAP / N/A with a one-line note.

A. Transport & network

  • TLS terminated correctly in real deploy (awareness from Day 60)
  • No plain secrets on public interfaces
  • pprof not public (Day 65)—localhost or build-tag only
  • /metrics network-restricted or acceptable risk documented
  • Timeouts on server and clients (ReadHeaderTimeout, outbound ctx)
srv := &http.Server{
    Addr:              cfg.Addr,
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       60 * time.Second,
}

Outbound:

client := &http.Client{Timeout: 10 * time.Second}
// or per-request context.WithTimeout

B. Authentication & authorization

  • Authn present on mutating/sensitive routes
  • Authz checks per resource (IDOR awareness)
  • Password hashing uses bcrypt/scrypt/argon2—not homemade
  • Tokens validated (exp, signature, audience) if JWT used
  • Admin routes separate and locked down
  • “Dev auth bypass” defaults to off
// IDOR sketch: always scope by principal
item, err := repo.GetForUser(ctx, userID, itemID)
if errors.Is(err, domain.ErrNotFound) {
    // 404 even if exists for someone else — avoid oracle if policy requires
}

C. Input & data

  • SQL via placeholders only ($1 / ?)—never string concat
  • os/exec with fixed binaries + CommandContext; no sh -c + user string
  • Request body size limits
  • Strict content-type for JSON APIs (at least on mutating routes)
  • Path traversal checked if serving files
  • Upload / batch limits (Day 56 batch max)
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
    // 415 or 400 per your policy
}

D. Secrets & config

  • No secrets in git history (scan quickly)
  • .env gitignored; .env.example only
  • Logs redact DSNs/tokens
  • CI secrets via platform secret store
  • JWT/signing keys from env, min entropy for lab

E. Dependencies & supply chain

  • govulncheck ./... clean or accepted with expiry
  • Minimal module graph (no abandoned critical deps)
  • replace directives reviewed
  • Toolchain is 1.26.x (stdlib patches)

F. Container & runtime

  • Non-root user
  • Read-only root FS where possible
  • Dropped capabilities (K8s securityContext awareness)
  • GOMEMLIMIT aligned with memory limit
  • No privileged docker flags in compose notes

G. Abuse & reliability as security

  • Timeouts everywhere (connection pileup is an availability attack)
  • Rate limit or at least document absence on login (stretch)
  • Panic recovery in HTTP boundary without swallowing auth bugs
  • Graceful shutdown (Day 78) reduces half-open weirdness
  • Load-test cliff notes linked from Day 80

Theory 2 — SQL injection quick proof

Bad:

q := "SELECT * FROM users WHERE name = '" + name + "'"
row := db.QueryRow(q) // never

Good:

row := db.QueryRowContext(ctx, `SELECT id, name FROM users WHERE name = ?`, name)

If you find concatenation—fix today. Grep assist:

rg -n 'Query(Context)?\([^)]*\+|Exec(Context)?\([^)]*\+' --glob '*.go' || true
rg -n 'fmt\.Sprintf\(`.*(SELECT|INSERT|UPDATE|DELETE)' --glob '*.go' || true

Theory 3 — Command injection quick proof

Bad:

exec.Command("sh", "-c", "grep "+userInput+" /var/log/app.log")

Good:

cmd := exec.CommandContext(ctx, "/usr/bin/grep", "--", userInput, "/var/log/app.log")
cmd.Path = "/usr/bin/grep" // fixed binary

Prefer pure Go implementations over shelling out. If you must shell out, fixed argv only—no string-built scripts.


Theory 4 — Security headers & CORS (light)

For browser-facing responses:

w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
// CSP when you have a real frontend story

APIs still benefit from careful CORS—default deny. Never combine Access-Control-Allow-Origin: * with credentialed requests.

// explicit allowlist sketch
if origin := r.Header.Get("Origin"); allowed[origin] {
    w.Header().Set("Access-Control-Allow-Origin", origin)
    w.Header().Set("Vary", "Origin")
}

Theory 5 — Debug endpoint exposure matrix

Endpoint Safe default
/debug/pprof/* 127.0.0.1 only or build tag debug
/metrics private network; no secrets in labels
/healthz OK public (no sensitive data)
/readyz OK public; do not leak DSNs in body
Error JSON No stack traces to clients
// separate mux/server for pprof
go http.ListenAndServe("127.0.0.1:6060", nil) // pprof registered on DefaultServeMux in import side effect — prefer dedicated mux

Prefer a dedicated debug server mux over hanging pprof on the public :8080 handler.


Theory 6 — Secret hygiene workflow

# triage carefully — expect false positives in tests/docs
rg -i 'api[_-]?key|secret|password\s*=|BEGIN (RSA |OPENSSH )?PRIVATE' \
  --glob '!**/*_test.go' \
  --glob '!**/testdata/**' || true
Finding Action
Real secret in tree Rotate; remove; rewrite git history if pushed
Example value in docs Use obviously fake (sk_test_xxx)
Test password Fine if non-production

.gitignore must include .env, *.pem, credentials.json patterns you actually use.


Worked example — SECURITY-CHECKLIST.md

# Security checklist — day81

Target: myservice @ <git sha>
Date:
Go: go1.26.x

## Scores
| Area | Status | Notes |
|------|--------|-------|
| Timeouts | PASS | ReadHeaderTimeout set |
| Authn | PASS | Bearer on /v1/* |
| Authz | GAP | list endpoint returns all users — ticket |
| SQL | PASS | placeholders only |
| Body limits | PASS | MaxBytesReader 1MiB |
| pprof | PASS | localhost only |
| metrics | PASS | private net note |
| govulncheck | PASS | clean (see SECURITY-SCAN.md) |
| Secrets | PASS | no .env committed |
| Container | PASS | nonroot distroless |
| Load cliffs | NOTE | see LOAD.md — pool at 200 RPS |

## Top fixes completed today
1. MaxBytesReader on JSON routes
2. ReadHeaderTimeout on http.Server

## Accepted risks
1. /metrics on same port — private network only — review: YYYY-MM-DD

## Tickets / follow-ups for capstone
1. IDOR on list endpoint

Lab 1 — Walk the full checklist

Suggested workspace: primary service/CLI.

  1. Copy the Theory 1 sections into SECURITY-CHECKLIST.md.
  2. Mark PASS / GAP / N/A with notes.
  3. Link Day 72 SECURITY-SCAN.md and Day 80 LOAD.md.
  4. Do not skip N/A without a reason (CLI vs service).

Lab 2 — Fix at least two real gaps

Pick high-signal fixes:

Priority Example fix
P0 SQL concat → placeholders
P0 pprof public → localhost
P1 Missing ReadHeaderTimeout
P1 Missing MaxBytesReader
P1 Auth bypass default on
P2 Security headers on browser routes
P2 Redact Authorization in logs
go test ./... -count=1
govulncheck ./...

Lab 3 — Re-scan supply chain + secret grep

  1. Re-run govulncheck ./....
  2. Run secret grep; triage hits.
  3. Confirm .env ignored: git check-ignore -v .env || true.
  4. Update checklist with results.

Lab 4 — AuthZ spot check (services)

Write one test or manual script:

# user A token creates item
# user B token GET/PUT that item → 403 or 404

If you only have a single-user lab, document IDOR as residual risk for multi-tenant future.


Lab 5 — Capstone handoff bullets

In checklist footer:

## Capstone (Days 82–89) must preserve
- Timeouts on http.Server
- Authn on mutating routes
- Placeholder SQL only
- pprof not public
- govulncheck in lint script

Day 89 harden day will re-open this file—make it honest.


Hour-by-hour suggestion

Time Focus
0:00–0:40 Full checklist pass
0:40–1:40 Two (or more) concrete fixes
1:40–2:10 govulncheck + secret grep
2:10–2:40 AuthZ spot check / residual risks
2:40–3:00 SECURITY-CHECKLIST.md polish

Common gotchas

Gotcha Fix
Checklist theater without fixes Minimum two concrete fixes
Disabling auth in “dev” left on Config flag default secure
CORS * with credentials Never
Trusting client-side role claims Server-side authz
Logging Authorization headers Redact middleware
500 bodies with err.Error() Generic client message
“N/A because CLI” skipping file perms Still check exec and secrets
Accepting metrics exposure forever Review date

Checkpoint

  • SECURITY-CHECKLIST.md complete
  • ≥2 fixes landed and tested
  • govulncheck re-run
  • pprof/metrics exposure stated
  • Secret grep triaged
  • Known gaps listed for capstone
  • Capstone handoff bullets written

Commit

git add .
git commit -m "day81: security checklist and fixes"

Write three personal gotchas before continuing.


Tomorrow

Day 82 — Capstone design doc: pick option A/B/C and write the plan before code sprawl. Bring security non-negotiables into the design (“auth on mutating routes,” “no public pprof”).