feat(anvil): expose devnet RPC over public HTTPS via Caddy - #8
Conversation
The anvil devnet was unreachable off-host: rpc-dev.decdn.org A-records straight to the droplet, but nftables default-deny opened no HTTP hole (Caddy bound 127.0.0.1:8080) and the documented Cloudflare tunnel was never stood up. A firewall change alone is a no-op since Caddy is loopback-bound, so this moves Caddy's listener public too. - Add caddy_public toggle (default true): Caddy serves rpc_hostname on 443 with auto-TLS (Let's Encrypt) + per-dev basic auth, reverse-proxying to the loopback anvil. Set false for the old loopback plain-HTTP mode (dev/CI or behind a tunnel). - Open tcp/80+443 for the anvil_devnet group via baseline_extra_inbound (new inventory/group_vars/anvil_devnet.yml), the same idiom decdn_nodes uses for udp/4433. 80 carries ACME HTTP-01 + the http->https redirect. - Pin molecule to caddy_public: false so CI keeps asserting the loopback bind and avoids a doomed public-ACME attempt in-container. - anvil itself stays loopback; only the auth-terminating Caddy proxy faces the internet. Update hard-rule #2, README, and CLAUDE.md accordingly. Verified: ansible-lint (production), molecule (converge/idempotence/verify), KICS (0 high/critical), galaxy build/check, and `make check-anvil` dry-run (shows the exact nftables + Caddyfile diffs). Live deploy intentionally deferred to an operator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a caddy_public option (defaulting to true) to expose the anvil devnet RPC directly over public HTTPS with auto-TLS (Let's Encrypt) and basic auth, while keeping anvil bound to loopback. It updates the Caddyfile template, adds firewall rules for ports 80 and 443, and updates documentation and Molecule tests accordingly. The review comments point out two important issues: first, defining baseline_extra_inbound in group_vars/anvil_devnet.yml may override global firewall rules due to Ansible variable precedence; second, the Jinja2 template check caddy_acme_email | length > 0 could cause compilation errors if the variable is undefined or null, and should be replaced with a safer default filter.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
This PR updates the internal anvil.yml deployment path so the devnet JSON-RPC becomes reachable off-host by having Caddy terminate TLS and serve the RPC hostname publicly over HTTPS, while keeping anvil itself bound to loopback.
Changes:
- Add a
caddy_publictoggle (defaulttrue) to switch Caddy between public HTTPS onrpc_hostnameand loopback-only HTTP for dev/CI or tunnel-based setups. - Open baseline firewall inbound tcp/80 and tcp/443 for the
anvil_devnetgroup via newinventory/group_vars/anvil_devnet.yml. - Update docs and Molecule to keep CI behavior loopback-only (
caddy_public: false) while production defaults to public HTTPS.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
CLAUDE.md |
Updates “localhost-only” rule to explicitly allow an auth+TLS-terminating proxy to be public. |
ansible/roles/caddy/templates/Caddyfile.j2 |
Adds conditional Caddy config for public HTTPS vs loopback-only HTTP. |
ansible/roles/caddy/defaults/main.yml |
Introduces caddy_public and caddy_acme_email defaults and documents exposure modes. |
ansible/README.md |
Updates exposure model, security notes, and deployment/verification instructions for public HTTPS Caddy. |
ansible/molecule/default/molecule.yml |
Pins caddy_public: false so Molecule continues asserting loopback bind + 401 behavior. |
ansible/inventory/group_vars/anvil_devnet.yml |
Adds baseline firewall openings for tcp/80 and tcp/443 on anvil devnet hosts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`caddy_acme_email | length > 0` errors under StrictUndefined or when the
var is set to null/None in group_vars. `| default('')` absorbs both and
relies on empty-string falsiness — verified across empty/None/set/undefined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
rpc-dev.decdn.orgwas unreachable from off-host. Diagnosis: the DNS A record points straight at the droplet (DigitalOcean), butnftablesis default-deny and the anvil stack opened no HTTP hole — Caddy binds127.0.0.1:8080and the documented Cloudflare tunnel was never stood up. So there was no route to the RPC. A firewall change alone is a no-op here (Caddy is loopback-bound), so this also moves Caddy's listener public.What
caddy_publictoggle (defaulttrue) — Caddy servesrpc_hostnameon 443 with auto-TLS (Let's Encrypt) + per-dev basic auth, reverse-proxying to the loopback anvil. Setfalsefor the old loopback plain-HTTP mode (dev/CI, or behind a tunnel).anvil_devnetgroup viabaseline_extra_inbound(newinventory/group_vars/anvil_devnet.yml), mirroring thedecdn_nodesudp/4433 idiom. 80 carries ACME HTTP-01 + the http→https redirect.caddy_public: falsesoverify.yml's loopback-bind + 401 assertions still hold and no public-ACME is attempted in-container.Security
Access now rests on the per-dev basic-auth password over TLS (open to the internet). The
devpassword should be rotated (make add-dev/ regenerate the TSV) — it predates this change and was surfaced during diagnosis.Verification
make lint-ansible— ansible-lint production profile + yamllint, 0 failuresmake molecule— converge / idempotence / verify green (chain id0x7a69, loopback binds, 401 on no-auth)make security— KICS 0 high/critical (exit 0); the new holes raised no findingmake build+make galaxy-check— collection unaffected (caddy is internal tooling)make check-anvildry-run — shows the exact intended diffs::8080→rpc-dev.decdn.orgHTTPS switch.Live deploy intentionally deferred to an operator (
make deploy-anvil) — it opens the host to the internet and provisions a publicly-logged cert.Note
inventory/hosts.ymlpoints bothdecdn-node-1(groupdecdn_nodes) andanvil-vps(groupanvil_devnet) at the samerpc-dev.decdn.org; runningsite.ymlagainst it would overwrite the firewall with only udp/4433. This PR is correct for theanvil.ymlpath — don't runsite.ymlagainst this host. Reconciling the dual-group inventory is out of scope.🤖 Generated with Claude Code