fix(security): remediate 2026-05-12 audit findings (Stages 2–5)
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user