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:
@@ -53,7 +53,12 @@ metadata:
|
||||
labels:
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
|
||||
# cloudflare-only + admin-auth wired in (audit F2/F3/CODE-L6). Order
|
||||
# matters: reject non-Cloudflare IPs, then basic auth, then headers,
|
||||
# then rate limit. The admin-basic-auth secret is created by
|
||||
# 02-setup-secrets.sh from config.yaml admin.basic_auth_* — that runs
|
||||
# before 03-deploy.sh, so the middleware always has its secret.
|
||||
traefik.ingress.kubernetes.io/router.middlewares: honeydue-cloudflare-only@kubernetescrd,honeydue-admin-auth@kubernetescrd,honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
@@ -98,3 +103,98 @@ spec:
|
||||
name: web
|
||||
port:
|
||||
number: 3000
|
||||
---
|
||||
# Auth-endpoint Ingress (audit F10 / LIVE-L12). A dedicated Ingress for the
|
||||
# auth paths so Traefik gives their longer path-prefix routers a higher
|
||||
# priority than honeydue-api's "/" router — these paths then get
|
||||
# auth-rate-limit (5/min) instead of the general rate-limit (100/min).
|
||||
# Anything not matched here falls through to honeydue-api unchanged.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: honeydue-api-auth
|
||||
namespace: honeydue
|
||||
labels:
|
||||
app.kubernetes.io/part-of: honeydue
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: honeydue-auth-rate-limit@kubernetescrd,honeydue-security-headers@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- api.myhoneydue.com
|
||||
secretName: cloudflare-origin-cert
|
||||
rules:
|
||||
- host: api.myhoneydue.com
|
||||
http:
|
||||
paths:
|
||||
- path: /api/auth/login
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/register
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/forgot-password
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/reset-password
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/residences/join-with-code
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/verify-reset-code
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/apple-sign-in
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/google-sign-in
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/refresh
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
- path: /api/auth/account
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: api
|
||||
port:
|
||||
number: 8000
|
||||
|
||||
@@ -21,12 +21,20 @@ spec:
|
||||
headers:
|
||||
frameDeny: true
|
||||
contentTypeNosniff: true
|
||||
browserXssFilter: true
|
||||
# browserXssFilter removed (audit L7): it emits the deprecated
|
||||
# X-XSS-Protection header, which can itself introduce XSS in legacy
|
||||
# browsers. Modern browsers ignore it.
|
||||
referrerPolicy: "strict-origin-when-cross-origin"
|
||||
customResponseHeaders:
|
||||
X-Content-Type-Options: "nosniff"
|
||||
X-Frame-Options: "DENY"
|
||||
Strict-Transport-Security: "max-age=31536000; includeSubDomains"
|
||||
# HSTS: 2-year max-age + preload (audit L5/CODE-L3). After this is
|
||||
# live on api/admin/app, submit myhoneydue.com to hstspreload.org.
|
||||
Strict-Transport-Security: "max-age=63072000; includeSubDomains; preload"
|
||||
# Cross-origin isolation (audit F9). COEP (require-corp) is omitted —
|
||||
# it commonly breaks third-party embeds; add only after testing.
|
||||
Cross-Origin-Opener-Policy: "same-origin"
|
||||
Cross-Origin-Resource-Policy: "same-origin"
|
||||
# Content-Security-Policy is intentionally NOT set here — the Go API
|
||||
# sets a CSP in internal/router/router.go that permits Google Fonts
|
||||
# for the landing page. Two CSP headers would intersect and break it.
|
||||
@@ -83,3 +91,24 @@ spec:
|
||||
basicAuth:
|
||||
secret: admin-basic-auth
|
||||
realm: "honeyDue Admin"
|
||||
|
||||
---
|
||||
# Strict rate limit for auth endpoints (audit F10 / LIVE-L12).
|
||||
# Applied via the honeydue-api-auth Ingress to login / register /
|
||||
# forgot-password / reset-password / join-with-code. depth: 2 makes the
|
||||
# limiter key on the real client IP rather than the Cloudflare edge IP
|
||||
# (request path: client -> Cloudflare -> Traefik). This is the edge half;
|
||||
# the per-account lockout in the Go app is the robust half.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: auth-rate-limit
|
||||
namespace: honeydue
|
||||
spec:
|
||||
rateLimit:
|
||||
average: 5
|
||||
burst: 10
|
||||
period: 1m
|
||||
sourceCriterion:
|
||||
ipStrategy:
|
||||
depth: 2
|
||||
|
||||
Reference in New Issue
Block a user