Skip to content

feat(anvil): expose devnet RPC over public HTTPS via Caddy - #8

Merged
thiras merged 2 commits into
mainfrom
fix/anvil-firewall-public-https
Jun 7, 2026
Merged

thiras merged 2 commits into
mainfrom
fix/anvil-firewall-public-https

Conversation

@thiras

@thiras thiras commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Why

rpc-dev.decdn.org was unreachable from off-host. Diagnosis: the DNS A record points straight at the droplet (DigitalOcean), but nftables is default-deny and the anvil stack opened no HTTP hole — Caddy binds 127.0.0.1:8080 and 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_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).
  • Firewall holes — open tcp/80+443 for the anvil_devnet group via baseline_extra_inbound (new inventory/group_vars/anvil_devnet.yml), mirroring the decdn_nodes udp/4433 idiom. 80 carries ACME HTTP-01 + the http→https redirect.
  • CI stays correct — molecule pinned to caddy_public: false so verify.yml's loopback-bind + 401 assertions still hold and no public-ACME is attempted in-container.
  • Backend stays loopback — anvil never faces the internet; only the auth-terminating Caddy proxy does. Hard-rule ci: add CI workflows and pre-commit hooks #2, README, and CLAUDE.md updated to match.

Security

Access now rests on the per-dev basic-auth password over TLS (open to the internet). The dev password 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 failures
  • ✅ make molecule — converge / idempotence / verify green (chain id 0x7a69, loopback binds, 401 on no-auth)
  • ✅ make security — KICS 0 high/critical (exit 0); the new holes raised no finding
  • ✅ make build + make galaxy-check — collection unaffected (caddy is internal tooling)
  • ✅ make check-anvil dry-run — shows the exact intended diffs:
            tcp dport 22 accept
    +        tcp dport 80 accept  # caddy ACME + http->https redirect
    +        tcp dport 443 accept  # caddy public RPC (rpc-dev)
    plus the Caddyfile loopback :8080 → rpc-dev.decdn.org HTTPS 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.yml points both decdn-node-1 (group decdn_nodes) and anvil-vps (group anvil_devnet) at the same rpc-dev.decdn.org; running site.yml against it would overwrite the firewall with only udp/4433. This PR is correct for the anvil.yml path — don't run site.yml against this host. Reconciling the dual-group inventory is out of scope.

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings June 7, 2026 00:08

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ansible/inventory/group_vars/anvil_devnet.yml
Comment thread ansible/roles/caddy/templates/Caddyfile.j2 Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_public toggle (default true) to switch Caddy between public HTTPS on rpc_hostname and loopback-only HTTP for dev/CI or tunnel-based setups.
  • Open baseline firewall inbound tcp/80 and tcp/443 for the anvil_devnet group via new inventory/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>
@thiras
thiras merged commit 939aa8d into main Jun 7, 2026
7 checks passed
@thiras
thiras deleted the fix/anvil-firewall-public-https branch June 7, 2026 00:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants