sanitize public mirror: drop personal IP and infra/customer hostnames
All checks were successful
Build and push coraza-spoa / Build-and-Push (push) Successful in 1m49s
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 1m55s

- trusted_ips.{list,map}: replace home IP with 127.0.0.1 + usage notes
- skill: resolve deploy host from gitignored target-host.local, ask if unset
  (no hardcoded server FQDN); customer host in WAF test -> <live-vhost>
- README / coraza README: registry FQDN in run examples -> placeholder
- 403 block page: drop hardcoded support link -> contact provider support
- CLAUDE.md: note whitelist files ship without real IPs
- .gitignore: ignore target-host.local and *.local

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 06:32:15 -07:00
parent 158ad3bde8
commit 1ff51da6f0
8 changed files with 65 additions and 17 deletions

View File

@@ -23,6 +23,32 @@ Do not skip the verify step. The container can come up "healthy" while still ser
--- ---
## Step 0a — Resolve the target host (never hardcoded)
This skill deliberately does **not** bake in a server hostname — this repo is mirrored to a public remote, so a real FQDN in the skill would leak into commits. Instead, resolve the deploy target into a `DEPLOY_HOST` shell variable that every `ssh` command below uses.
```bash
HOST_FILE=".claude/skills/haproxy-manager-deploy/target-host.local"
DEPLOY_HOST="$(cat "$HOST_FILE" 2>/dev/null)"
```
- **If `$DEPLOY_HOST` is non-empty**, use it — that's the user's saved target. The file is gitignored, so the real hostname never lands in a commit.
- **If it's empty**, ask the user which server this deploy targets (e.g. production vs. staging) and what its hostname or SSH alias is. Then offer to save it so future deploys don't have to ask:
```bash
echo 'the-host-they-gave.example' > "$HOST_FILE" # gitignored — safe to store the real FQDN here
```
Confirm `$DEPLOY_HOST` is set before running any `ssh` step:
```bash
[ -n "$DEPLOY_HOST" ] || echo "DEPLOY_HOST not set — ask the user for the target server"
```
All commands below assume the variable is set in the same shell session (`ssh root@"$DEPLOY_HOST" ...`).
---
## Step 0 — Confirm before pushing ## Step 0 — Confirm before pushing
If the user just said "deploy" or "ship the haproxy fix", confirm what's actually changing: a template, the Python manager, the coraza-spoa subdir (separate image, separate workflow), or a static asset. Look at `git status` and `git diff` and read the diff back to the user if it's non-trivial. If the user just said "deploy" or "ship the haproxy fix", confirm what's actually changing: a template, the Python manager, the coraza-spoa subdir (separate image, separate workflow), or a static asset. Look at `git status` and `git diff` and read the diff back to the user if it's non-trivial.
@@ -72,7 +98,7 @@ Pushing immediately triggers the Gitea Actions build.
The Go build inside coraza-spoa takes ~2-3 minutes; the haproxy-manager-base build is faster (~1-2 min). Don't bother polling the runs UI — just pull on the target server until the digest changes: The Go build inside coraza-spoa takes ~2-3 minutes; the haproxy-manager-base build is faster (~1-2 min). Don't bother polling the runs UI — just pull on the target server until the digest changes:
```bash ```bash
ssh root@deploy-target.example 'until docker pull -q repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest 2>&1 | tail -1 | grep -qE "Image is up to date|Status: Downloaded"; do sleep 15; done' ssh root@"$DEPLOY_HOST" 'until docker pull -q repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest 2>&1 | tail -1 | grep -qE "Image is up to date|Status: Downloaded"; do sleep 15; done'
``` ```
`-q` suppresses the noisy layer progress so the grep can match cleanly. If you started this command before the CI build finished, it'll loop until the new image lands; once the digest matches, it exits. `-q` suppresses the noisy layer progress so the grep can match cleanly. If you started this command before the CI build finished, it'll loop until the new image lands; once the digest matches, it exits.
@@ -80,7 +106,7 @@ ssh root@deploy-target.example 'until docker pull -q repo.anhonesthost.net/cloud
To confirm you got the new image, check the image-creation time vs your push: To confirm you got the new image, check the image-creation time vs your push:
```bash ```bash
ssh root@deploy-target.example 'docker images repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base --format "{{.CreatedSince}}"' ssh root@"$DEPLOY_HOST" 'docker images repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base --format "{{.CreatedSince}}"'
``` ```
It should say "X minutes ago" matching the build wait, not "yesterday". It should say "X minutes ago" matching the build wait, not "yesterday".
@@ -93,10 +119,10 @@ The image is `gcr.io/distroless/static-debian12:nonroot`-based, no shell. To pee
```bash ```bash
# haproxy-manager-base (has sh): # haproxy-manager-base (has sh):
ssh root@deploy-target.example 'docker run --rm --entrypoint sh repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest -c "ls /haproxy/errors/ && head -5 /haproxy/errors/403-waf.html"' ssh root@"$DEPLOY_HOST" 'docker run --rm --entrypoint sh repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest -c "ls /haproxy/errors/ && head -5 /haproxy/errors/403-waf.html"'
# coraza-spoa (distroless, no sh) — use docker create + docker cp instead: # coraza-spoa (distroless, no sh) — use docker create + docker cp instead:
ssh root@deploy-target.example 'docker create --name _peek repo.anhonesthost.net/cloud-hosting-platform/coraza-spoa:latest && docker cp _peek:/etc/coraza/config.yaml - | tar xO; docker rm _peek' ssh root@"$DEPLOY_HOST" 'docker create --name _peek repo.anhonesthost.net/cloud-hosting-platform/coraza-spoa:latest && docker cp _peek:/etc/coraza/config.yaml - | tar xO; docker rm _peek'
``` ```
This step exists because the CI build can succeed but ship the wrong file (wrong commit pulled, build cache issue, etc.). Catching it here is one step earlier than catching it from a customer report. This step exists because the CI build can succeed but ship the wrong file (wrong commit pulled, build cache issue, etc.). Catching it here is one step earlier than catching it from a customer report.
@@ -106,12 +132,12 @@ This step exists because the CI build can succeed but ship the wrong file (wrong
## Step 6 — Recreate the container ## Step 6 — Recreate the container
```bash ```bash
ssh root@deploy-target.example '/root/whp/scripts/container-manager.sh recreate haproxy-manager' ssh root@"$DEPLOY_HOST" '/root/whp/scripts/container-manager.sh recreate haproxy-manager'
``` ```
For coraza-spoa changes: For coraza-spoa changes:
```bash ```bash
ssh root@deploy-target.example '/root/whp/scripts/container-manager.sh recreate coraza-spoa' ssh root@"$DEPLOY_HOST" '/root/whp/scripts/container-manager.sh recreate coraza-spoa'
``` ```
`container-manager.sh recreate` does: stop, remove, docker pull (idempotent if already pulled), start with the right flags from settings.json. **It reads `/docker/whp/settings.json` for things like `coraza_waf.mode`**, so if the user has toggled mode while you were building, the recreated container reflects the current setting — not whatever it was when you started. `container-manager.sh recreate` does: stop, remove, docker pull (idempotent if already pulled), start with the right flags from settings.json. **It reads `/docker/whp/settings.json` for things like `coraza_waf.mode`**, so if the user has toggled mode while you were building, the recreated container reflects the current setting — not whatever it was when you started.
@@ -123,7 +149,7 @@ ssh root@deploy-target.example '/root/whp/scripts/container-manager.sh recreate
For haproxy-manager: For haproxy-manager:
```bash ```bash
ssh root@deploy-target.example ' ssh root@"$DEPLOY_HOST" '
echo "=== container ===" echo "=== container ==="
docker ps --filter name=haproxy-manager --format "image: {{.Image}} status: {{.Status}}" docker ps --filter name=haproxy-manager --format "image: {{.Image}} status: {{.Status}}"
echo "=== healthy ===" echo "=== healthy ==="
@@ -154,18 +180,20 @@ If your change affects what a visitor sees (block pages, redirects, security res
```bash ```bash
# Inject a temporary ACL that forces the WAF deny path on a custom header, # Inject a temporary ACL that forces the WAF deny path on a custom header,
# fire one request, observe the rendered response, then revert + reload. # fire one request, observe the rendered response, then revert + reload.
ssh root@deploy-target.example ' ssh root@"$DEPLOY_HOST" '
docker exec haproxy-manager cp /etc/haproxy/haproxy.cfg /tmp/cfg-bak docker exec haproxy-manager cp /etc/haproxy/haproxy.cfg /tmp/cfg-bak
docker exec haproxy-manager sh -c "sed -i \"/http-request send-spoe-group coraza coraza-req/a\\\\ http-request set-var(txn.coraza.action) str(deny) if { req.hdr(x-force-waf-block) -m str yes }\" /etc/haproxy/haproxy.cfg" docker exec haproxy-manager sh -c "sed -i \"/http-request send-spoe-group coraza coraza-req/a\\\\ http-request set-var(txn.coraza.action) str(deny) if { req.hdr(x-force-waf-block) -m str yes }\" /etc/haproxy/haproxy.cfg"
docker exec haproxy-manager sh -c "echo reload | socat stdio /tmp/haproxy-cli" >/dev/null docker exec haproxy-manager sh -c "echo reload | socat stdio /tmp/haproxy-cli" >/dev/null
sleep 1 sleep 1
curl -sSk -D - -H "x-force-waf-block: yes" -H "Host: example.com" "https://localhost/" | head -40 curl -sSk -D - -H "x-force-waf-block: yes" -H "Host: <live-vhost>" "https://localhost/" | head -40
# revert # revert
docker exec haproxy-manager cp /tmp/cfg-bak /etc/haproxy/haproxy.cfg docker exec haproxy-manager cp /tmp/cfg-bak /etc/haproxy/haproxy.cfg
docker exec haproxy-manager sh -c "echo reload | socat stdio /tmp/haproxy-cli" >/dev/null docker exec haproxy-manager sh -c "echo reload | socat stdio /tmp/haproxy-cli" >/dev/null
' '
``` ```
**Pick a real `<live-vhost>`.** The `Host:` header must match a domain currently served by this haproxy-manager, or the request won't route to the WAF path. Don't hardcode a customer hostname in this skill — pull a live one at test time (any entry from the panel's domain list, or `docker exec haproxy-manager ls /etc/letsencrypt/live`) and substitute it.
**The injection point matters.** Insert AFTER `http-request send-spoe-group coraza coraza-req`, because the SPOE call overwrites `txn.coraza.action` based on the real Coraza verdict — if you inject before it, your override is wiped. **The injection point matters.** Insert AFTER `http-request send-spoe-group coraza coraza-req`, because the SPOE call overwrites `txn.coraza.action` based on the real Coraza verdict — if you inject before it, your override is wiped.
**The reload mechanism matters.** Use `echo reload | socat stdio /tmp/haproxy-cli` — the container is python-based but doesn't have `kill` in PATH, and `docker kill --signal=HUP` signals the python manager (PID 1), not haproxy. **The reload mechanism matters.** Use `echo reload | socat stdio /tmp/haproxy-cli` — the container is python-based but doesn't have `kill` in PATH, and `docker kill --signal=HUP` signals the python manager (PID 1), not haproxy.

4
.gitignore vendored
View File

@@ -37,3 +37,7 @@ ENV/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Local-only deploy config (never commit real hostnames)
.claude/skills/haproxy-manager-deploy/target-host.local
*.local

View File

@@ -94,7 +94,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `trusted_ips.list` — Source IP whitelist for rate limit bypass (one CIDR/IP per line) - `trusted_ips.list` — Source IP whitelist for rate limit bypass (one CIDR/IP per line)
- `trusted_ips.map` — Real IP whitelist for proxy-header matching (format: `<IP> 1`) - `trusted_ips.map` — Real IP whitelist for proxy-header matching (format: `<IP> 1`)
- Both files are baked into the Docker image via `COPY` in the Dockerfile - Both files are baked into the Docker image via `COPY` in the Dockerfile
- Currently contains phone system IP `127.0.0.1` - Ship as comment-only templates (no real IPs). Add trusted IPs locally and do **not** commit them — this repo is mirrored publicly. Entries persist in the `/etc/haproxy` named volume across recreates
### Timeout Hardening (hap_header.tpl) ### Timeout Hardening (hap_header.tpl)

View File

@@ -6,10 +6,10 @@ A Flask-based API service for managing HAProxy configurations with dynamic SSL c
To run the container: To run the container:
```bash ```bash
# Without API key authentication (default) # Without API key authentication (default)
docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy --name haproxy-manager repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy --name haproxy-manager your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest
# With API key authentication (recommended for production) # With API key authentication (recommended for production)
docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy -e HAPROXY_API_KEY=your-secure-api-key-here --name haproxy-manager repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest docker run -d -p 80:80 -p 443:443 -p 8000:8000 -v lets-encrypt:/etc/letsencrypt -v haproxy:/etc/haproxy -e HAPROXY_API_KEY=your-secure-api-key-here --name haproxy-manager your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest
``` ```
## Features ## Features
@@ -402,7 +402,7 @@ docker run -d \
-e HAPROXY_DEFAULT_MAIN_MESSAGE="This website is currently under construction and will be available soon." \ -e HAPROXY_DEFAULT_MAIN_MESSAGE="This website is currently under construction and will be available soon." \
-e HAPROXY_DEFAULT_SECONDARY_MESSAGE="Please check back later or contact us for more information." \ -e HAPROXY_DEFAULT_SECONDARY_MESSAGE="Please check back later or contact us for more information." \
--name haproxy-manager \ --name haproxy-manager \
repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest
``` ```
## Example Usage ## Example Usage
@@ -416,7 +416,7 @@ docker run -d \
-v haproxy:/etc/haproxy \ -v haproxy:/etc/haproxy \
-e HAPROXY_API_KEY=your-secure-api-key-here \ -e HAPROXY_API_KEY=your-secure-api-key-here \
--name haproxy-manager \ --name haproxy-manager \
repo.anhonesthost.net/cloud-hosting-platform/haproxy-manager-base:latest your-registry.example.com/cloud-hosting-platform/haproxy-manager-base:latest
# Add a domain # Add a domain
curl -X POST http://localhost:8000/api/domain \ curl -X POST http://localhost:8000/api/domain \

View File

@@ -32,7 +32,7 @@ docker run -d \
--network client-net \ --network client-net \
--restart unless-stopped \ --restart unless-stopped \
-v /var/log/coraza:/var/log/coraza \ -v /var/log/coraza:/var/log/coraza \
repo.anhonesthost.net/cloud-hosting-platform/coraza-spoa:latest your-registry.example.com/cloud-hosting-platform/coraza-spoa:latest
``` ```
Then on the `haproxy-manager` container, add the env var: Then on the `haproxy-manager` container, add the env var:

View File

@@ -100,7 +100,7 @@
<div class="owner"> <div class="owner">
<h2>Site owner?</h2> <h2>Site owner?</h2>
<p>If you operate this site and believe this block is incorrect, please <a href="https://support.example.com/submitticket.php" rel="noopener">open a support ticket</a> and include the request reference above. Our team can look up exactly which rule fired and adjust it if it's a false positive.</p> <p>If you operate this site and believe this block is incorrect, please contact your hosting provider's support team and include the request reference above. They can look up exactly which rule fired and adjust it if it's a false positive.</p>
</div> </div>
<p class="small">Reference IDs expire from our active logs after 14 days, so please open a ticket promptly if you'd like this investigated.</p> <p class="small">Reference IDs expire from our active logs after 14 days, so please open a ticket promptly if you'd like this investigated.</p>

View File

@@ -1 +1,9 @@
# Source-IP whitelist — exempt from HAProxy rate limits (one IP or CIDR per line).
# Referenced by templates/hap_listener.tpl:
# acl is_trusted_ip src -f /etc/haproxy/trusted_ips.list
#
# Add trusted source IPs below. Do NOT commit real/personal IPs to this repo —
# it is mirrored publicly. Keep real entries in an untracked local copy, or add
# them directly on the server (the file lives in the /etc/haproxy named volume
# and persists across container recreates).
127.0.0.1 127.0.0.1

View File

@@ -1 +1,9 @@
# Real-IP whitelist for proxy-header matching — exempt from HAProxy rate limits.
# Format: "<IP> 1" (one per line). Referenced by templates/hap_listener.tpl:
# acl is_whitelisted var(txn.real_ip),map_ip(/etc/haproxy/trusted_ips.map,0) -m int gt 0
#
# Add trusted real IPs below. Do NOT commit real/personal IPs to this repo —
# it is mirrored publicly. Keep real entries in an untracked local copy, or add
# them directly on the server (the file lives in the /etc/haproxy named volume
# and persists across container recreates).
127.0.0.1 1 127.0.0.1 1