fix(security): remediate 2026-05-12 audit findings (Stages 2–5)
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled
Backend CI / Build (push) Has been cancelled

Remediation of the 2026-05-12/13 audits (78 findings + cluster gaps),
tracked in deploy-k3s/SECURITY.md, plus fixes from two independent
post-remediation reviews.

Auth & sessions:
- SHA-256 hashed auth-token storage (C1); prior-token cache eviction on
  re-login (MEDIUM-1)
- local Google JWKS verification, iss/aud/exp checks (C2/C3)
- constant-time login + generic errors (L1/LIVE-L11/LIVE-L13)
- per-account login lockout keyed on distinct source IPs (M5/MEDIUM-3)
- verified-email gating, login rate limiting (LIVE-L19, H1-H3)

IAP & webhooks:
- Apple/Google cross-account replay protection (C5/C6/C10/C13, H5/H6)
- migrations 000003-000006 (token hashing, IAP replay, audit_log +
  webhook_event_log table creation, append-only audit log)

Authorization & races:
- file-ownership owner-OR-member fix (C7), atomic share-code join
  (C9/H9), device-token reassignment (C8/LOW-3)

Secrets & deploy:
- secrets file-mounted at /etc/honeydue/secrets, not env (F8); Redis
  password out of the ConfigMap (HIGH-1); B2 keys reconciled
- digest-pinned images, admin ingress hardening, CSP/HSTS, /metrics
  lockdown; kubeconfig 0600, etcd secrets-encryption, fail2ban +
  unattended-upgrades at provision; secret-rotation runbook

Build, vet, and the full test suite (incl. -race) pass; the goose
migration chain is verified against PostgreSQL 16.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-05-16 22:28:33 -05:00
parent 2004f9c5b2
commit c77ff07ce9
59 changed files with 2819 additions and 1245 deletions
+29 -9
View File
@@ -75,10 +75,13 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// responses are unaffected — they don't load any assets, so any CSP is fine.
// frame-ancestors stays 'none' to block clickjacking.
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
// XSSProtection deliberately empty (audit L7): the X-XSS-Protection
// header is deprecated and has itself caused XSS in legacy browsers.
XSSProtection: "",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 31536000,
HSTSMaxAge: 63072000, // 2 years — preload-eligible (audit L5/CODE-L3)
HSTSPreloadEnabled: true,
ReferrerPolicy: "strict-origin-when-cross-origin",
ContentSecurityPolicy: "default-src 'self'; " +
"style-src 'self' https://fonts.googleapis.com; " +
@@ -86,6 +89,8 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
"img-src 'self' data:; " +
"script-src 'self'; " +
"connect-src 'self'; " +
"object-src 'none'; " + // audit L8 — disable plugins/embeds
"base-uri 'self'; " + // audit L8 — block <base> hijacking
"frame-ancestors 'none'",
}))
e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{
@@ -136,9 +141,20 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// labeled by route pattern, method, and status code.
e.Use(prom.HTTPMiddleware())
// /metrics endpoint exposed for vmagent scrape. No auth — bound to
// the cluster network only; not exposed via Cloudflare.
e.GET("/metrics", prom.Handler())
// /metrics endpoint for the in-cluster vmagent scrape (audit LIVE-L1).
// vmagent scrapes api pods directly (pod-to-pod), so its requests carry
// no X-Forwarded-For. Any request that DOES carry one reached us through
// Traefik/Cloudflare — i.e. the public internet — and is refused with a
// 404. The api pod port is not exposed outside the cluster, so a request
// cannot reach /metrics without going through Traefik, and Traefik always
// appends X-Forwarded-For — the check cannot be bypassed.
metricsHandler := prom.Handler()
e.GET("/metrics", func(c echo.Context) error {
if c.Request().Header.Get("X-Forwarded-For") != "" {
return echo.NewHTTPError(http.StatusNotFound)
}
return metricsHandler(c)
})
// Serve landing page static files (if static directory is configured)
staticDir := cfg.Server.StaticDir
@@ -204,6 +220,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
// Wire Redis cache for residence-ID lookups across the four services that
// read it on the request hot path. Cache is best-effort; nil cache is OK.
if deps.Cache != nil {
authService.SetCacheService(deps.Cache) // per-account login lockout (audit M5)
residenceService.SetCacheService(deps.Cache)
taskService.SetCacheService(deps.Cache)
contractorService.SetCacheService(deps.Cache)
@@ -316,7 +333,7 @@ func SetupRouter(deps *Dependencies) *echo.Echo {
protected.Use(custommiddleware.TimezoneMiddleware())
{
setupProtectedAuthRoutes(protected, authHandler)
setupResidenceRoutes(protected, residenceHandler)
setupResidenceRoutes(protected, residenceHandler, authMiddleware.RequireVerified())
setupTaskRoutes(protected, taskHandler)
setupSuggestionRoutes(protected, suggestionHandler)
setupContractorRoutes(protected, contractorHandler)
@@ -583,7 +600,7 @@ func setupPublicDataRoutes(api *echo.Group, residenceHandler *handlers.Residence
}
// setupResidenceRoutes configures residence routes
func setupResidenceRoutes(api *echo.Group, residenceHandler *handlers.ResidenceHandler) {
func setupResidenceRoutes(api *echo.Group, residenceHandler *handlers.ResidenceHandler, requireVerified echo.MiddlewareFunc) {
residences := api.Group("/residences")
{
residences.GET("/", residenceHandler.ListResidences)
@@ -598,8 +615,11 @@ func setupResidenceRoutes(api *echo.Group, residenceHandler *handlers.ResidenceH
residences.DELETE("/:id/", residenceHandler.DeleteResidence)
residences.GET("/:id/share-code/", residenceHandler.GetShareCode)
residences.POST("/:id/generate-share-code/", residenceHandler.GenerateShareCode)
residences.POST("/:id/generate-share-package/", residenceHandler.GenerateSharePackage)
// Audit LIVE-L19: generating a residence share code requires a
// verified email — it blocks bad-faith unverified signups from
// minting share codes.
residences.POST("/:id/generate-share-code/", residenceHandler.GenerateShareCode, requireVerified)
residences.POST("/:id/generate-share-package/", residenceHandler.GenerateSharePackage, requireVerified)
residences.POST("/:id/generate-tasks-report/", residenceHandler.GenerateTasksReport)
residences.GET("/:id/users/", residenceHandler.GetResidenceUsers)
residences.DELETE("/:id/users/:user_id/", residenceHandler.RemoveResidenceUser)