Secure deployment gate (harden + scan, verified)
Find the security problems in a deployment by probing it, and prove the fixes
hold — TLS, security headers, exposed ports, and the proxy/container config are
gated by checks that run, not by a checklist you tick.
Core principle
Security posture is measured, not assumed. The loop is: scan the live target
- audit the config → triage by severity → fix the root cause (usually: put a
hardened reverse proxy in front and close everything else) → re-scan until the
gate is green.
Be honest about scope (the rule that keeps this skill correct): these are
automated configuration and surface checks. A green gate is a baseline, not a
clean bill of health — it does not find logic flaws, broken authz, injection,
or anything needing credentials or human judgement, and it is not a substitute
for a real penetration test. Report "0 blocking findings against the encoded
checks", never "the deployment is secure". → references/01-scope-authorization-and-threat-model.md
⚠️ Authorization — read first
Active scanning (port connects, TLS handshakes, HTTP requests) may only be run
against systems you own or are contracted to test. Scanning third-party hosts
without permission is illegal in most jurisdictions. The live scanners refuse
to run until you assert authorization ("authorized": true in the config, or
--authorize). Record who authorized it and for which hosts. The static config
audit needs no authorization — it touches no network.
When to use vs. not
- Use for: hardening a deployment; scanning your own server/site for TLS,
header, and open-port problems; setting up HTTPS properly; choosing and
configuring a reverse proxy (Caddy/nginx); gating deployment security in CI.
- Not for: scanning systems you don't control; a full penetration test or
red-team; application-level security (authz, injection, business logic) — for
those, get a human pentest. For cloud/Terraform posture use the sibling
iac-compliance-review; for app accessibility use a11y-gate.
Inputs to gather first
- The target(s) — the public URL(s)/host(s) of the deployment, and written
confirmation you're authorized to scan them.
- The deployment config — the Caddyfile / nginx conf / docker-compose /
Dockerfile, so the static audit can run (no network, no authorization needed).
- The severity bar — default gate blocks on
critical + high.
Workflow
Load each reference when you reach its step. Scripts are stdlib-only Python 3.11+
(no installs).
Confirm scope & authorization, and that automated ≠ pentest. → references/01-scope-authorization-and-threat-model.md
cp scripts/securedeploy.config.example.json scripts/securedeploy.config.json
# edit: set "authorized": true ONLY for hosts you control; list targets + configDir
Audit the config statically (offline — run this first, it needs nothing running). → references/05-reverse-proxy-hardening.md
python3 scripts/config_audit.py --config scripts/securedeploy.config.json
Scan the live deployment — TLS, headers, and exposed ports in one gate. → references/02-tls-and-https.md, 03-security-headers.md, 04-network-exposure-and-ports.md
python3 scripts/deploy_scan.py --config scripts/securedeploy.config.json
# or a one-off: python3 scripts/deploy_scan.py --url https://app.example.com --authorize
Fix root causes. Put a hardened reverse proxy in front (Caddy gives you
automatic HTTPS + renewal + redirect for free), add the security headers,
bind databases to loopback, and default-deny the firewall. Copy the hardened
examples. → references/05-reverse-proxy-hardening.md
scripts/examples/Caddyfile.hardened # the recommended setup
scripts/examples/docker-compose.hardened.yml # DB not published; least privilege
scripts/examples/nginx.hardened.conf # if you already run nginx
scripts/examples/firewall-ufw.sh # default-deny; only 80/443 (+SSH)
Re-scan until green, then verify from OUTSIDE the host that only 80/443
answer.
Gate in CI and complete the manual review for what automation can't reach
(auth, app logic, secrets management). → references/06-running-it-and-ci.md
Try it on the bundled fixtures first
Prove the gate works in both directions before you trust it — no network needed:
python3 scripts/config_audit.py scripts/examples/docker-compose.insecure.yml # findings, exit 1
python3 scripts/config_audit.py scripts/examples/docker-compose.hardened.yml # clean, exit 0
python3 scripts/headers_check.py --from scripts/fixtures/headers.insecure.json # findings, exit 1
python3 scripts/tls_check.py --from scripts/fixtures/tls.insecure.json # findings, exit 1
What's in this skill
scripts/deploy_scan.py — the live gate: runs TLS + headers + port scanners against every authorized target, aggregates into one report and one exit code.
scripts/tls_check.py — certificate validity/expiry, hostname match, self-signed/untrusted chain, weak-protocol (SSLv3/TLS 1.0/1.1) acceptance, and HTTP→HTTPS redirect.
scripts/headers_check.py — HSTS, CSP, X-Content-Type-Options, clickjacking, Referrer-Policy, Permissions-Policy, server version disclosure, and Secure/HttpOnly/SameSite cookie flags.
scripts/port_scan.py — connect-scan for risky internet-exposed ports (DB, cache, admin, Docker/etcd/Kubelet); public-target findings are downgraded to info on private/loopback targets.
scripts/config_audit.py — offline static audit of Caddyfile / nginx / docker-compose / Dockerfile / .env (exposed DB ports, privileged, mounted docker.sock, self-signed public certs, weak TLS, committed secrets).
scripts/secure_common.py — shared findings, the authorization guard, report writers.
scripts/policies.json — the catalog: header requirements, risky-port list with severities, TLS thresholds, and the static config rules.
scripts/examples/ — the hardened Caddy/nginx/compose/firewall reference setup and insecure fixtures that must fail.
scripts/fixtures/ — captured observations so every scanner is testable offline, in both directions.
references/01–06 — scope & authorization, TLS/HTTPS, security headers, network exposure, reverse-proxy hardening, and running it in CI.
Definition of done
Guardrails — avoid these mistakes
- Never scan what you don't own. The tool refuses without asserted
authorization; don't work around it. Record who authorized which hosts.
- Don't report "secure" from a green gate. These are surface checks. State
"0 blocking automated findings; manual review done; no pentest performed."
- Use a reverse proxy with automatic HTTPS. Caddy renews certs and redirects
HTTP→HTTPS by default — most TLS findings become impossible to hit.
- A database port must never face the internet. Publish Docker ports as
127.0.0.1:5432:5432, never 5432:5432; default-deny the firewall.
- Self-signed is not "HTTPS is done." It encrypts but doesn't authenticate;
public sites need a publicly-trusted cert.
tls internal is LAN-only.
- Don't suppress a finding to go green.
ignore is for a verified,
written-down accepted risk — not to quiet the scanner.
- Fix the root cause, not the symptom. One hardened proxy + a default-deny
firewall closes most findings at once; chasing them individually doesn't.
1---2name: secure-deploy-gate3description: Harden and pentest a web deployment, gated by checks that actually run — a stdlib Python scanner probes a live target's TLS/HTTPS (cert validity, weak protocols, HTTP→HTTPS redirect), security headers (HSTS, CSP, cookies, version disclosure), and connect-scans for risky internet-exposed ports (databases, caches, admin/Docker/Kubelet APIs), plus an offline static audit of the reverse-proxy and container config (Caddy, nginx, docker-compose, Dockerfile) — and ships a hardened Caddy/nginx/firewall reference setup. Findings map to a severity and fail the build; active scanning is authorization-gated. Use to secure or harden a deployment, pentest/scan your own server or site, set up HTTPS/TLS properly, pick a reverse proxy (Caddy), find open ports or exposed services, add security headers, or gate deployment security in CI. Triggers: "secure deployment", "pentest my server", "harden", "HTTPS/TLS setup", "Caddy", "reverse proxy", "open ports", "exposed database", "security headers", "HSTS".4license: MIT5---67# Secure deployment gate (harden + scan, verified)89Find the security problems in a deployment by **probing it**, and prove the fixes10hold — TLS, security headers, exposed ports, and the proxy/container config are11gated by checks that run, not by a checklist you tick.1213## Core principle1415**Security posture is measured, not assumed.** The loop is: scan the live target16+ audit the config → triage by severity → fix the root cause (usually: put a17hardened reverse proxy in front and close everything else) → re-scan until the18gate is green.1920**Be honest about scope (the rule that keeps this skill correct):** these are21automated configuration and surface checks. A green gate is a **baseline, not a22clean bill of health** — it does not find logic flaws, broken authz, injection,23or anything needing credentials or human judgement, and it is not a substitute24for a real penetration test. Report "0 blocking findings against the encoded25checks", never "the deployment is secure". → `references/01-scope-authorization-and-threat-model.md`2627## ⚠️ Authorization — read first2829Active scanning (port connects, TLS handshakes, HTTP requests) may only be run30against systems you **own or are contracted to test**. Scanning third-party hosts31without permission is illegal in most jurisdictions. The live scanners **refuse32to run** until you assert authorization (`"authorized": true` in the config, or33`--authorize`). Record who authorized it and for which hosts. The static config34audit needs no authorization — it touches no network.3536## When to use vs. not3738- Use for: hardening a deployment; scanning your own server/site for TLS,39 header, and open-port problems; setting up HTTPS properly; choosing and40 configuring a reverse proxy (Caddy/nginx); gating deployment security in CI.41- Not for: scanning systems you don't control; a full penetration test or42 red-team; application-level security (authz, injection, business logic) — for43 those, get a human pentest. For cloud/Terraform posture use the sibling44 `iac-compliance-review`; for app accessibility use `a11y-gate`.4546## Inputs to gather first47481. **The target(s)** — the public URL(s)/host(s) of the deployment, and written49 confirmation you're authorized to scan them.502. **The deployment config** — the Caddyfile / nginx conf / docker-compose /51 Dockerfile, so the static audit can run (no network, no authorization needed).523. **The severity bar** — default gate blocks on `critical` + `high`.5354## Workflow5556Load each reference when you reach its step. Scripts are stdlib-only Python 3.11+57(no installs).58591. **Confirm scope & authorization**, and that automated ≠ pentest. → `references/01-scope-authorization-and-threat-model.md`60 ```bash61 cp scripts/securedeploy.config.example.json scripts/securedeploy.config.json62 # edit: set "authorized": true ONLY for hosts you control; list targets + configDir63 ```64652. **Audit the config statically** (offline — run this first, it needs nothing running). → `references/05-reverse-proxy-hardening.md`66 ```bash67 python3 scripts/config_audit.py --config scripts/securedeploy.config.json68 ```69703. **Scan the live deployment** — TLS, headers, and exposed ports in one gate. → `references/02-tls-and-https.md`, `03-security-headers.md`, `04-network-exposure-and-ports.md`71 ```bash72 python3 scripts/deploy_scan.py --config scripts/securedeploy.config.json73 # or a one-off: python3 scripts/deploy_scan.py --url https://app.example.com --authorize74 ```75764. **Fix root causes.** Put a hardened reverse proxy in front (Caddy gives you77 automatic HTTPS + renewal + redirect for free), add the security headers,78 bind databases to loopback, and default-deny the firewall. Copy the hardened79 examples. → `references/05-reverse-proxy-hardening.md`80 ```bash81 scripts/examples/Caddyfile.hardened # the recommended setup82 scripts/examples/docker-compose.hardened.yml # DB not published; least privilege83 scripts/examples/nginx.hardened.conf # if you already run nginx84 scripts/examples/firewall-ufw.sh # default-deny; only 80/443 (+SSH)85 ```86875. **Re-scan until green**, then verify from OUTSIDE the host that only 80/44388 answer.89906. **Gate in CI** and complete the manual review for what automation can't reach91 (auth, app logic, secrets management). → `references/06-running-it-and-ci.md`9293## Try it on the bundled fixtures first9495Prove the gate works in both directions before you trust it — no network needed:96```bash97python3 scripts/config_audit.py scripts/examples/docker-compose.insecure.yml # findings, exit 198python3 scripts/config_audit.py scripts/examples/docker-compose.hardened.yml # clean, exit 099python3 scripts/headers_check.py --from scripts/fixtures/headers.insecure.json # findings, exit 1100python3 scripts/tls_check.py --from scripts/fixtures/tls.insecure.json # findings, exit 1101```102103## What's in this skill104105- `scripts/deploy_scan.py` — the live gate: runs TLS + headers + port scanners against every authorized target, aggregates into one report and one exit code.106- `scripts/tls_check.py` — certificate validity/expiry, hostname match, self-signed/untrusted chain, weak-protocol (SSLv3/TLS 1.0/1.1) acceptance, and HTTP→HTTPS redirect.107- `scripts/headers_check.py` — HSTS, CSP, X-Content-Type-Options, clickjacking, Referrer-Policy, Permissions-Policy, server version disclosure, and Secure/HttpOnly/SameSite cookie flags.108- `scripts/port_scan.py` — connect-scan for risky internet-exposed ports (DB, cache, admin, Docker/etcd/Kubelet); public-target findings are downgraded to info on private/loopback targets.109- `scripts/config_audit.py` — **offline** static audit of Caddyfile / nginx / docker-compose / Dockerfile / .env (exposed DB ports, `privileged`, mounted docker.sock, self-signed public certs, weak TLS, committed secrets).110- `scripts/secure_common.py` — shared findings, the authorization guard, report writers.111- `scripts/policies.json` — the catalog: header requirements, risky-port list with severities, TLS thresholds, and the static config rules.112- `scripts/examples/` — the hardened Caddy/nginx/compose/firewall reference setup **and** insecure fixtures that must fail.113- `scripts/fixtures/` — captured observations so every scanner is testable offline, in both directions.114- `references/01–06` — scope & authorization, TLS/HTTPS, security headers, network exposure, reverse-proxy hardening, and running it in CI.115116## Definition of done117118- [ ] `config_audit` reports **0** blocking findings across the proxy + container config.119- [ ] `deploy_scan` against the live target reports **0** blocking findings:120 valid trusted cert, TLS ≥ 1.2, HTTP redirects to HTTPS, HSTS + core121 headers present, session cookies flagged Secure/HttpOnly/SameSite.122- [ ] **No database, cache, admin, or orchestration port** answers from the123 public internet (verified by a port scan from outside the host).124- [ ] A hardened reverse proxy terminates TLS; the app and datastores listen on125 loopback/private only; the firewall is default-deny.126- [ ] Findings triaged; each waived one has a written reason in `ignore`.127- [ ] CI runs the gate; manual review of auth/app-logic/secrets done — and the128 result is reported as "0 blocking automated findings", not "secure".129130## Guardrails — avoid these mistakes131132- **Never scan what you don't own.** The tool refuses without asserted133 authorization; don't work around it. Record who authorized which hosts.134- **Don't report "secure" from a green gate.** These are surface checks. State135 "0 blocking automated findings; manual review done; no pentest performed."136- **Use a reverse proxy with automatic HTTPS.** Caddy renews certs and redirects137 HTTP→HTTPS by default — most TLS findings become impossible to hit.138- **A database port must never face the internet.** Publish Docker ports as139 `127.0.0.1:5432:5432`, never `5432:5432`; default-deny the firewall.140- **Self-signed is not "HTTPS is done."** It encrypts but doesn't authenticate;141 public sites need a publicly-trusted cert. `tls internal` is LAN-only.142- **Don't suppress a finding to go green.** `ignore` is for a verified,143 written-down accepted risk — not to quiet the scanner.144- **Fix the root cause, not the symptom.** One hardened proxy + a default-deny145 firewall closes most findings at once; chasing them individually doesn't.