Skip to content

feat(sponsord_onramp): deploy sponsord-onramp behind Caddy beside sponsord - #87

Merged
thiras merged 4 commits into
mainfrom
feat/sponsord-onramp
Oct 5, 2026
Merged

thiras merged 4 commits into
mainfrom
feat/sponsord-onramp

Conversation

@thiras

@thiras thiras commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Upstream decdn/sponsord ships two server-side services. Ansible only deployed the daemon. This adds a sponsord_onramp role for sponsord-onramp: the public Turnstile gate, the decdn.sh / decdn.ps1 installers, and the API the decdn-sponsored CLI polls. (decdn-sponsored itself runs on users' machines, so it is not a deploy target.)

  • Placement: a second play in playbooks/sponsord.yml on a new sponsord_onramp_hosts group. Those hosts must also be in sponsord_hosts, because the onramp uses the daemon's own API token and calls it on loopback. make deploy-sponsord runs both plays.
  • Install, unit and gates: the sponsord role's patterns.
    • Install: a manual binary, or a GPG-verified sponsord-onramp-v* release.
    • Unit: DynamicUser, with the daemon token and a host-provisioned Turnstile secret as LoadCredential= credentials. PartOf=sponsord.service.
    • Gates: a restart-input record, and a fatal /healthz check on the loopback listener.
  • Public inputs are validated: the RPC URL is served to every user, so the role refuses userinfo, a query, or any character the installers cannot quote. The release pins and the sitekey are checked against upstream's formats. CapacityBond and SlashJudge come from a third generated network mirror.
  • TLS front: Caddy from the distribution by default.
    • The role owns /etc/caddy/Caddyfile, runs caddy validate before it lands, and turns the admin API and HTTP/3 off.
    • It refuses a Caddyfile it did not write unless that file is the package's untouched default.
    • Probes: plain http must return Caddy's 308 redirect, which is fatal. The https probe is fatal too, except that a certificate ACME has not issued yet only warns.
    • sponsord_onramp_proxy: none lets you bring your own proxy.
  • Firewall: tcp/80 and tcp/443 are derived from group membership in playbooks/group_vars/all.yml.
  • Alloy: grafana_alloy_sponsord_onramp_enabled labels and re-levels the onramp's journal. There is no scrape, because the onramp has no /metrics.
  • Collection: the role ships in the decdn.node collection, which now has five roles.

There is also a separate fix commit, 5eb5012. On a host in both decdn_nodes and sponsord_hosts, the sponsord play's baseline could not render the node's udp/4433 rule, because decdn_bind_port is undefined outside the decdn_node role. The public holes are now built from a host's groups, so every play renders the same firewall. ansible/tests/firewall-holes pins this per host shape and runs in make test-scripts. CI's scripts job now installs ansible-core.

Testing

  • New molecule/sponsord-onramp (Debian 12 and Ubuntu 24.04, stub binaries, the real distro Caddy):
    • converge, idempotence, and the https path through Caddy;
    • a forged X-Forwarded-For is replaced by the real client address;
    • a Turnstile rotation restarts only the onramp;
    • a failed run does not record its inputs, and a missing record forces a restart;
    • the onramp recovers on its own after a daemon restart;
    • gate page install and removal;
    • proxy: none;
    • ACME mode;
    • a dead upstream behind Caddy fails the run;
    • a foreign Caddyfile is refused, and the package's default is taken over.
  • molecule/validation: 32 new negative cases.
  • lint-alloy: a new render case, and a level-harness run against the real Alloy binary.
  • Gates run locally: make lint, lint-ansible, lint-alloy, test-scripts (the firewall check also in a core-only venv), security (KICS, 0 HIGH), galaxy-check, plus the molecule scenarios sponsord-onramp, validation, sponsord and grafana-cloud.

Notes

  • Release mode is not exercised for the onramp, since upstream has cut no sponsord-onramp-v* release yet. Its install tasks mirror the sponsord role's, which molecule/sponsord tests.
  • --check on a brand-new host still fails when it tries to start the unit, as it already does in the sponsord role.
  • The Alloy systemd collector now also watches sponsord-onramp.service and caddy.service. That changes config.alloy once on every Grafana-enabled host, so expect one Alloy restart on the next deploy.
  • Compose and Helm are out of scope.

🤖 Generated with Claude Code

thiras and others added 3 commits October 5, 2026 15:04
baseline_extra_inbound came from playbooks/group_vars/decdn_nodes.yml and
read decdn_bind_port, a decdn_node role default. On a host in both
decdn_nodes and sponsord_hosts, the sponsord play runs baseline without
that role, so the nftables render hit an undefined variable.

all.yml now builds the list of public holes from the host's groups
(_baseline_service_inbound, with the 4433 fallback), and decdn_nodes.yml
and the new sponsord_hosts.yml both point baseline_extra_inbound at it.
Every play on a host renders the same firewall. tests/firewall-holes
pins the result per host shape and runs in `make test-scripts`; CI's
scripts job installs ansible-core for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsord

Upstream decdn/sponsord ships two server-side services; only the daemon was
deployed. The new sponsord_onramp role deploys the public onramp (Turnstile
gate, decdn.sh/decdn.ps1 installers, the decdn-sponsored CLI API) as a
second play of playbooks/sponsord.yml on sponsord_onramp_hosts, which must
also be in sponsord_hosts: it reads the daemon's own API token and calls
it on loopback.

- Install, unit, gates: the sponsord role's patterns. Manual binary or a
  GPG-verified sponsord-onramp-v* release (KEYS from the sponsord role),
  DynamicUser with the token and the host-provisioned Turnstile secret as
  LoadCredential=, PartOf=sponsord.service, a restart-inputs record, and
  a fatal /healthz gate on a loopback listener.
- Public inputs: the RPC URL is served to every user, so preflight
  refuses userinfo, a query and anything upstream's installers cannot
  quote. Release pins and the sitekey follow upstream's shapes.
  CapacityBond/SlashJudge come from a third generated network mirror.
- TLS front: Caddy from the distro by default (role-owned, validated
  Caddyfile, admin API off, ACME or tls internal; a foreign Caddyfile is
  refused without an explicit overwrite), or proxy "none". tcp/80 + 443
  come from _baseline_service_inbound.
- Alloy: grafana_alloy_sponsord_onramp_enabled labels and re-levels the
  onramp's journal stream (no /metrics, no scrape); the systemd collector
  also watches sponsord-onramp.service and caddy.service.
- Tests: molecule/sponsord-onramp (Debian 12 + Ubuntu 24.04, real Caddy
  with tls internal: https chain, a forged X-Forwarded-For, Turnstile
  rotation, fatal gate, foreign Caddyfile), 25 validation cases, a new
  lint-alloy render + level-harness case, the firewall-holes shapes.
- The role ships in the decdn.node collection (now five roles).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/firewall-holes: write each host's list with the core copy module.
  The json stdout callback lives in ansible.posix, so the check failed on
  a core-only install such as CI's. It also pins all.yml's fallbacks
  (4433, "caddy") to the role defaults.
- Caddy probes: a plain-http probe must get Caddy's 308 redirect (fatal,
  no certificate needed). With ACME, only a not-yet-issued certificate
  (curl 35/60) warns; a 502, nothing on 443 or a hang now fails the run.
  HTTP/3 is off, since udp/443 is not opened.
