# Deployment & Policy Operations Source: https://humanymous.net/how-to/deployment-policy-operations.html Project: humanymous — defensive anti-bot / browser-automation detection (Apache-2.0). --- # Deployment & policy operations **Diátaxis quadrant:** How-to. **Audience:** operators and integrators deploying humanymous Gate ("Gate" after first mention) beyond a first local run. This page is a shelf of independent, goal-titled recipes. Each follows the same template — a goal, numbered steps, and a way to verify — so you can jump to the one task you need and stop. Concepts and full option tables are linked, not repeated here: for the mechanism behind a request see [How Gate sees a request](../concepts/how-gate-sees-a-request.md), and for every flag, preset, and threshold see the [CLI, config & per-route policy reference](../reference/cli-config-policy.md). > **Note:** This repository is a reference implementation, not a production-hardened build. **ACME (Let's Encrypt) and bring-your-own TLS ARE supported and wired in the reference binary** — see [HTTPS / TLS certificates](./https-tls-certificates.md) for the full walkthrough. What remains a prod-delta is automated signing-key rotation. Each recipe says so where it applies. Pick your recipe: - [Deploy from the published image with pull-only Compose](#recipe-deploy-from-the-published-image-with-pull-only-compose) - [Move from the dev certificate to production TLS](#recipe-move-from-the-dev-certificate-to-production-tls) - [Cloak the origin so direct hits get a 421](#recipe-cloak-the-origin-so-direct-hits-get-a-421) - [Test your integration behind Gate](#test-your-integration-behind-gate) - [Stand up the admin listener with seeded role tokens](#recipe-stand-up-the-admin-listener-with-seeded-role-tokens) - [Move a route from monitor to enforce](#recipe-move-a-route-from-monitor-to-enforce) - [Set rate limits and work the ban ladder](#recipe-set-rate-limits-and-work-the-ban-ladder) - [Not yet in the reference: a good-bot allowlist](#not-yet-in-the-reference-a-good-bot-allowlist) --- ## Recipe: deploy from the published image with pull-only Compose **Goal:** run a production Gate by pulling the published container image instead of building from source. **Steps:** 1. Use the pull-only Compose file. `deployments/compose.release.yaml` references `ghcr.io/modootoday/humanymous-gate:${HMN_VERSION:-latest}` (multi-arch `linux/amd64` + `linux/arm64`, cosign-signed), so `compose up` is a **pull, not a source build**. It runs Gate hardened — distroless, non-root, read-only root filesystem, dropped capabilities — with ACME TLS on `:443`, a sealed keystore, and a durable, replay-verified audit WAL, all on named volumes. 2. Provide config. From `deployments/`, copy the example env and route files and edit them: ``` cd deployments cp .env.example .env # HMN_UPSTREAM, HMN_DOMAIN, HMN_UNSEAL, HMN_ADMIN_TOKENS cp routes.conf.example routes.conf ``` `.env` sets your origin (`HMN_UPSTREAM`), the public domain for the Let's Encrypt cert (`HMN_DOMAIN`, TLS-ALPN-01 needs `:443` reachable), the keystore unseal passphrase (`HMN_UNSEAL`), and real admin tokens (`HMN_ADMIN_TOKENS` — the binary refuses to boot on the shipped `e2e-*` dev tokens). `routes.conf` sets your per-route presets. Gate joins your existing origin network (`HMN_ORIGIN_NET`) and proxies to your app. 3. Start it: ``` docker compose -f compose.release.yaml up -d ``` The default `HMN_VERSION=latest` tracks the newest release. **Verify:** the edge exposes HTTP probes — `GET /__hmn/healthz` (liveness) and `/__hmn/readyz` (readiness — 503s during drain). The image is distroless (no shell), so there is **no** Docker `HEALTHCHECK`; point your orchestrator's `httpGet` probes at those paths. The admin console is served at `https:///__hmn/admin/console` on the loopback-mapped admin listener (`127.0.0.1:8445`). > **Note:** `-admin-addr :8445` binds every container interface, so any container sharing the origin network can reach `gate:8445` — the host-loopback port mapping restricts *host* access, not intra-network access. Front the admin plane with mTLS/SSO or attach Gate to a dedicated internal admin network, and do not run untrusted workloads on the origin network. The standalone detection engine is published too, as `ghcr.io/modootoday/humanymous-core:latest`, for self-testing and the demo; its own runbook (flags, ports, and the published-image `docker run` recipes) lives in [Run the detection engine](../how-to/run-detection-engine.md). Building from source stays fully supported — see [Install requirements](../reference/install-requirements.md); the recipes below apply whether you run the published image or your own build. --- ## Recipe: move from the dev certificate to production TLS **Goal:** understand what TLS the reference terminates, and what the path to production certificates is. **Steps:** 1. Run Gate and observe the certificate it presents. The reference generates a self-signed certificate **in memory** on boot: ECDSA P-256, CN `humanymous.local`, with SANs `localhost`, `humanymous.local`, `127.0.0.1`, and `::1`. The negotiated TLS floor is version 1.2. 2. Expect a browser certificate warning when you visit `https://localhost:8444` in development. This is the self-signed dev cert, and the warning is normal for local use. 3. Configure production TLS with a CLI flag. Production terminates TLS with ACME-issued or bring-your-own certificates, and the reference binary supports both: `-tls-cert`/`-tls-key` for a bring-your-own PEM certificate, or `-acme-domain` (with optional `-acme-cache`/`-acme-email`) for a Let's Encrypt certificate via TLS-ALPN-01, which requires binding `:443`. **Verify:** in development, inspect the presented certificate (for example with `openssl s_client`) and confirm the CN is `humanymous.local` and the SANs include `localhost` and `127.0.0.1`. ``` openssl s_client -connect localhost:8444 -servername localhost /dev/null | openssl x509 -noout -subject -ext subjectAltName ``` Expected (dev): a `subject=CN=humanymous.local` line and a SAN list containing `localhost`, `humanymous.local`, `127.0.0.1`, `::1`. > **Note:** production TLS is supplied by CLI flag. For bring-your-own certificates, pass `-tls-cert ` and `-tls-key ` (PEM file paths). For automatic certificates, `-acme-domain ` requests a Let's Encrypt cert over TLS-ALPN-01 (which requires binding `:443`), `-acme-cache ` sets the on-disk cache directory (default `acme-cache` — in a container this **must** be an absolute path on a persistent volume, e.g. `/acme-cache`), `-acme-email ` sets the optional ACME account contact, and `-acme-directory ` selects the ACME server (empty = production Let's Encrypt; set the LE staging URL to dry-run issuance without burning rate limits). With none set, Gate falls back to the in-memory self-signed dev cert. The admin listener always stays self-signed. Full walkthrough: [HTTPS / TLS certificates](./https-tls-certificates.md). For the complete certificate walkthrough — Let's Encrypt (with a persistent ACME cache volume, staging dry-run, automatic renewal, and rate-limit caution), bring-your-own PEMs, and running behind an existing TLS terminator/CDN — see [HTTPS / TLS certificates](./https-tls-certificates.md). For the full TLS/ingress hardening table, see the [CLI, config & per-route policy reference](../reference/cli-config-policy.md). --- ## Recipe: cloak the origin so direct hits get a 421 **Goal:** make sure clients cannot skip Gate by talking to the origin directly. A direct-to-origin request should be refused with a `421` (hard rule HR-24), so all traffic is forced through the edge where it is scored and enforced. **Steps:** 1. Choose a shared secret and pass it to Gate as a hex origin-cloaking HMAC key with `-origin-key`. When Gate proxies a request to the origin, it signs it with this key in the `X-Hmny-Origin-Auth` header. ``` bin/gate.exe -addr :8444 -upstream http://127.0.0.1:9000 -origin-key ``` 2. Configure your origin app to **require and validate** `X-Hmny-Origin-Auth` against the same key on every inbound request, and to reject requests that lack a valid value with `421 Misdirected Request`. Gate emits the signed header; the origin is what enforces the check. 3. Restrict origin network reachability as well (firewall/private network) so the origin is only addressable from Gate. The header check is the application-layer backstop; network isolation is the first line. > **Note:** If you do not set `-origin-key`, the key is a random ephemeral value per boot. Cloaking only works end-to-end when the origin validates the same key you gave Gate, so a fixed, shared key is required for this recipe. **Verify:** send a request straight to the origin, bypassing Gate. It should be refused. ``` curl -i http://127.0.0.1:9000/ ``` Expected: `HTTP/1.1 421 Misdirected Request` from the origin (no valid `X-Hmny-Origin-Auth`). The same path fetched through the edge (`https://localhost:8444/`) is served normally. In the Ledger, a direct-origin bypass attempt surfaces as HR-24. See the [Hard rules, verdicts & signal-ID reference](../reference/hard-rules-verdicts.md). > **Note:** validating `X-Hmny-Origin-Auth` (and returning `421` when it is absent or wrong) is the operator's responsibility, but you do **not** have to write the check from scratch. The reference ships the public, importable package `github.com/modootoday/humanymous/pkg/origincloak` — a Go origin validates with one call, `origincloak.Valid(key, r.Header.Get(origincloak.Header), time.Now())` — plus a worked reference validator (`validOriginAuth`) in `test/gate/e2e.mjs` you can port to njs/Lua. The full construction, the mandatory ±1-bucket epoch grace, and copy-paste Go and nginx/njs validators are in the [Gate → origin request contract](../reference/gate-origin-contract.md). The bundled demo origin (`deployments/origin/default.conf`, plain nginx) does **not** perform this check — it is there to be proxied, not to demonstrate cloaking enforcement. --- ## Test your integration behind Gate **Goal:** exercise your own app *through* Gate and confirm it behaves correctly on each verdict — an ALLOW is served, a DENY/CHALLENGE never reaches it, and a direct-to-origin hit is refused with `421`. **Steps:** 1. Run Gate in front of your app with a fixed origin key (so cloaking validates end to end) and your app validating `X-Hmny-Origin-Auth` per the [Gate → origin request contract](../reference/gate-origin-contract.md). ``` bin/gate.exe -addr :8444 -admin-addr :8445 -upstream http://127.0.0.1:9000 -origin-key ``` 2. Drive each verdict **through the edge** (`https://localhost:8444`), not against your app directly: - **ALLOW** — a normal browser-shaped request is served your app's response (`200`, your body). With a valid verdict token on the fast path it stays `200`. - **CHALLENGE** — a request that scores as suspect on an enforcing route gets the PoW interstitial (or `401` on a strict route with no beacon); your origin is **not** contacted. - **DENY** — a request that scores automated (e.g. a `webdriver`/beacon signal) is blocked at the edge with `403`; your origin is **not** contacted. - **421** — a request sent **straight to your app** (bypassing Gate) lacks a valid `X-Hmny-Origin-Auth` and your validator returns `421 Misdirected Request`. 3. Use the worked harness as your template. `test/gate/e2e.mjs` stands up an upstream stub that enforces the cloaking check and asserts exactly these outcomes against a live Gate — `human-passes` (ALLOW `200`), `bot-blocked-at-edge` + `origin-not-contacted` (DENY `403`, zero upstream hits), `strict-route-fail-closed` (CHALLENGE), and `origin-direct-hit-blocked` (`421`). It targets Gate's edge on `:8444` and the admin listener on `:8445`. Copy its request shapes to drive your own app. > **Important:** this is the Gate edge harness. The **red-team CLI** (`cmd/redteam`, `bin/redteam.exe -attack … -host …`) is a **different** target — its default `127.0.0.1:8443` is the standalone **Core detection engine**, *not* Gate. Do not point it at your Gate edge (`:8444`); to exercise your app behind Gate, use the `test/gate/e2e.mjs` flow above. See [Self-validation & red team](./self-validation-red-team.md). **Verify:** the four outcomes above each hold: `200` served from your app on ALLOW, `403`/challenge with your origin never hit on DENY/CHALLENGE, and `421` on a direct-to-origin hit. Watch the decisions land in the Overview view of the Ledger. --- ## Recipe: stand up the admin listener with seeded role tokens **Goal:** run the authenticated admin listener with the four RBAC roles, using deterministic tokens you control instead of random per-boot tokens. **Steps:** 1. Choose one token string per role and set them in `HMN_ADMIN_TOKENS`, in the format `auditor:,operator:,approver:,dpo:`. Then start Gate; the admin listener defaults to `127.0.0.1:8445` (loopback) (a separate, cross-origin listener from the public edge). ``` HMN_ADMIN_TOKENS="auditor:,operator:,approver:,dpo:" bin/gate.exe -addr :8444 -admin-addr :8445 -upstream http://127.0.0.1:9000 ``` 2. If you leave `HMN_ADMIN_TOKENS` unset, Gate mints **random tokens per boot** and prints them at startup. That is fine for a quick local look but changes on every restart; seed the variable when you want stable tokens. 3. Keep the roles separate. The four roles map to distinct capabilities: **Auditor** reads only; **Operator** reads and operates (request bans/erasure, lift bans, cancel erasure); **Approver** reads and approves (ban/kill-switch commits); **DPO** reads, operates, approves, and is the only role that can commit an erasure. Dual-control actions require the second action to come from a **distinct** role holder — so hand each token to a different person. See the [RBAC & separation-of-duties reference](../reference/rbac-separation-of-duties.md). **Verify:** call `whoami` with one of your tokens and confirm the server-derived identity. Actor identity comes from the token, not from any request body. ``` curl -sk -H "Authorization: Bearer " https://localhost:8445/__hmn/admin/whoami ``` Expected: a response identifying the Operator role. A missing or invalid token returns `404` (deny-by-default), not `401` — the admin surface is non-discoverable. Note that `/__hmn/admin/*` is **not** served on the public edge (`:8444` 404s it); it lives only on the admin listener. Then open the Ledger at `https://localhost:8445/__hmn/admin/console` and walk the views with the [Ledger tour](./audit-console-tour.md). --- ## Recipe: move a route from monitor to enforce **Goal:** take a route that is only scoring-and-logging and have it actually enforce verdicts (for example, promote a path from `monitor` to `balanced`, or to `strict`). > **Important:** Route presets are set at **startup** via the route table (`Config.Routes` in `cmd/gate/main.go`). There is **no runtime per-route policy-write endpoint** in the reference. Changing one route's preset means editing the route table and restarting the process. The only runtime enforcement levers are the fleet-wide kill switch and the boot-global `-monitor` flag. **Steps:** 1. Confirm the current per-route posture. Read it live from the Policy view in the Ledger, or via the admin API: ``` curl -sk -H "Authorization: Bearer " https://localhost:8445/__hmn/admin/policy ``` 2. Edit the route table in `cmd/gate/main.go` (`Config.Routes`). Set the target prefix to the preset you want — `monitor` (inject, score, log, no enforce), `balanced` (enforce, fail-open on an unknown safe GET/HEAD), or `strict` (enforce, fail-closed on unknown, synchronous re-score before state-changing actions). Routes match by **longest-prefix wins**; unmatched paths use `balanced`. Do not use `low` or `api` — those names are reserved and fall back to `balanced`. 3. Rebuild and restart Gate so the new route table takes effect. ``` go build -o bin/gate.exe ./cmd/gate ``` 4. Make sure you are not globally demoting enforcement. If Gate is running with `-monitor`, or the kill switch is active, **every** route is downgraded to monitor regardless of its preset. Remove `-monitor` for the route to enforce. **Verify:** re-read `/__hmn/admin/policy` and confirm the route now shows the enforcing preset. Then send traffic that would score as automated and confirm the edge acts on it — a DENY blocks before the origin is contacted, a CHALLENGE serves the PoW interstitial. Watch the decision land in the Overview view of the Ledger; every decision is written to the audit log before it takes effect. > **Tip:** Roll out enforcement the low-risk way — start a route in `monitor`, watch its would-be verdicts in Overview for a while, then promote it. See [Will this break my app?](../explanation/will-this-break-my-app.md). --- ## Recipe: set rate limits and work the ban ladder **Goal:** understand the control-plane flood limiter and the auto-escalating ban ladder, and apply or lift a ban from the admin API. **Steps:** 1. Know the flood limiter. A per-IP sliding-window limiter guards the control-plane endpoints `/__hmn/collect` and `/__hmn/session`. Reference defaults: **window 10s**, **soft threshold 60**, **hard threshold 120**. A hard breach returns `429` *before* scoring. These defaults are set from config fields (`Config.RateWindow` / `Config.RateSoft` / `Config.RateHard`) at startup, the same way the route table is — changing them means editing the config and restarting, not a runtime call. 2. Understand the auto-ban ladder. Repeated abuse escalates with strike decay: **1h → 6h → 24h → permanent**. Auto bans are recorded with `Source=auto`; bans you add by hand are `Source=manual`. Ban keys are `ip:` or `fp:`. ```mermaid stateDiagram-v2 [*] --> None None --> Ban1h: abuse (auto) or manual temp ban Ban1h --> Ban6h: re-offend Ban6h --> Ban24h: re-offend Ban24h --> Perm: re-offend Ban1h --> None: expiry / strike decay / lift Ban6h --> None: expiry / strike decay / lift Ban24h --> None: expiry / strike decay / lift Perm --> None: lift (single Operator) note right of Perm: manual permanent / CIDR ban = dual-control commit by a distinct Approver ``` 3. Apply a temporary ban by hand. A temporary ban is a single Operator action (no second approver). Post it to the admin listener with the Operator token: ``` curl -sk -X POST -H "Authorization: Bearer " https://localhost:8445/__hmn/admin/bans -H "Content-Type: application/json" -d '{"Key":"fp:abc123","Reason":"scraper","DurationSec":3600}' ``` A positive `DurationSec` on an `ip:`/`fp:` key applies immediately and returns `{"ok":true,"permanent":false}`. `DurationSec:0` (permanent) or a `cidr:` key instead returns a pending dual-control action for a distinct Approver. 4. Lift a ban. Unblocking is also a single Operator action, and it takes a **`?key=` query parameter, not a body**: ``` curl -sk -X POST -H "Authorization: Bearer " "https://localhost:8445/__hmn/admin/bans/lift?key=fp:abc123" ``` Full admin request/response shapes are in the [CLI, config & policy reference](../reference/cli-config-policy.md#request--response-shapes). **Verify:** list active bans and confirm your change: ``` curl -sk -H "Authorization: Bearer " https://localhost:8445/__hmn/admin/bans ``` Expected: the ban appears (or is gone, after a lift) with its key, source, and remaining duration; it is also visible in the Rate Limits & Bans view of the Ledger. > **Warning:** Permanent and CIDR bans require **dual-control** — the commit must come from a distinct Approver, not the Operator who requested it. Bulk ban (`/__hmn/admin/bans/bulk`) is temporary-only; permanent and CIDR entries are rejected from bulk. For the full apply/approve/lift workflow, escalation, and the kill switch, see the [Kill switch & bans runbook](../runbooks/kill-switch-and-bans.md). --- ## Not yet in the reference: a good-bot allowlist A good-bot allowlist (letting a named, trusted automated client through without challenge) is a **reserved concept** in this project. There is **no admin endpoint for an allowlist in the reference build**, and no working recipe for one. Do not configure or promise an allowlist as a shipping feature. If you need to let known-good automation through today, the honest levers are the route table (an `off` or `monitor` preset on a specific prefix, set at startup) and the operational controls above — with the understanding that a preset applies to *all* traffic on that route, not to a specific client identity. > **TODO(verify):** the intended production shape of a good-bot allowlist (identity model and admin surface), for readers planning ahead. --- ## Related pages - [CLI, config & per-route policy reference](../reference/cli-config-policy.md) — every flag, preset, threshold, and endpoint. - [Kill switch & bans runbook](../runbooks/kill-switch-and-bans.md) — the fleet-wide kill switch and the full ban workflow. - [Key management](./key-management.md) — the sealed keystore (`-keystore` / `HMN_UNSEAL`), persistent identity, and why unsealed keys are ephemeral. - [RBAC & separation-of-duties reference](../reference/rbac-separation-of-duties.md) — roles, capabilities, and dual-control. - [How Gate sees a request](../concepts/how-gate-sees-a-request.md) — the L1–L7 pipeline and verdict flow behind these controls.