# Architecture ## Overview Single GCP `e2-micro` running three rootless Podman containers under user `matt`. Imperva Cloud WAF sits in front of everything. The origin is network-isolated so that the only things that can reach the app are Imperva's edge nodes. ``` Visitor ──TLS──▶ Imperva Cloud WAF ──TLS (re-encrypt)──▶ Caddy :443 ──HTTP──▶ backend/frontend (terminates, (self-signed cert) inspects, re-encrypts) ``` ## The two TLS legs (this is the crux of the design) There are two independent TLS connections, and they use different certificates. Mixing them up is the single easiest way to break this setup. **Leg 1 — Visitor → Imperva (edge).** Imperva presents a publicly-trusted certificate for `flytoouter.space` to browsers. This is Imperva's *managed* cert (auto-provisioned by Imperva). It must be publicly trusted or every visitor gets a browser warning. We do **not** touch this with our self-signed cert. **Leg 2 — Imperva → origin (re-encryption).** Imperva opens a *second* TLS connection back to the GCP origin and inspects/re-encrypts before forwarding. Caddy serves this leg on `:443` with a **self-signed certificate**. Imperva is the only client that ever sees this cert, so self-signed is fine and produces no visitor-facing warnings. DNS for `flytoouter.space` must point at **Imperva**, not at `35.203.147.44`. If it resolves straight to the origin, browsers try to negotiate TLS with the self-signed origin cert and fail. ## Request flow through the origin Caddy (`caddy-proxy`) is the only container bound to a public port. It terminates the re-encryption TLS and reverse-proxies over plain HTTP to the app containers via the `host.containers.internal` alias on the default rootless bridge network: - `handle /api/*` → `host.containers.internal:8000` (FastAPI backend) - `handle /*` → `host.containers.internal:8080` (Vue frontend) `handle` (not `handle_path`) is used so the `/api` prefix is preserved for the backend routes (`/api/v1/users`, etc.). ## Container stack | Container | Image | Port | Role | |----------------|------------------------------------|-------------|--------------------------------------------------| | `caddy-proxy` | `docker.io/library/caddy:2-alpine` | `443:443` | TLS termination (re-encryption leg) + reverse proxy | | `backend-api` | custom Python 3.11-slim (FastAPI) | `8000:8000` | REST API + local SQLite `demo.db` | | `frontend-vue` | custom multi-stage (Node build → Nginx) | `8080:8080` | Vue SPA (flight booking UI) | Caddy currently serves only `:443`. There is intentionally no `:80` site block (see DECISIONS.md — a stray `:80` block under `auto_https off` caused the `ambiguous site definition` failure). If Imperva needs an HTTP health check, an `:80` block can be added back as a separate, non-conflicting listener. ## Network / edge security - **Origin target tag:** `imperva-origin`. - **`allow-imperva-waf`:** HTTP/HTTPS ingress restricted to Imperva's published IPv4 CIDR blocks (e.g. `199.83.128.0/21`, `198.143.32.0/19`, …). Confirm `tcp:443` is in the allowed ports since the origin path is now 443-only. - **`allow-home-ssh`:** SSH (22) locked to the operator's static residential IP. - All default open (`0.0.0.0/0`) rules purged. Because the origin only answers Imperva CIDRs, the deliberately-vulnerable endpoints are unreachable directly. Every attack must pass through the WAF — which is exactly why they register as blocked events in the portal. ## Host hardening (from original build) - `/swapfile` mounted via `/etc/fstab` to mitigate OOM kills on the burstable e2-micro. - `loginctl enable-linger matt` so rootless containers persist across reboots. - `sysctl net.ipv4.ip_unprivileged_port_start=80` to let rootless Caddy bind low ports. ## Cert details (origin, Leg 2) - Self-signed, RSA 2048, 3650-day validity. - **SAN covers both** `DNS:flytoouter.space` and `IP:35.203.147.44`, so whichever way Imperva addresses the backend, the cert matches. - Files live on the host at `~/imperva-proxy/certs/{origin.crt,origin.key}` and are bind-mounted read-only into Caddy at `/etc/caddy/certs/`.