- The foreign-Caddyfile guard keys on the file, not the dpkg package: a
  Caddyfile without the marker is refused unless it is the package's
  untouched default (dpkg's conffile md5) or the overwrite is confirmed.
  An unreadable file is an error, not "no marker".
- Drop the hard-coded sponsord_hosts group check (it broke the role for
  collection users); the token gate and /healthz already prove the daemon
  is there. Cross-check the onramp profile's chain id against an explicit
  sponsord_chain_id. Derive the onramp's files from sponsord_onramp_etc,
  and make the Caddyfile path a role var. Tighten the ACME email check,
  report an unreadable Turnstile secret as such, and warn while a
  role-owned Caddy is left behind under proxy "none".
- Tests:
  - The stubs now use the daemon token (an authenticated /v1/info) and
    require ONRAMP_BIND/ONRAMP_DAEMON_URL.
  - verify asserts the exact X-Forwarded-For and contract addresses.
  - side_effect covers the no-op re-run, the record surviving a failed
    run, the missing-record restart, PartOf recovery, the gate page,
    proxy "none", ACME mode, a dead upstream behind Caddy and the
    package-default Caddyfile.
  - Seven more validation cases.
  - Unique Alloy harness markers.
- Docs: hard rule 2 and the firewall notes name the Caddy holes, the
  role README says the playbooks (not the role) open them, Ubuntu needs
  universe, and the wording fixes from the comment review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 5, 2026 13:33

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.

Copilot review overview

🟡 Changes recommended

Group membership is not enforced, and Caddy can start with its package-default configuration before validation.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds the sponsord_onramp Ansible role, deploying the public sponsorship gateway behind Caddy alongside sponsord.

Changes:

  • Adds hardened onramp installation, configuration, validation, TLS proxying, and health gates.
  • Derives shared firewall rules and extends Alloy journal processing.
  • Adds Molecule, validation, firewall, packaging, CI, and documentation coverage.
File Description
tests/​scripts-test.sh Runs firewall resolution checks.
scripts/​sync-network-profiles.py Generates the onramp network subset.
README.md Documents the public onramp.
docs/​requirements.md Adds platform and port requirements.
ansible/​tests/​firewall-holes/​inventory.yml Defines firewall host fixtures.
ansible/​tests/​firewall-holes/​expected.json Records expected firewall rules.
ansible/​tests/​firewall-holes/​check.sh Validates resolved firewall rules.
ansible/​tests/​alloy-config/​validate.sh Tests onramp log processing.
ansible/​tests/​alloy-config/​render.yml Adds an onramp Alloy render case.
ansible/​roles/​sponsord/​README.md Links the companion onramp role.
ansible/​roles/​sponsord_onramp/​vars/​main/​networks.yml Mirrors required contract addresses.
ansible/​roles/​sponsord_onramp/​vars/​main/​caddy.yml Defines the packaged Caddyfile path.
ansible/​roles/​sponsord_onramp/​vars/​main/​arch.yml Maps supported release architectures.
ansible/​roles/​sponsord_onramp/​templates/​sponsord-onramp.service.j2 Defines the hardened systemd unit.
ansible/​roles/​sponsord_onramp/​templates/​sponsord-onramp.env.j2 Renders public onramp settings.
ansible/​roles/​sponsord_onramp/​templates/​Caddyfile.j2 Configures TLS reverse proxying.
ansible/​roles/​sponsord_onramp/​tasks/​main.yml Implements provisioning and readiness gates.
ansible/​roles/​sponsord_onramp/​tasks/​install.yml Implements manual and verified installs.
ansible/​roles/​sponsord_onramp/​tasks/​caddy.yml Installs, configures, and probes Caddy.
ansible/​roles/​sponsord_onramp/​README.md Documents role operation and variables.
ansible/​roles/​sponsord_onramp/​meta/​main.yml Adds Galaxy role metadata.
ansible/​roles/​sponsord_onramp/​handlers/​main.yml Adds service restart handlers.
ansible/​roles/​sponsord_onramp/​defaults/​main.yml Defines role defaults.
ansible/​roles/​grafana_alloy/​templates/​config.alloy.j2 Labels and re-levels onramp logs.
ansible/​roles/​grafana_alloy/​tasks/​preflight.yml Validates new Alloy settings.
ansible/​roles/​grafana_alloy/​README.md Documents onramp observability.
ansible/​roles/​grafana_alloy/​defaults/​main.yml Adds onramp monitoring defaults.
ansible/​README.md Documents the fifth deployment role.
ansible/​playbooks/​sponsord.yml Adds the onramp deployment play.
ansible/​playbooks/​group_vars/​sponsord_hosts.yml Applies shared firewall rules.
ansible/​playbooks/​group_vars/​decdn_nodes.yml Uses the shared firewall list.
ansible/​playbooks/​group_vars/​all.yml Derives firewall and Alloy settings.
ansible/​molecule/​validation/​converge.yml Adds negative input tests.
ansible/​molecule/​sponsord/​files/​sponsord-stub Adds authenticated daemon information.
ansible/​molecule/​sponsord-onramp/​verify.yml Verifies the converged deployment.
ansible/​molecule/​sponsord-onramp/​side_effect.yml Tests rotation and failure recovery.
ansible/​molecule/​sponsord-onramp/​prepare.yml Stages scenario prerequisites.
ansible/​molecule/​sponsord-onramp/​molecule.yml Defines the new scenario.
ansible/​molecule/​sponsord-onramp/​files/​sponsord-onramp-stub Provides the test onramp server.
ansible/​molecule/​sponsord-onramp/​files/​gate.html Provides a custom gate fixture.
ansible/​molecule/​sponsord-onramp/​converge.yml Runs the production playbook.
ansible/​Makefile Documents the added scenario and role.
ansible/​inventory/​hosts.yml.example Adds example onramp membership.
ansible/​inventory/​group_vars/​sponsord_onramp_hosts.yml Provides inventory configuration guidance.
ansible/​inventory/​group_vars/​decdn_nodes.yml Updates firewall guidance.
ansible/​galaxy/​README.md Documents the collection’s fifth role.
ansible/​galaxy/​galaxy.yml Updates collection metadata.
ansible/​galaxy/​CHANGELOG.md Records the role and firewall fix.
ansible/​galaxy/​build.sh Packages the new role.
AGENTS.md Updates repository architecture guidance.
.github/​workflows/​upstream-drift.yml Tracks the additional generated mirror.
.github/​workflows/​molecule.yml Documents the expanded suite.
.github/​workflows/​ci.yml Installs Ansible for firewall tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ansible/roles/sponsord_onramp/tasks/caddy.yml
Comment thread ansible/playbooks/sponsord.yml
…n the playbook

From the PR review:

- Install Caddy with policy_rc_d 101, so the package's maintainer scripts
  never start it on its stock config while 80/443 are already open. A run
  that failed after the install could otherwise leave the default server
  public. Caddy first starts from the restart handler, after the managed
  Caddyfile has validated.
- playbooks/sponsord.yml gains a fact-free first play that refuses an
  onramp host outside sponsord_hosts: such a host would skip baseline and
  get no Caddy firewall holes. The role still checks no group name, so
  collection users keep their own groups. tests/scripts-test.sh lifts that
  play out with yq and runs it against a fixture, both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thiras
thiras merged commit d743568 into main Oct 5, 2026
26 of 27 checks passed
@thiras
thiras deleted the feat/sponsord-onramp branch October 5, 2026 14:28
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