This guide is for NGINX administrators running F5 WAF for NGINX (NGINX App Protect) — you know
nginx.conf, server/location blocks, and a nginx -s reload, but you don't need to hand-write an
App Protect policy or any DevOps tooling. The copilot does that part.
What it does: it finds an exploitable vulnerability in your app, and — behind a human gate — puts an App Protect band-aid in front of the app in minutes, proves the exploit is actually blocked, and takes the band-aid off again when your code fix ships. It is the NGINX twin of the BIG-IP flow in BIGIP.md — the same finding, same band-aid, emitted for the WAF you already run.
Why it's safe to try (read this first):
- It only touches its own vhost. Every file the copilot writes is named
vpcopilot-…and lives in its own managed include directory. It never edits your existingserverblocks or policies — removing its vhost is a clean undo. nginx -tgates every change. Nothing is reloaded unless the box's own config test passes.- It rolls itself back. If an applied band-aid doesn't actually block the exploit, the copilot removes it automatically and reloads. It never leaves a control that doesn't work.
- The copilot owns one
server(a private sandbox). It manages a single named vhost that reverse-proxies to your app, and drops each band-aid into avpcopilot-active/include on thatlocation. It can't reach your other vhosts, and removing its vhost is a clean undo. - You never write an App Protect policy. SSH is just how the copilot ships the policy file and
reloads — you point it at your app's origin, it does the rest. If you've ever set
app_protect_policy_fileon alocation, that's exactly what it's automating. - Nothing is kept unless it's proven. The copilot fires the finding's real exploit at the app through the box after applying the band-aid; it only leaves the control on if the exploit is blocked and normal traffic still works. (App Protect loads a policy into its enforcer a second or two after a reload, so the copilot waits for enforcement to go live before it decides.)
| Requirement | How to check |
|---|---|
| NGINX Plus + App Protect installed | nginx -V 2>&1 | grep app-protect, or the copilot's nginx-lab status reports whether the module is loaded. The copilot attaches policies — it does not install App Protect. |
| SSH access with reload rights | A user who can write the policy/include dirs and run nginx -t + the reload (root, or sudo NOPASSWD scoped to nginx -s reload). A copy-only user can't apply. |
| Reachability | Where the copilot runs must reach the box over SSH. If it's not directly reachable, forward the port (an SSH/SSM tunnel) and point NGINX_SSH_HOST at your end of it. |
| The validation URL goes through the box | The app URL you validate against must resolve to the NGINX vhost — not a CDN in front, not the origin behind — or the exploit probe proves nothing. |
| Version | NGINX Plus with App Protect (the v4-style module: ngx_http_app_protect_module.so + raw-JSON app_protect_policy_file). |
Give the copilot how to reach the box over SSH — in the console (vpcopilot console, ⚙ Setup)
or your .env:
NGINX_SSH_HOST # e.g. nginx.internal (or your tunnel's near end, 127.0.0.1)
NGINX_SSH_PORT # default 22
NGINX_SSH_USER # a user with write + reload rights
NGINX_SSH_KEY # path to the private key (preferred) — or NGINX_SSH_PASSWORD
NGINX_RELOAD_CMD # default: sudo nginx -s reload
NGINX_POLICY_DIR # default: /etc/app_protect/conf
NGINX_INCLUDE_DIR # default: /etc/nginx/conf.d
Test the connection — this tells you whether the box answers over SSH and whether the App Protect module is loaded:
vpcopilot nginx-lab statusThis creates the private vhost that fronts your app — a reverse proxy to your app's origin, whose
location includes the copilot's (empty) managed policy dir, with no band-aid yet. Idempotent:
run it again and it converges.
vpcopilot nginx-lab create \
--server vpcopilot.lab \ # the server_name the copilot owns
--origin 10.0.0.20:8080 # where your app actually runs (host:port)vpcopilot nginx-lab rm --server vpcopilot.lab removes it when you're done. The nginx catch-all _
server is refused outright, and any server in VPCOPILOT_PROTECTED_NGINX_SITES needs
--allow-protected-site.
Point the copilot at your app's code (read-only, no changes) so it has findings to mitigate. See TRY_IT.md — start on a known-vulnerable app (VAmPI / crAPI) if you want to see it work first.
Preview first (nothing changes — the copilot stages the policy and runs nginx -t, then rolls
the staging back):
vpcopilot apply-nginx --finding <finding-id> --url http://my-app.internal --dry-runThen apply it for real. By default this is a safe smoke test: it writes the policy + managed
include, reloads, fires the exploit through the box, shows you before → after blocked, and then
removes it again. Add --keep to leave it enforcing.
vpcopilot apply-nginx --finding <finding-id> --url http://my-app.internal --keepIf the band-aid doesn't block the exploit, the copilot removes it and reloads — it won't leave a control that isn't working.
The band-aid is temporary. When the real fix is deployed, take it off (the app stays up — only the managed include comes off):
vpcopilot retire-nginx --finding <finding-id> --server vpcopilot.lab- The copilot only ever writes
vpcopilot-files in its own include dir — a change or a detach can never touch your ownserverblocks or anapp_protect_policy_fileyou set. The nginx catch-all_server is refused unconditionally; any server inVPCOPILOT_PROTECTED_NGINX_SITESneeds an explicit--allow-protected-site. nginx -tgates every change; auto-rollback removes any band-aid that doesn't block.- Every change is on the record — what changed, which vulnerability justified it, whether it's still live, and who ran it — exportable as an evidence bundle (see AUDIT.md).
| You see | What it means / fix |
|---|---|
unreachable |
The box isn't reachable over SSH from here — check NGINX_SSH_HOST/PORT/KEY, or open a tunnel and point at its near end. |
App Protect: NOT loaded |
The box answers but the App Protect module isn't loaded — load_module modules/ngx_http_app_protect_module.so; and confirm the enforcer service is running. |
nginx -t failed |
The box rejected the config — the copilot never reloads a config that fails the test, and shows you the box's own error. |
no App Protect band-aid |
This finding's best control (e.g. rate-limit, bot) has no App Protect form — mitigate it on F5 Distributed Cloud, or ship the code fix. |
did not block — rolled back |
The band-aid attached and reloaded but the exploit still got through (even after waiting for enforcement) — the copilot removed it rather than leave a control that doesn't work. |
The same three forms as BIG-IP, each proven on a live NGINX Plus + App Protect box (R37):
| Form | Control | What it does |
|---|---|---|
| value-constraint | service_policy |
rejects a request whose parameter is out of range (e.g. a negative transfer amount) |
| response-masking | waf_data_guard |
masks leaked secrets (PAN / SSN) in the response |
| API-contract | api_schema |
blocks a served-but-undocumented (off-contract) endpoint |
Signature sets (waf) are honestly declined — NAP keeps freshly-imported signatures in staging
(log-only), so a signature band-aid would look applied and block nothing. Rate-limiting,
malicious-user, and bot defense have no App Protect object and are F5 Distributed Cloud features (or a
code fix).
In the console: expand Apply on your own NGINX (App Protect) in the ④ Mitigate step — pick a finding, apply, watch it validate, keep or retire. The commands above are the headless equivalent.
