# honeyDue — Production Security Hardening Guide

Comprehensive security documentation for the honeyDue K3s deployment. Covers every layer from cloud provider to application.

**Last updated**: 2026-03-28

---

## Table of Contents

1. [Threat Model](#1-threat-model)
2. [Hetzner Cloud (Host)](#2-hetzner-cloud-host)
3. [K3s Cluster](#3-k3s-cluster)
4. [Pod Security](#4-pod-security)
5. [Network Segmentation](#5-network-segmentation)
6. [Redis](#6-redis)
7. [PostgreSQL (Neon)](#7-postgresql-neon)
8. [Cloudflare](#8-cloudflare)
9. [Container Images](#9-container-images)
10. [Secrets Management](#10-secrets-management)
11. [B2 Object Storage](#11-b2-object-storage)
12. [Monitoring & Alerting](#12-monitoring--alerting)
13. [Incident Response](#13-incident-response)
14. [Compliance Checklist](#14-compliance-checklist)

---

## 1. Threat Model

### What We're Protecting

| Asset | Impact if Compromised |
|-------|----------------------|
| User credentials (bcrypt hashes) | Account takeover, password reuse attacks |
| Auth tokens | Session hijacking |
| Personal data (email, name, residences) | Privacy violation, regulatory exposure |
| Push notification keys (APNs, FCM) | Spam push to all users, key revocation |
| Cloudflare origin cert | Direct TLS impersonation |
| Database credentials | Full data exfiltration |
| Redis data | Session replay, job queue manipulation |
| B2 storage keys | Document theft or deletion |

### Attack Surface

```
Internet
   │
   ▼
Cloudflare (WAF, DDoS protection, TLS termination)
   │
   ▼ (origin cert, Full Strict)
Hetzner Cloud Firewall (ports 22, 443, 6443)
   │
   ▼
K3s Traefik Ingress (Cloudflare-only IP allowlist)
   │
   ├──► API pods (Go) ──► Neon PostgreSQL (external, TLS)
   │                   ──► Redis (internal, authenticated)
   │                   ──► APNs/FCM (external, TLS)
   │                   ──► B2 Storage (external, TLS)
   │                   ──► SMTP (external, TLS)
   │
   ├──► Admin pods (Next.js) ──► API pods (internal)
   │
   └──► Worker pods (Go) ──► same as API
```

### Trust Boundaries

1. **Internet → Cloudflare**: Untrusted. Cloudflare handles DDoS, WAF, TLS.
2. **Cloudflare → Origin**: Semi-trusted. Origin cert validates, IP allowlist enforces.
3. **Ingress → Pods**: Trusted network, but segmented by NetworkPolicy.
4. **Pods → External Services**: Outbound only, TLS required, credentials scoped.
5. **Pods → K8s API**: Denied. Service accounts have no permissions.

---

## 2. Hetzner Cloud (Host)

### Firewall Rules

Only three ports should be open on the Hetzner Cloud Firewall:

| Port | Protocol | Source | Purpose |
|------|----------|--------|---------|
| 22 | TCP | Your IP(s) only | SSH management |
| 443 | TCP | Cloudflare IPs only | HTTPS traffic |
| 6443 | TCP | Your IP(s) only | K3s API (kubectl) |

```bash
# Verify Hetzner firewall rules (Hetzner CLI)
hcloud firewall describe honeydue-fw
```

### SSH Hardening

- **Key-only authentication** — password auth disabled in `/etc/ssh/sshd_config`
- **Root login disabled** — `PermitRootLogin no`
- **fail2ban active** — auto-bans IPs after 5 failed SSH attempts

```bash
# Verify SSH config on each node
ssh user@NODE_IP "grep -E 'PasswordAuthentication|PermitRootLogin' /etc/ssh/sshd_config"
# Expected: PasswordAuthentication no, PermitRootLogin no

# Check fail2ban status
ssh user@NODE_IP "sudo fail2ban-client status sshd"
```

### OS Updates

```bash
# Enable unattended security updates (Ubuntu 24.04)
ssh user@NODE_IP "sudo apt install unattended-upgrades && sudo dpkg-reconfigure -plow unattended-upgrades"
```

---

## 3. K3s Cluster

### Secret Encryption at Rest

K3s is configured with `secrets-encryption: true` in the server config. This encrypts all Secret resources in etcd using AES-CBC.

```bash
# Verify encryption is active
k3s secrets-encrypt status
# Expected: Encryption Status: Enabled

# Rotate encryption keys (do periodically)
k3s secrets-encrypt rotate-keys
k3s secrets-encrypt reencrypt
```

### RBAC

Each workload has a dedicated ServiceAccount with `automountServiceAccountToken: false`:

| ServiceAccount | Used By | K8s API Access |
|---------------|---------|----------------|
| `api` | API deployment | None |
| `worker` | Worker deployment | None |
| `admin` | Admin deployment | None |
| `redis` | Redis deployment | None |

No Roles or RoleBindings are created — pods have zero K8s API access.

```bash
# Verify service accounts exist
kubectl get sa -n honeydue

# Verify no roles are bound
kubectl get rolebindings -n honeydue
kubectl get clusterrolebindings | grep honeydue
# Expected: no results
```

### Pod Disruption Budgets

Prevent node maintenance from taking down all replicas:

| Workload | Replicas | minAvailable |
|----------|----------|-------------|
| API | 3 | 2 |
| Worker | 2 | 1 |

```bash
# Verify PDBs
kubectl get pdb -n honeydue
```

### Audit Logging (Optional Enhancement)

K3s supports audit logging for API server requests:

```yaml
# Add to K3s server config for detailed audit logging
# /etc/rancher/k3s/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets", "configmaps"]
  - level: RequestResponse
    users: ["system:anonymous"]
  - level: None
    resources:
      - group: ""
        resources: ["events"]
```

### WireGuard (Optional Enhancement)

K3s supports WireGuard for encrypting inter-node traffic:

```bash
# Enable WireGuard on K3s (add to server args)
# --flannel-backend=wireguard-native
```

---

## 4. Pod Security

### Security Contexts

Every pod runs with these security restrictions:

**Pod-level:**
```yaml
securityContext:
  runAsNonRoot: true
  runAsUser: <uid>       # 1000 (api/worker), 1001 (admin), 999 (redis)
  runAsGroup: <gid>
  fsGroup: <gid>
  seccompProfile:
    type: RuntimeDefault  # Linux kernel syscall filtering
```

**Container-level:**
```yaml
securityContext:
  allowPrivilegeEscalation: false  # Cannot gain more privileges than parent
  readOnlyRootFilesystem: true     # Filesystem is immutable
  capabilities:
    drop: ["ALL"]                  # No Linux capabilities
```

### Writable Directories

With `readOnlyRootFilesystem: true`, writable paths use emptyDir volumes:

| Pod | Path | Purpose | Backing |
|-----|------|---------|---------|
| API | `/tmp` | Temp files | emptyDir (64Mi) |
| Worker | `/tmp` | Temp files | emptyDir (64Mi) |
| Admin | `/app/.next/cache` | Next.js ISR cache | emptyDir (256Mi) |
| Admin | `/tmp` | Temp files | emptyDir (64Mi) |
| Redis | `/data` | Persistence | PVC (5Gi) |
| Redis | `/tmp` | AOF rewrite temp | emptyDir tmpfs (64Mi) |

### User IDs

| Container | UID:GID | Source |
|-----------|---------|--------|
| API | 1000:1000 | Dockerfile `app` user |
| Worker | 1000:1000 | Dockerfile `app` user |
| Admin | 1001:1001 | Dockerfile `nextjs` user |
| Redis | 999:999 | Alpine `redis` user |

```bash
# Verify all pods run as non-root
kubectl get pods -n honeydue -o jsonpath='{range .items[*]}{.metadata.name}{" runAsNonRoot="}{.spec.securityContext.runAsNonRoot}{"\n"}{end}'
```

---

## 5. Network Segmentation

### Default-Deny Policy

All ingress and egress traffic in the `honeydue` namespace is denied by default. Explicit NetworkPolicy rules allow only necessary traffic.

### Allowed Traffic

```
                  ┌─────────────┐
                  │   Traefik    │
                  │ (kube-system)│
                  └──────┬──────┘
                         │
              ┌──────────┼──────────┐
              │          │          │
              ▼          ▼          │
         ┌────────┐ ┌────────┐     │
         │  API   │ │ Admin  │     │
         │ :8000  │ │ :3000  │     │
         └───┬────┘ └────┬───┘     │
             │           │         │
     ┌───────┤           │         │
     │       │           │         │
     ▼       ▼           ▼         │
 ┌───────┐ ┌────────┐ ┌────────┐  │
 │ Redis │ │External│ │  API   │  │
 │ :6379 │ │Services│ │(in-clr)│  │
 └───────┘ └────────┘ └────────┘  │
     ▲                             │
     │       ┌────────┐            │
     └───────│ Worker │────────────┘
             └────────┘
```

| Policy | From | To | Ports |
|--------|------|----|-------|
| `default-deny-all` | all | all | none |
| `allow-dns` | all pods | kube-dns | 53 UDP/TCP |
| `allow-ingress-to-api` | Traefik (kube-system) | API pods | 8000 |
| `allow-ingress-to-admin` | Traefik (kube-system) | Admin pods | 3000 |
| `allow-ingress-to-redis` | API + Worker pods | Redis | 6379 |
| `allow-egress-from-api` | API pods | Redis, external (443, 5432, 587) | various |
| `allow-egress-from-worker` | Worker pods | Redis, external (443, 5432, 587) | various |
| `allow-egress-from-admin` | Admin pods | API pods (in-cluster) | 8000 |

**Key restrictions:**
- Redis is reachable ONLY from API and Worker pods
- Admin can ONLY reach the API service (no direct DB/Redis access)
- No pod can reach private IP ranges except in-cluster services
- External egress limited to specific ports (443, 5432, 587)

```bash
# Verify network policies
kubectl get networkpolicy -n honeydue

# Test: admin pod should NOT be able to reach Redis
kubectl exec -n honeydue deploy/admin -- nc -zv redis.honeydue.svc.cluster.local 6379
# Expected: timeout/refused
```

---

## 6. Redis

### Authentication

Redis requires a password when `redis.password` is set in `config.yaml`:

- Password passed via `REDIS_PASSWORD` environment variable from `honeydue-secrets`
- Redis starts with `--requirepass $REDIS_PASSWORD`
- Health probes authenticate with `-a $REDIS_PASSWORD`
- Go API connects via `redis://:PASSWORD@redis.honeydue.svc.cluster.local:6379/0`

### Network Isolation

- Redis has **no Ingress** — not exposed outside the cluster
- NetworkPolicy restricts access to API and Worker pods only
- Admin pods cannot reach Redis

### Memory Limits

- `--maxmemory 256mb` — hard cap on Redis memory
- `--maxmemory-policy noeviction` — returns errors rather than silently evicting data
- K8s resource limit: 512Mi (headroom for AOF rewrite)

### Dangerous Command Renaming (Optional Enhancement)

For additional protection, rename dangerous commands in a custom `redis.conf`:

```
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command DEBUG ""
rename-command CONFIG "HONEYDUE_CONFIG_a7f3b"
```

```bash
# Verify Redis auth is required
kubectl exec -n honeydue deploy/redis -- redis-cli ping
# Expected: (error) NOAUTH Authentication required.

kubectl exec -n honeydue deploy/redis -- redis-cli -a "$REDIS_PASSWORD" ping
# Expected: PONG
```

---

## 7. PostgreSQL (Neon)

### Connection Security

- **SSL required**: `sslmode=require` in connection string
- **Connection limits**: `max_open_conns=25`, `max_idle_conns=10`
- **Scoped credentials**: Database user has access only to `honeydue` database
- **Password rotation**: Change in Neon dashboard, update `secrets/postgres_password.txt`, re-run `02-setup-secrets.sh`

### Access Control

- Only API and Worker pods have egress to port 5432 (NetworkPolicy enforced)
- Admin pods cannot reach the database directly
- Redis pods have no external egress

```bash
# Verify only API/Worker can reach Neon
kubectl exec -n honeydue deploy/admin -- nc -zv ep-xxx.us-east-2.aws.neon.tech 5432
# Expected: timeout (blocked by network policy)
```

### Query Safety

- GORM uses parameterized queries (SQL injection prevention)
- No raw SQL in handlers — all queries go through repositories
- Decimal fields use `shopspring/decimal` (no floating-point errors)

---

## 8. Cloudflare

### TLS Configuration

- **Mode**: Full (Strict) — Cloudflare validates the origin certificate
- **Origin cert**: Stored as K8s Secret `cloudflare-origin-cert`
- **Minimum TLS**: 1.2 (set in Cloudflare dashboard)
- **HSTS**: Enabled via security headers middleware

### Origin Lockdown

The `cloudflare-only` Traefik middleware restricts all ingress to Cloudflare IP ranges only. Direct requests to the origin IP are rejected with 403.

```bash
# Test: direct request to origin should fail
curl -k https://ORIGIN_IP/api/health/
# Expected: 403 Forbidden

# Test: request through Cloudflare should work
curl https://api.myhoneydue.com/api/health/
# Expected: 200 OK
```

### Cloudflare IP Range Updates

Cloudflare IP ranges change infrequently but should be checked periodically:

```bash
# Compare current ranges with deployed middleware
diff <(curl -s https://www.cloudflare.com/ips-v4; curl -s https://www.cloudflare.com/ips-v6) \
     <(kubectl get middleware cloudflare-only -n honeydue -o jsonpath='{.spec.ipAllowList.sourceRange[*]}' | tr ' ' '\n')
```

### WAF & Rate Limiting

- **Cloudflare WAF**: Enable managed rulesets in dashboard (OWASP Core, Cloudflare Specials)
- **Rate limiting**: Traefik middleware (100 req/min, burst 200) + Go API auth rate limiting
- **Bot management**: Enable in Cloudflare dashboard for API routes

### Security Headers

Applied via Traefik middleware to all responses:

| Header | Value |
|--------|-------|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
| `X-Frame-Options` | `DENY` |
| `X-Content-Type-Options` | `nosniff` |
| `X-XSS-Protection` | `1; mode=block` |
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
| `Content-Security-Policy` | `default-src 'self'; frame-ancestors 'none'` |
| `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` |
| `X-Permitted-Cross-Domain-Policies` | `none` |

---

## 9. Container Images

### Build Security

- **Multi-stage builds**: Build stage discarded, only runtime artifacts copied
- **Alpine base**: Minimal attack surface (~5MB base)
- **Non-root users**: `app:1000` (Go), `nextjs:1001` (admin)
- **Stripped binaries**: Go binaries built with `-ldflags "-s -w"` (no debug symbols)
- **No shell in final image** (Go containers): Only the binary + CA certs

### Image Scanning (Recommended)

Add image scanning to CI/CD before pushing to GHCR:

```bash
# Trivy scan (run in CI)
trivy image --severity HIGH,CRITICAL --exit-code 1 ghcr.io/NAMESPACE/honeydue-api:latest

# Grype alternative
grype ghcr.io/NAMESPACE/honeydue-api:latest --fail-on high
```

### Version Pinning

- Redis image: `redis:7-alpine` (pin to specific tag in production, e.g., `redis:7.4.2-alpine`)
- Go base: pinned in Dockerfile
- Node base: pinned in admin Dockerfile

---

## 10. Secrets Management

### At-Rest Encryption

K3s encrypts all Secret resources in etcd with AES-CBC (`--secrets-encryption` flag).

### Secret Inventory

| Secret | Contains | Rotation Procedure |
|--------|----------|--------------------|
| `honeydue-secrets` | DB password, SECRET_KEY, SMTP password, FCM key, Redis password | Update source files + re-run `02-setup-secrets.sh` |
| `honeydue-apns-key` | APNs .p8 private key | Replace file + re-run `02-setup-secrets.sh` |
| `cloudflare-origin-cert` | TLS cert + key | Regenerate in Cloudflare + re-run `02-setup-secrets.sh` |
| `ghcr-credentials` | Registry PAT | Regenerate GitHub PAT + re-run `02-setup-secrets.sh` |
| `admin-basic-auth` | htpasswd hash | Update config.yaml + re-run `02-setup-secrets.sh` |

### Rotation Procedure

```bash
# 1. Update the secret source (file or config.yaml value)
# 2. Re-run the secrets script
./scripts/02-setup-secrets.sh

# 3. Restart affected pods to pick up new secret values
kubectl rollout restart deployment/api deployment/worker -n honeydue

# 4. Verify pods are healthy
kubectl get pods -n honeydue -w
```

### Secret Hygiene

- `secrets/` directory is gitignored — never committed
- `config.yaml` is gitignored — never committed
- Scripts validate secret files exist and aren't empty before creating K8s secrets
- `SECRET_KEY` requires minimum 32 characters
- ConfigMap redacts sensitive values in `04-verify.sh` output

---

## 11. B2 Object Storage

### Access Control

- **Scoped application key**: Create a B2 key with access to only the `honeydue` bucket
- **Permissions**: Read + Write only (no `deleteFiles`, no `listAllBucketNames`)
- **Bucket-only**: Key cannot access other buckets in the account

```bash
# Create scoped B2 key (Backblaze CLI)
b2 create-key --bucket BUCKET_NAME honeydue-api readFiles,writeFiles,listFiles
```

### Upload Validation (Go API)

- File size limit: `STORAGE_MAX_FILE_SIZE` (10MB default)
- Allowed MIME types: `STORAGE_ALLOWED_TYPES` (images + PDF only)
- Path traversal protection in upload handler
- Files served via authenticated proxy (`media_handler`) — no direct B2 URLs exposed to clients

### Versioning

Enable B2 bucket versioning to protect against accidental deletion:

```bash
# Enable versioning on the B2 bucket
b2 update-bucket --versioning enabled BUCKET_NAME
```

---

## 12. Monitoring & Alerting

### Log Aggregation

K3s logs are available via `kubectl logs`. For persistent log aggregation:

```bash
# View API logs
kubectl logs -n honeydue -l app.kubernetes.io/name=api --tail=100 -f

# View worker logs
kubectl logs -n honeydue -l app.kubernetes.io/name=worker --tail=100 -f

# View all warning events
kubectl get events -n honeydue --field-selector type=Warning --sort-by='.lastTimestamp'
```

**Recommended**: Deploy Loki + Grafana for persistent log search and alerting.

### Health Monitoring

```bash
# Continuous health monitoring
watch -n 10 "kubectl get pods -n honeydue -o wide && echo && kubectl top pods -n honeydue 2>/dev/null"

# Check pod restart counts (indicator of crashes)
kubectl get pods -n honeydue -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.containerStatuses[*]}{.restartCount}{end}{"\n"}{end}'
```

### Alerting Thresholds

| Metric | Warning | Critical | Check Command |
|--------|---------|----------|---------------|
| Pod restarts | > 3 in 1h | > 10 in 1h | `kubectl get pods` |
| API response time | > 500ms p95 | > 2s p95 | Cloudflare Analytics |
| Memory usage | > 80% limit | > 95% limit | `kubectl top pods` |
| Redis memory | > 200MB | > 250MB | `redis-cli info memory` |
| Disk (PVC) | > 80% | > 95% | `kubectl exec ... df -h` |
| Certificate expiry | < 30 days | < 7 days | Cloudflare dashboard |

### Audit Trail

- **K8s events**: `kubectl get events -n honeydue` (auto-pruned after 1h)
- **Go API**: zerolog structured logging with credential masking
- **Cloudflare**: Access logs, WAF logs, rate limiting logs in dashboard
- **Hetzner**: SSH auth logs in `/var/log/auth.log`

---

## 13. Incident Response

### Playbook: Compromised API Token

```bash
# 1. Rotate SECRET_KEY to invalidate ALL tokens
echo "$(openssl rand -hex 32)" > secrets/secret_key.txt
./scripts/02-setup-secrets.sh
kubectl rollout restart deployment/api deployment/worker -n honeydue

# 2. All users will need to re-authenticate
```

### Playbook: Compromised Database Credentials

```bash
# 1. Rotate password in Neon dashboard
# 2. Update local secret file
echo "NEW_PASSWORD" > secrets/postgres_password.txt
./scripts/02-setup-secrets.sh
kubectl rollout restart deployment/api deployment/worker -n honeydue

# 3. Monitor for connection errors
kubectl logs -n honeydue -l app.kubernetes.io/name=api --tail=50 -f
```

### Playbook: Compromised Push Notification Keys

```bash
# APNs: Revoke key in Apple Developer Console, generate new .p8
cp new_key.p8 secrets/apns_auth_key.p8
./scripts/02-setup-secrets.sh
kubectl rollout restart deployment/api deployment/worker -n honeydue

# FCM: Rotate server key in Firebase Console
echo "NEW_FCM_KEY" > secrets/fcm_server_key.txt
./scripts/02-setup-secrets.sh
kubectl rollout restart deployment/api deployment/worker -n honeydue
```

### Playbook: Suspicious Pod Behavior

```bash
# 1. Isolate the pod (remove from service)
kubectl label pod SUSPICIOUS_POD -n honeydue app.kubernetes.io/name-

# 2. Capture state for investigation
kubectl logs SUSPICIOUS_POD -n honeydue > /tmp/suspicious-logs.txt
kubectl describe pod SUSPICIOUS_POD -n honeydue > /tmp/suspicious-describe.txt

# 3. Delete and let deployment recreate
kubectl delete pod SUSPICIOUS_POD -n honeydue
```

### Communication Plan

1. **Internal**: Document incident timeline in a private channel
2. **Users**: If data breach — notify affected users within 72 hours
3. **Vendors**: Revoke/rotate all potentially compromised credentials
4. **Post-mortem**: Document root cause, timeline, remediation, prevention

---

## 14. Compliance Checklist

Run through this checklist before production launch and periodically thereafter.

### Infrastructure

- [ ] Hetzner firewall allows only ports 22, 443, 6443
- [ ] SSH password auth disabled on all nodes
- [ ] fail2ban active on all nodes
- [ ] OS security updates enabled (unattended-upgrades)

```bash
# Verify
hcloud firewall describe honeydue-fw
ssh user@NODE "grep PasswordAuthentication /etc/ssh/sshd_config"
ssh user@NODE "sudo fail2ban-client status sshd"
```

### K3s Cluster

- [ ] Secret encryption enabled
- [ ] Service accounts created with no API access
- [ ] Pod disruption budgets deployed
- [ ] No default service account used by workloads

```bash
# Verify
k3s secrets-encrypt status
kubectl get sa -n honeydue
kubectl get pdb -n honeydue
kubectl get pods -n honeydue -o jsonpath='{range .items[*]}{.metadata.name}{" sa="}{.spec.serviceAccountName}{"\n"}{end}'
```

### Pod Security

- [ ] All pods: `runAsNonRoot: true`
- [ ] All containers: `allowPrivilegeEscalation: false`
- [ ] All containers: `readOnlyRootFilesystem: true`
- [ ] All containers: `capabilities.drop: ["ALL"]`
- [ ] All pods: `seccompProfile.type: RuntimeDefault`

```bash
# Verify (automated check in 04-verify.sh)
./scripts/04-verify.sh
```

### Network

- [ ] Default-deny NetworkPolicy applied
- [ ] 8+ explicit allow policies deployed
- [ ] Redis only reachable from API + Worker
- [ ] Admin only reaches API service
- [ ] Cloudflare-only middleware applied to all ingress

```bash
# Verify
kubectl get networkpolicy -n honeydue
kubectl get ingress -n honeydue -o yaml | grep cloudflare-only
```

### Authentication & Authorization

- [ ] Redis requires password
- [ ] Admin panel has basic auth layer
- [ ] API uses bcrypt for passwords
- [ ] Auth tokens have expiration
- [ ] Rate limiting on auth endpoints

```bash
# Verify Redis auth
kubectl exec -n honeydue deploy/redis -- redis-cli ping
# Expected: NOAUTH error

# Verify admin auth
kubectl get secret admin-basic-auth -n honeydue
```

### Secrets

- [ ] All secrets stored as K8s Secrets (not ConfigMap)
- [ ] Secrets encrypted at rest (K3s)
- [ ] No secrets in git history
- [ ] SECRET_KEY >= 32 characters
- [ ] Secret rotation documented

```bash
# Verify no secrets in ConfigMap
kubectl get configmap honeydue-config -n honeydue -o yaml | grep -iE 'password|secret|token|key'
# Should show only non-sensitive config keys (EMAIL_HOST, APNS_KEY_ID, etc.)
```

### TLS & Headers

- [ ] Cloudflare Full (Strict) mode enabled
- [ ] Origin cert valid and not expired
- [ ] HSTS header present with includeSubDomains
- [ ] CSP header: `default-src 'self'; frame-ancestors 'none'`
- [ ] Permissions-Policy blocks camera/mic/geo
- [ ] X-Frame-Options: DENY

```bash
# Verify headers (via Cloudflare)
curl -sI https://api.myhoneydue.com/api/health/ | grep -iE 'strict-transport|content-security|permissions-policy|x-frame'
```

### Container Images

- [ ] Multi-stage Dockerfile (no build tools in runtime)
- [ ] Non-root user in all images
- [ ] Alpine base (minimal surface)
- [ ] No secrets baked into images

```bash
# Verify non-root
kubectl get pods -n honeydue -o jsonpath='{range .items[*]}{.metadata.name}{" uid="}{.spec.securityContext.runAsUser}{"\n"}{end}'
```

### External Services

- [ ] PostgreSQL: `sslmode=require`
- [ ] B2: Scoped application key (single bucket)
- [ ] APNs: .p8 key (not .p12 certificate)
- [ ] SMTP: TLS enabled (`use_tls: true`)

---

## Quick Reference Commands

```bash
# Full security verification
./scripts/04-verify.sh

# Rotate all secrets
./scripts/02-setup-secrets.sh && \
kubectl rollout restart deployment/api deployment/worker deployment/admin -n honeydue

# Check for security events
kubectl get events -n honeydue --field-selector type=Warning

# Emergency: scale down everything
kubectl scale deployment --all -n honeydue --replicas=0

# Emergency: restore
kubectl scale deployment api -n honeydue --replicas=3
kubectl scale deployment worker -n honeydue --replicas=2
kubectl scale deployment admin -n honeydue --replicas=1
kubectl scale deployment redis -n honeydue --replicas=1
```
