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.
This commit is contained in:
matt committed 2026-07-29 11:08:56 -07:00
commit 4d60fdd139
28 files changed
+2609

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
# 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`.