docs/deployment: record security hardening pass + webapp + APNs
Backend CI / Test (push) Has been cancelled
Backend CI / Contract Tests (push) Has been cancelled
Backend CI / Build (push) Has been cancelled
Backend CI / Lint (push) Has been cancelled
Backend CI / Secret Scanning (push) Has been cancelled

Mark roadmap items done (network policies, Traefik middleware, CF Full
strict, CF IP UFW restriction, webapp deploy, APNs wired up, admin
URL-baking fix, admin probe bug). Update Chapter 4 (firewall rule
inventory now shows CF-only :443, no :80), Chapter 6 (request flow
walks through TLS on :443 and middleware hops), Chapter 13 (CF SSL
mode is Full strict, not Flexible; documents the origin cert
install), Chapter 7 (adds the web service section — proxy pattern,
3 replicas, PostHog build-args), and Appendix C (web manifests, CF
origin cert paths on disk, APNs .p8 path, updated network-policies
applied status).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-24 15:50:59 -05:00
parent ace03d2340
commit 7e77e3bbab
6 changed files with 198 additions and 124 deletions
+38 -41
View File
@@ -5,8 +5,9 @@
Cloudflare sits in front of every public request. It provides DNS
(authoritative nameservers for `myhoneydue.com`), TLS termination at
the edge, DDoS mitigation, caching, and the round-robin fan-out across
our three node IPs. We use the Free plan. TLS mode is "Flexible"
(HTTP between CF and origin). This chapter documents every Cloudflare
our three node IPs. We use the Free plan. TLS mode is **Full (strict)**
— CF connects to origin over HTTPS and verifies the origin's cert
against CF's own Origin CA. This chapter documents every Cloudflare
setting that matters.
## DNS
@@ -72,53 +73,49 @@ when you want sub-second failover.
## TLS
### Mode: Flexible
### Mode: Full (strict)
CF Dashboard → SSL/TLS → Overview → **Flexible**.
CF Dashboard → SSL/TLS → Overview → **Full (strict)**.
**What this means:**
- User ↔ Cloudflare: **TLS** (HTTPS)
- Cloudflare ↔ Origin: **plaintext HTTP** (port 80)
- User ↔ Cloudflare: **TLS** (HTTPS) — CF serves its own Let's Encrypt cert
- Cloudflare ↔ Origin: **TLS** (HTTPS :443) — origin serves our CF Origin CA cert; CF verifies it chains to CF's Origin CA root
**Why we chose it:**
- No origin cert required on the Hetzner nodes
- Zero Traefik cert-management complexity
- Fine for a site where CF terminates all user-facing TLS
**How it's wired:**
- k8s secret `cloudflare-origin-cert` (type `kubernetes.io/tls`) holds
`tls.crt` + `tls.key`. The cert is valid for `*.myhoneydue.com` +
`myhoneydue.com`, 15-year validity, issued by
`CloudFlare Origin CA SSL Certificate Authority`.
- All three `Ingress` resources in `deploy-k3s/manifests/ingress/ingress-simple.yaml`
reference the secret via `spec.tls[].secretName`.
- Traefik terminates TLS on :443 using the cert. Backend pods still
speak plain HTTP over the cluster network (Traefik → pod is an
intra-cluster hop, encrypted at the Flannel overlay layer).
**Downsides:**
- An attacker with network access between CF and Hetzner could read
traffic. Realistically: nobody between CF's POPs and Hetzner's
Nuremberg DC, but it's theoretically plaintext on the wire.
- MitM risk if DNS gets hijacked and traffic is routed through an
unintended origin.
**Why we chose Full (strict) over Flexible:**
- CF → origin traffic was plaintext on Flexible. Between Cloudflare's
POPs and Hetzner Nuremberg is a lot of internet. Full (strict)
closes that gap.
- Origin cert is a CF-internal-only CA, so it's useless to anyone who
isn't CF. Non-CF clients that somehow bypass the UFW CF-IP allowlist
can't impersonate the origin because their cert wouldn't chain to
CF's Origin CA root.
### Future: Full (strict)
**Maintenance:** the Origin CA cert is valid for 15 years (expires
Apr 2041). No action needed until then. If rotation is ever required,
regenerate in CF dashboard → SSL/TLS → Origin Server, re-run the
`kubectl create secret tls cloudflare-origin-cert --dry-run=client -o yaml | kubectl apply -f -`
command, Traefik picks it up on next secret reload (no pod restart).
The next step up is **Full (strict)**: CF verifies origin's TLS cert
and connects over HTTPS. Cloudflare provides free **Origin CA
certificates** for this: they're issued by a CF-internal CA that only
CF's own edge accepts. An attacker without a CF-signed cert can't
impersonate our origin.
### Regenerating the cert (for the record)
Path to enable:
1. Generate Origin CA cert in CF dashboard → SSL/TLS → Origin Server
2. Download as PEM
3. Create k8s Secret `cloudflare-origin-cert`:
```bash
kubectl create secret tls cloudflare-origin-cert -n honeydue \
--cert=origin.crt --key=origin.key
```
4. Add `tls:` block to our Ingress:
```yaml
spec:
tls:
- hosts: [api.myhoneydue.com]
secretName: cloudflare-origin-cert
```
5. Switch CF SSL mode to Full (strict)
Trad-off: the `cloudflare-origin-cert` expires (default 15 years), so
low maintenance. **TODO** (Chapter 20).
```bash
# After downloading cf-origin-cert.pem + cf-origin-key.pem from CF dashboard:
kubectl -n honeydue create secret tls cloudflare-origin-cert \
--cert=cf-origin-cert.pem \
--key=cf-origin-key.pem \
--dry-run=client -o yaml | kubectl apply -f -
```
### Edge certificate