Security hardening: TLS at origin, security headers, network policies, admin probe fix

Four related hardening changes made on the live cluster during this
session. Each manifest captures the final working state so a fresh
`kubectl apply` of the repo reproduces it.

1. Cloudflare Full (strict) TLS — ingresses now carry `tls:` blocks
   pointing at `cloudflare-origin-cert` secret (installed imperatively
   from the CF Origin CA PEM). CF SSL mode flipped from Flexible to
   Full (strict). CF↔origin is now HTTPS; origin serves a CF-issued
   cert that only CF can validate.

2. Traefik middleware attached to all three ingresses — `rate-limit`
   (100/min avg, 200 burst) and `security-headers` (frame-deny,
   nosniff, HSTS, referrer policy, permissions policy). `admin-auth`
   middleware was also defined in middleware.yaml but is not attached
   (needs an unset basic-auth secret) and was deleted at runtime.

3. `security-headers` middleware: stripped the
   Content-Security-Policy entry. The Go API sets its own CSP in
   internal/router/router.go that permits Google Fonts for the
   landing page. Two CSP headers combine via intersection (most
   restrictive wins), which would break the landing page. Next.js
   apps set their own CSP via middleware. Header kept documentation
   comments explain this.

4. NetworkPolicies — default-deny + explicit allows, applied. Added
   missing policies for `web`. Corrected the Traefik ingress rule: the
   scaffold used `namespaceSelector: kube-system`, but our Traefik
   runs as a DaemonSet with `hostNetwork: true`, so traffic arrives
   with the NODE IP as source. Fixed to an `ipBlock` list of the
   three node IPs plus the cluster pod CIDR (10.42.0.0/16).

5. admin livenessProbe path fix: was hitting /admin/ (404) which
   caused a 6-hour crashloop cycle (87 restarts) before the bug was
   caught. Fixed to / — matches the startupProbe and readinessProbe
   paths that were corrected earlier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-24 15:50:47 -05:00
