# CLAUDE.md Context and working guide for the Imperva WAF demo lab. Read this first, then the files under `docs/` for depth. This file is meant to orient any AI agent (or human) picking up the project so they don't re-derive decisions that were already made and deliberately reversed once. --- ## What this project is A deliberately-vulnerable flight-booking web app sitting behind the **Imperva Cloud WAF**, used to demonstrate that the WAF detects and blocks common OWASP attacks and surfaces them in the Imperva portal. The app itself is not the point — it exists to give the WAF something realistic to protect and to produce clean, distinct blocked events on screen during a live demo. The stack runs as **rootless Podman containers** on a single small GCP instance. All public traffic is forced through Imperva; the origin only accepts connections from Imperva's IP ranges. ## Current state (as of this handoff) - Backend (`backend-api`), frontend (`frontend-vue`), and Caddy (`caddy-proxy`) are deployed and working on the GCP host. - **Edge/origin TLS is done and verified.** Imperva terminates the visitor's TLS at the edge and **re-encrypts to the origin over HTTPS** using a **self-signed cert** that Caddy serves on `:443`. Confirmed working end to end (`https://flytoouter.space/` loads with Imperva's trusted edge cert; origin serves self-signed to Imperva only). - A **local-build → ship-over-SSH → swap-on-host** deploy pipeline is defined (`deploy.sh`). See `docs/DEPLOY.md`. - **Repo scaffolded locally** by pulling the exact source that's live on the host (`~/imperva-backend`, `~/imperva-frontend`, `~/imperva-proxy`) via `scp`, so the repo matches what's actually running, not just what was intended. - **The frontend on the GCP host is still a near-empty stub** (just whitespace and a page title) — the real four-screen Vue SPA now exists in this repo's `frontend/` but has not been shipped yet. See "What's next". ## What's next The four-screen frontend and three attack endpoints described below are **built and locally verified**, but **not yet deployed to the GCP host** — the host still runs the old stub frontend and two-endpoint backend. Next step is running `./deploy.sh` from a real host terminal (see the Podman/sandbox gotcha below), then walking the `docs/DEMO_RUNBOOK.md` pre-demo checklist against `https://flytoouter.space/`. Three OWASP attacks, each blocked by an Imperva **default signature** and shown as a *distinct* event type in the portal: 1. SQL Injection — `/api/v1/users?q=` (Passenger / Admin Lookup screen) 2. Reflected XSS — `/api/v1/search?term=` (Flight Search screen) 3. Path Traversal — `/api/v1/download?file=` (Boarding Pass Download screen) Plus a Login screen (`/api/v1/login`) kept as the credential-stuffing bonus (not in the core demo — needs a custom rate policy, see DECISIONS.md D7). See `docs/DEMO_RUNBOOK.md` for payloads and expected portal classifications. **Resolved:** confirmed via `podman inspect` and the host's `.bash_history` that `backend-api` and `frontend-vue` have **no bind mounts** — `demo.db` is created inside the image by `init_db()` on container start, not mounted from the host. The real run commands were: ``` podman run -d --name backend-api --restart always -p 8000:8000 backend-api podman run -d --name frontend-vue --restart always -p 8080:8080 frontend-vue ``` `deploy.sh`'s `swap()` reproduces exactly this — `--name` and `-p` only, nothing else needed. --- ## Key facts an agent needs (these override the original handoff doc) - **Host:** GCP `e2-micro`, Ubuntu, static IP `35.203.147.44`, domain `flytoouter.space`. - **Host user:** `matt`. **Rootless Podman socket is under UID `1001`, not 1000** — the original architecture doc says 1000; that was wrong for this host. Prefer the connection alias so the UID is irrelevant. - **Podman connection alias (local → host):** `gcp-lab`, already configured and set as default: `ssh://matt@35.203.147.44:22/run/user/1001/podman/podman.sock` key: `~/.ssh/google_compute_engine`. - **SSH ingress** is locked to the operator's static residential IP. - **HTTP/HTTPS ingress** (`allow-imperva-waf`) is restricted to Imperva's published IPv4 CIDRs only; all `0.0.0.0/0` rules were purged. The vulnerable app is therefore never directly reachable — attacks must traverse the WAF, which is what makes them appear in the portal. - **e2-micro cannot build the Vue multi-stage image** (npm build is memory-hungry even with the swapfile). Always build locally and ship the finished image. Never build on the host. ## Repo layout (intended) ``` . ├── CLAUDE.md # this file ├── deploy.sh # local build → ship over ssh → swap on host ├── backend/ # FastAPI app + Containerfile ├── frontend/ # Vue SPA + multi-stage Containerfile ├── imperva-proxy/ │ ├── Caddyfile # origin reverse proxy (mirrors host ~/imperva-proxy/) │ └── certs/ # origin.crt / origin.key (self-signed; do NOT commit key) └── docs/ ├── ARCHITECTURE.md # topology, TLS legs, request flow, network ├── DECISIONS.md # every design choice + why + rejected alternatives ├── DEPLOY.md # the ship-over-ssh workflow in detail └── DEMO_RUNBOOK.md # attacks, payloads, expected portal events ``` > Note: On the **host**, the Caddyfile and certs live at `~/imperva-proxy/`. Keep the > repo copy in sync with what's mounted into `caddy-proxy`. ## Conventions / gotchas (learned the hard way — see DECISIONS.md) - **One catch-all `:80`/`:443` site block in the Caddyfile.** With `auto_https off`, a hostname block (e.g. `flytoouter.space { }`) collapses onto the same port as a `:443`/`:80` block and Caddy fails with `ambiguous site definition`. Host-based routing, if ever needed, goes *inside* the single block as a matcher — never as a second site block. - **Use `handle`, not `handle_path`,** for `/api/*` so the `/api` prefix is preserved for the backend. - **Mount paths in `podman run` must be absolute** (`/home/matt/...`), not `~`. A tilde that doesn't expand creates an empty anonymous mount and Caddy dies with `open /etc/caddy/certs/origin.crt: no such file or directory`. - **Origin cert is self-signed on purpose.** Only Imperva sees it, so no browser warnings. Never put the self-signed cert on the *edge* (visitor-facing) side — that stays Imperva's managed cert. - **`https://35.203.147.44/` resetting is expected** when the origin serves only the intended port and/or your source IP isn't an Imperva CIDR. Test the origin from the host with `curl -vk https://localhost/`, and test the real path via `https://flytoouter.space/`. - **Do not commit `origin.key`.** Add `imperva-proxy/certs/*.key` to `.gitignore`. - **`podman` builds must run on the real host terminal, not inside a sandboxed coding-agent shell.** If you're an AI agent editing this repo from within a sandbox, check whether `podman`/`docker` resolves before assuming you can run `deploy.sh` yourself. On this project's dev machine, the agent sandbox is a *nested* container (different userland/libc than the real host) sitting on the same physical disk as `~/workspace` — so files edited in the sandbox are real and immediately usable, but a `podman` binary installed *inside* the sandbox will collide with the host's existing rootless container storage (`~/.local/share/containers`, shared because it's the same disk) and fail with a `database configuration mismatch` error. Don't try to work around this by resetting or pointing storage elsewhere — it risks corrupting the host's real container state. Edit code from the sandbox; run `./deploy.sh` from a normal terminal on the actual workstation. ## How to deploy (short version) From the repo root, with the `gcp-lab` connection present: ```bash ./deploy.sh # build both images, ship over ssh, swap both containers VERSION=v2 ./deploy.sh # same, but tag images v2 for a named rollback point ./deploy.sh --restart-caddy # after editing the Caddyfile (new routes) ``` Full detail, including per-step flags and the layer-shipping trade-off, in `docs/DEPLOY.md`.