Files
matt 4d60fdd139 Initial commit: Imperva WAF demo lab
Flight-booking app scaffolded from the source running on the GCP host,
plus the four-screen frontend and three OWASP attack endpoints (SQLi,
reflected XSS, path traversal) built and locally verified against a
live uvicorn/vite dev pair. Includes deploy.sh (local build -> ship
over ssh -> swap on host) and docs/ carried over from the handoff.
2026-07-29 11:08:56 -07:00

170 lines
7.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design Decisions & Rationale
An ADR-style log of the choices made in this project, *why* they were made, and what was
rejected. Several of these were reversed mid-stream — the reversals are kept here on
purpose so nobody re-walks a dead end.
---
## D1 — WAF terminates client TLS; origin does not do public HTTPS
**Decision:** Imperva handles all public-facing TLS. The origin never runs ACME /
Let's Encrypt and never serves a publicly-trusted cert.
**Why:** The whole point of the lab is that Imperva is the edge. Having the origin also
try to obtain public certs is redundant, and on this locked-down host ACME would fail
anyway (egress is restricted and ingress is Imperva-only). Keeping cert management at
the edge is simpler and matches the intended topology.
---
## D2 — First attempt: plain-HTTP origin (REVERSED)
**Decision (later reversed):** Have Caddy serve only plain HTTP on `:80` and let
Imperva connect to the origin over HTTP; `auto_https off`.
**Why it was chosen:** The user's initial goal was "forward https/http traffic from the
WAF without Caddy doing SSL." The cleanest literal reading is: Imperva terminates TLS,
origin speaks plain HTTP, Caddy needs no cert at all.
**Why it was reversed:** The user then wanted a certificate they could upload to /
associate with Imperva (origin re-encryption). Plain HTTP has no cert to produce, so we
moved to a self-signed HTTPS origin (D5).
**Artifacts from this phase worth remembering:**
- `https://35.203.147.44/` returning `PR_CONNECT_RESET_ERROR` was *expected* here —
nothing was listening on 443. That reset is not a bug; it's the correct result of
hitting a port with no listener. Test HTTP origins with `curl -v http://localhost/`
from the host, and the real path via the hostname.
---
## D3 — Single catch-all site block in the Caddyfile
**Decision:** Exactly one site block (`:443`, or `:80` in the HTTP phase). No separate
hostname block.
**Why:** With `auto_https off`, a hostname block like `flytoouter.space { }` no longer
routes to 443 automatically — it binds the same plain port as an explicit `:80`/`:443`
block. Two blocks fighting over one port produces:
```
Error: adapting config using caddyfile: ambiguous site definition: :80
```
This actually happened (a leftover domain block from the pre-WAF config remained in the
file). The container crash-looped, spamming the error hundreds of times — that volume is
just the restart policy retrying, not hundreds of distinct problems.
**Rejected:** keeping host-specific blocks. If host-based routing is ever wanted (e.g.
rejecting requests whose Host isn't `flytoouter.space` as origin hardening), it goes
*inside* the single block as a `@host` matcher, never as a second site block.
---
## D4 — `handle` not `handle_path` for `/api/*`
**Decision:** Route the API with `handle /api/* { reverse_proxy … }`.
**Why:** `handle_path` strips the matched prefix; `handle` preserves it. The backend's
routes are defined under `/api/v1/...`, so the prefix must survive the proxy hop.
---
## D5 — Self-signed cert on the origin, generated with OpenSSL (not Caddy internal CA)
**Decision:** Generate the origin cert with `openssl` (self-signed, RSA 2048, 3650
days, SAN = DNS + IP) and hand the files to Caddy via `tls <crt> <key>`.
**Why OpenSSL over Caddy's `tls internal`:** `tls internal` mints a cert via Caddy's
internal CA and buries it in `/data/caddy/pki/...`, which is awkward to export. The lab
needs the cert *as a file on disk* — to upload to / associate with Imperva and to have
explicit control over the SAN. OpenSSL gives that in one command.
**Why self-signed is acceptable here:** Only Imperva connects to the origin. Browsers
only ever see Imperva's edge cert (D1). So the self-signed cert causes zero
visitor-facing warnings. The critical rule: **the self-signed cert belongs only on the
re-encryption leg, never on the edge/visitor leg.**
**SAN includes both DNS and IP** so Imperva matches regardless of whether it addresses
the origin by hostname or by `35.203.147.44`.
---
## D6 — Debugging the "cert not found" start failure
**Symptom:** `caddy validate` reported
`open /etc/caddy/certs/origin.crt: no such file or directory`, and curl to `:443` reset.
**Root cause & rule:** The config was fine; the cert file wasn't visible *inside the
container*. Two common causes on rootless Podman:
1. **Tilde didn't expand** in `-v ~/imperva-proxy/certs:...`, creating an empty
anonymous mount. Fix: use absolute paths (`/home/matt/...`).
2. Stale `:Z` SELinux relabel state from an earlier container. Fix: recreate the
container.
The TLS reset was a *symptom* of Caddy refusing to start the HTTPS server, not an
independent TLS problem. Always read the `validate` / `podman logs` output first.
---
## D7 — Three OWASP attacks for the demo: SQLi, Reflected XSS, Path Traversal
**Decision:** Demo exactly these three.
**Why these three:** Imperva blocks all three with **default, out-of-the-box
signatures** (no custom rules needed), and each surfaces as a **different, clearly
labeled event** in the portal (SQL Injection / Cross-Site Scripting / Illegal Resource
Access). Three distinct classifications reads as comprehensive coverage rather than
three flavors of one thing.
**Why credential stuffing / rate-limiting was left OUT of the demo set:** It needs a
**custom rate policy** configured in Imperva, not a signature match. It doesn't produce
the clean one-payload-one-block moment the other three do. Kept as an optional "bonus"
for later if we want to showcase custom rules.
---
## D8 — Deploy via `podman save | ssh podman load`, not a container registry
**Decision:** Build locally, pipe the image over the existing SSH connection into the
host's Podman, then swap the container via the `gcp-lab` connection alias.
**Why:** Zero new infrastructure. All the pieces already exist (SSH key, locked
ingress, remote Podman socket, connection alias). A registry (GCP Artifact Registry or
GHCR) would add credential management on both ends and, for Artifact Registry, IAM
wiring (service account + `artifactregistry.reader`) that's easy to get subtly wrong.
**Trade-off accepted:** `podman save` ships **every layer every time**, so each deploy
moves the full image even for a one-line change (~20–40s on this lab). The registry's
layer-caching win only pays off at high deploy frequency, which a demo lab isn't.
**Upgrade path:** If this ever becomes team-touched or high-frequency, migrating to
Artifact Registry is clean — only the *ship* step changes (`push`/`pull` instead of
`save | ssh load`); the rest of `deploy.sh` is unchanged.
---
## D9 — Build locally, never on the host
**Decision:** All image builds happen on the local workstation.
**Why:** The `e2-micro` (shared-core, burstable) doesn't have the memory to run the Vue
multi-stage build (npm install + build) reliably, even with the swapfile. Building
locally and shipping the finished image sidesteps OOM kills entirely.
---
## D10 — Frontend screens map 1:1 to attacks
**Decision:** Give the flight-booking SPA four screens, each a natural home for one
attack, so the demo feels like poking a real product rather than hitting raw endpoints:
- Passenger/admin lookup → SQLi (`/api/v1/users?q=`)
- Flight search results → Reflected XSS (`/api/v1/search?term=`)
- Boarding-pass download → Path Traversal (`/api/v1/download?file=`)
- Login → existing endpoint, kept as the credential-stuffing bonus
**Why:** A believable UI makes the "exploitable without the WAF, blocked with it"
narrative land better in a live demo than curling endpoints.