parent 15359401fa
commit ace03d2340
4 changed files with 110 additions and 14 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ spec:
timeoutSeconds: 5 timeoutSeconds: 5
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /admin/ path: /
port: 3000 port: 3000
initialDelaySeconds: 30 initialDelaySeconds: 30
periodSeconds: 30 periodSeconds: 30
@@ -1,11 +1,10 @@
# Simple hostname-based Ingress — no TLS (Cloudflare Flexible handles edge # Hostname-based Ingress with TLS terminated at Traefik using the
# TLS, CF→origin is plain HTTP on 80). Upgrade to Full (strict) by # Cloudflare Origin CA cert (secret/cloudflare-origin-cert). CF→origin
# adding back a `tls:` block with a Cloudflare Origin CA cert stored in # encryption enables CF SSL mode "Full (strict)".
# secret/cloudflare-origin-cert.
# #
# Middleware chain (security headers, rate limit, CF-only allowlist, admin # Middleware chain (security headers, rate limit, CF-only allowlist, admin
# basic auth) is defined in `middleware.yaml` but NOT attached here — # basic auth) is defined in `middleware.yaml`. security-headers + rate-limit
# annotate this ingress to turn any of them on. # are attached below via annotation.
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
@@ -13,8 +12,15 @@ metadata:
namespace: honeydue namespace: honeydue
labels: labels:
app.kubernetes.io/part-of: honeydue app.kubernetes.io/part-of: honeydue
annotations:
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls:
- hosts:
- api.myhoneydue.com
- myhoneydue.com
secretName: cloudflare-origin-cert
rules: rules:
- host: api.myhoneydue.com - host: api.myhoneydue.com
http: http:
@@ -46,8 +52,14 @@ metadata:
namespace: honeydue namespace: honeydue
labels: labels:
app.kubernetes.io/part-of: honeydue app.kubernetes.io/part-of: honeydue
annotations:
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls:
- hosts:
- admin.myhoneydue.com
secretName: cloudflare-origin-cert
rules: rules:
- host: admin.myhoneydue.com - host: admin.myhoneydue.com
http: http:
@@ -67,8 +79,14 @@ metadata:
namespace: honeydue namespace: honeydue
labels: labels:
app.kubernetes.io/part-of: honeydue app.kubernetes.io/part-of: honeydue
annotations:
traefik.ingress.kubernetes.io/router.middlewares: honeydue-security-headers@kubernetescrd,honeydue-rate-limit@kubernetescrd
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls:
- hosts:
- app.myhoneydue.com
secretName: cloudflare-origin-cert
rules: rules:
- host: app.myhoneydue.com - host: app.myhoneydue.com
http: http:
+4 -1
View File
@@ -27,7 +27,10 @@ spec:
X-Content-Type-Options: "nosniff" X-Content-Type-Options: "nosniff"
X-Frame-Options: "DENY" X-Frame-Options: "DENY"
Strict-Transport-Security: "max-age=31536000; includeSubDomains" Strict-Transport-Security: "max-age=31536000; includeSubDomains"
Content-Security-Policy: "default-src 'self'; frame-ancestors 'none'" # 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.
# admin and web apps set their own CSP via Next.js middleware.
Permissions-Policy: "camera=(), microphone=(), geolocation=()" Permissions-Policy: "camera=(), microphone=(), geolocation=()"
X-Permitted-Cross-Domain-Policies: "none" X-Permitted-Cross-Domain-Policies: "none"
+81 -6
View File
@@ -47,10 +47,19 @@ spec:
policyTypes: policyTypes:
- Ingress - Ingress
ingress: ingress:
# Traefik runs as DaemonSet with hostNetwork=true, so traffic from it
# arrives with the NODE IP as source (not a pod IP). The node pod CIDR
# 10.42.0.0/16 covers any intra-cluster caller; the three node IPs
# cover Traefik on hostNetwork.
- from: - from:
- namespaceSelector: - ipBlock:
matchLabels: cidr: 178.105.32.198/32 # ubuntu-8gb-nbg1-1
kubernetes.io/metadata.name: kube-system - ipBlock:
cidr: 178.104.247.152/32 # ubuntu-8gb-nbg1-2
- ipBlock:
cidr: 178.104.249.189/32 # ubuntu-8gb-nbg1-3
- ipBlock:
cidr: 10.42.0.0/16 # cluster pod CIDR
ports: ports:
- protocol: TCP - protocol: TCP
port: 8000 port: 8000
@@ -69,10 +78,17 @@ spec:
policyTypes: policyTypes:
- Ingress - Ingress
ingress: ingress:
# Traefik runs as DaemonSet with hostNetwork=true — see allow-ingress-to-api
# for the rationale. Same ipBlock list.
- from: - from:
- namespaceSelector: - ipBlock:
matchLabels: cidr: 178.105.32.198/32
kubernetes.io/metadata.name: kube-system - ipBlock:
cidr: 178.104.247.152/32
- ipBlock:
cidr: 178.104.249.189/32
- ipBlock:
cidr: 10.42.0.0/16
ports: ports:
- protocol: TCP - protocol: TCP
port: 3000 port: 3000
@@ -200,3 +216,62 @@ spec:
ports: ports:
- protocol: TCP - protocol: TCP
port: 8000 port: 8000
---
# --- Web: allow ingress from Traefik (kube-system namespace) ---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-web
namespace: honeydue
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: web
policyTypes:
- Ingress
ingress:
# Traefik runs as DaemonSet with hostNetwork=true — see allow-ingress-to-api
# for the rationale. Same ipBlock list.
- from:
- ipBlock:
cidr: 178.105.32.198/32
- ipBlock:
cidr: 178.104.247.152/32
- ipBlock:
cidr: 178.104.249.189/32
- ipBlock:
cidr: 10.42.0.0/16
ports:
- protocol: TCP
port: 3000
---
# --- Web: allow egress for the Next.js server-side proxy routes ---
# Browser → app.myhoneydue.com → web pod (Node.js) → api.myhoneydue.com
# The web pod resolves api.myhoneydue.com via public DNS and hits
# Cloudflare (143.). We don't know which CF IP yet at policy time, so
# allow HTTPS to public ipBlock (except private CIDRs).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-egress-from-web
namespace: honeydue
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: web
policyTypes:
- Egress
egress:
# HTTPS to public (api.myhoneydue.com via CF, PostHog, any other remote)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443