go-auth-default-bootstrap-token
GAP-019 required three changes to an auth-gated Go API. Implemented at ~/go-auth-default-bootstrap-token/ (module go-auth-default-bootstrap-token, stdlib only):
1. --require-auth now defaults to true (main.go):
// GAP-019: the API must be authenticated by default. Exported as a var so
// tests can assert the default; do not flip this back to false.
var defaultRequireAuth = true
requireAuth = flag.Bool("require-auth", defaultRequireAuth,
"require authentication for /v1/ and /api/ routes (GAP-019: default is now true)")
2. First-run bootstrap token (iam/bootstrap.go) — generates a 32-hex token only when <data-dir>/iam/principals.yaml is missing; subsequent runs load the persisted token and never regenerate (unless --regenerate-token):
func EnsureBootstrap(dataDir string, force bool) (*Result, error) {
iamDir := filepath.Join(dataDir, "iam")
principalsPath := filepath.Join(iamDir, "principals.yaml")
policiesPath := filepath.Join(iamDir, "policies.yaml")
if !force {
if tok, err := loadToken(principalsPath); err == nil { // rerun: reuse, no regen
return &Result{Token: tok, Created: false, IAMDir: iamDir}, nil
}
}
tok, err := generateToken() // 16 random bytes → 32 lowercase hex chars
...
if err := writeIAMFiles(iamDir, principalsPath, policiesPath, tok); err != nil {
return nil, err
}
return &Result{Token: tok, Created: true, IAMDir: iamDir}, nil
}
Files are written atomically (temp file + rename, mode 0600) so a crash mid-write can't leave a half-written principals.yaml that would trigger an unwanted regen. On Created=true the token is printed to stdout once, with --print-token as the ops escape hatch to recover it, and --regenerate-token for explicit rotation.
3. IAMAuth path exemption (iam/iam.go) — only /v1/... and /api/... are gated; everything else (healthchecks, SPA) stays open:
func Middleware(requireAuth bool, token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !requireAuth || !IsAPIPath(r.URL.Path) { // escape hatch: non-API paths stay open
next.ServeHTTP(w, r)
return
}
if !validBearer(r, token) { // constant-time compare via crypto/subtle
w.Header().Set("WWW-Authenticate", `Bearer realm="api", error="invalid_token"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func IsAPIPath(p string) bool {
if i := strings.IndexByte(p, '?'); i >= 0 { p = p[:i] }
if p == "/v1" || strings.HasPrefix(p, "/v1/") { return true }
return p == "/api" || strings.HasPrefix(p, "/api/")
}
Routes: /healthz (open), / catch-all SPA (open; authenticated-but-unknown /v1|x//api|x → 404, not SPA fallthrough), /v1/status + /api/status (bearer-gated).
**Unit tests (14, all passing, `go vet` clean):** bootstrap creates `0600` files + valid 32-hex token; no regen on rerun (token identical); force regenerates; operator-edited token is picked up; deleted `iam/` dir regenerates; format validation; middleware 401 anonymous / 200 with token / 401 wrong token & bad scheme (incl. lowercase `bearer`) / health + SPA + `/v1x`-style paths open / `--require-auth=false` anonymous 200 / `IsAPIPath` boundaries (incl. query strings); end-to-end matrix; default-flag regression guard. **Live verification with the real binary** (all four required checks reproduced, then cleaned up): ``` 401-anonymous /v1/status -> 401 ✓ (was anonymous before GAP-019) 200-token /v1/status -> 200 ✓ 200-health /healthz -> 200 ✓ (exempt) 200-escape-hatch-SPA / -> 200 ✓ (exempt, SPA stays open) ``` Plus: `/api/status` 401/200 ✓, wrong token 401 ✓, `/index.html` + `/static/app.js` open ✓, **no regen on rerun** (0 bootstrap banners, `--print-token` returned the identical token across runs) ✓, files persisted `-rw-------` with both YAMLs ✓, `--require-auth=false` → anonymous API 200 (opt-in escape hatch) ✓, `--regenerate-token` rotates ✓. **Edge cases caught during verification:** an unrelated "Helios" gateway service already bound to port 18080 silently hijacked every curl with its own 401s — I moved `net.Listen` before the "listening" log so bind failures are loud, and re-verified on a free port.
{"model": "deepseek-v4-flash", "problem_class": "go-auth-default-bootstrap-token", "result": "passed", "tests": 14}GAP-019 required three changes to an auth-gated Go API. Implemented at ~/go-auth-default-bootstrap-token/ (module go-auth-default-bootstrap-token, stdlib only):
1. --require-auth now defaults to true (main.go):
// GAP-019: the API must be authenticated by default. Exported as a var so
// tests can assert the default; do not flip this back to false.
var defaultRequireAuth = true
requireAuth = flag.Bool("require-auth", defaultRequireAuth,
"require authentication for /v1/ and /api/ routes (GAP-019: default is now true)")
2. First-run bootstrap token (iam/bootstrap.go) — generates a 32-hex token only when <data-dir>/iam/principals.yaml is missing; subsequent runs load the persisted token and never regenerate (unless --regenerate-token):
func EnsureBootstrap(dataDir string, force bool) (*Result, error) {
iamDir := filepath.Join(dataDir, "iam")
principalsPath := filepath.Join(iamDir, "principals.yaml")
policiesPath := filepath.Join(iamDir, "policies.yaml")
if !force {
if tok, err := loadToken(principalsPath); err == nil { // rerun: reuse, no regen
return &Result{Token: tok, Created: false, IAMDir: iamDir}, nil
}
}
tok, err := generateToken() // 16 random bytes → 32 lowercase hex chars
...
if err := writeIAMFiles(iamDir, principalsPath, policiesPath, tok); err != nil {
return nil, err
}
return &Result{Token: tok, Created: true, IAMDir: iamDir}, nil
}
Files are written atomically (temp file + rename, mode 0600) so a crash mid-write can't leave a half-written principals.yaml that would trigger an unwanted regen. On Created=true the token is printed to stdout once, with --print-token as the ops escape hatch to recover it, and --regenerate-token for explicit rotation.
3. IAMAuth path exemption (iam/iam.go) — only /v1/... and /api/... are gated; everything else (healthchecks, SPA) stays open:
func Middleware(requireAuth bool, token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !requireAuth || !IsAPIPath(r.URL.Path) { // escape hatch: non-API paths stay open
next.ServeHTTP(w, r)
return
}
if !validBearer(r, token) { // constant-time compare via crypto/subtle
w.Header().Set("WWW-Authenticate", `Bearer realm="api", error="invalid_token"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func IsAPIPath(p string) bool {
if i := strings.IndexByte(p, '?'); i >= 0 { p = p[:i] }
if p == "/v1" || strings.HasPrefix(p, "/v1/") { return true }
return p == "/api" || strings.HasPrefix(p, "/api/")
}
Routes: /healthz (open), / catch-all SPA (open; authenticated-but-unknown /v1|x//api|x → 404, not SPA fallthrough), /v1/status + /api/status (bearer-gated).
**Unit tests (14, all passing, `go vet` clean):** bootstrap creates `0600` files + valid 32-hex token; no regen on rerun (token identical); force regenerates; operator-edited token is picked up; deleted `iam/` dir regenerates; format validation; middleware 401 anonymous / 200 with token / 401 wrong token & bad scheme (incl. lowercase `bearer`) / health + SPA + `/v1x`-style paths open / `--require-auth=false` anonymous 200 / `IsAPIPath` boundaries (incl. query strings); end-to-end matrix; default-flag regression guard. **Live verification with the real binary** (all four required checks reproduced, then cleaned up): ``` 401-anonymous /v1/status -> 401 ✓ (was anonymous before GAP-019) 200-token /v1/status -> 200 ✓ 200-health /healthz -> 200 ✓ (exempt) 200-escape-hatch-SPA / -> 200 ✓ (exempt, SPA stays open) ``` Plus: `/api/status` 401/200 ✓, wrong token 401 ✓, `/index.html` + `/static/app.js` open ✓, **no regen on rerun** (0 bootstrap banners, `--print-token` returned the identical token across runs) ✓, files persisted `-rw-------` with both YAMLs ✓, `--require-auth=false` → anonymous API 200 (opt-in escape hatch) ✓, `--regenerate-token` rotates ✓. **Edge cases caught during verification:** an unrelated "Helios" gateway service already bound to port 18080 silently hijacked every curl with its own 401s — I moved `net.Listen` before the "listening" log so bind failures are loud, and re-verified on a free port.
{"model": "deepseek-v4-flash", "problem_class": "go-auth-default-bootstrap-token", "result": "passed", "tests": 14}