Skip to content

feat(sponsord): add the sponsord role, standalone or beside a node - #82

Merged
thiras merged 3 commits into
mainfrom
feat/sponsord-role
Oct 5, 2026
Merged

thiras merged 3 commits into
mainfrom
feat/sponsord-role

Conversation

@thiras

@thiras thiras commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This adds a sponsord role and playbooks/sponsord.yml (make deploy-sponsord; site.yml imports it too). They deploy decdn/sponsord, the onboarding sponsor: it signs capabilities with the treasury key and keeps the PaymentPool topped up. The role works in three setups:

  • on a host with no node (a new sponsord_hosts group);
  • on a host that is also in decdn_nodes;
  • from the decdn.node Galaxy collection.

The role covers the daemon only. The public sponsord-onramp is a follow-up. cloud-init stays node-only, because its lint allows only decdn_nodes.

Role behaviour

  • Install. A locally built binary by default: upstream has cut no sponsord-v* release yet. A GPG-verified release is the other option; its version stamp is written only after an exact --version match.
  • Secrets. The unit runs as a DynamicUser. The API token, treasury keystore and password arrive as LoadCredential= credentials, never as environment variables.
    • The API token is generated on the host and never replaced.
    • The treasury keystore and password are provisioned by the operator. The role never creates that wallet.
  • Keystore copy. systemd 254+ writes credentials 0440, and sponsord refuses a group-readable keystore. So ExecStartPre copies the keystore 0600 into the unit's private RuntimeDirectory.
    • Running the role against a real sponsord build on Ubuntu 24.04 found this. Upstream's own deploy/systemd/sponsord.service fails there for the same reason, so it's worth an issue upstream.
  • RPC URL. It has the same two homes and provenance guards as decdn_rpc_url. A secret.env written on the host may hold SPONSORD_RPC_URL only. As an EnvironmentFile it would override the validated bind address, pool and contract.
  • Readiness gate. The listener is loopback only (asserted). sponsord has no config-validate command, so the deploy fails unless /healthz answers {"ok":true} from a listener owned by the running unit. The daemon binds only after the keystore decrypts and the on-chain pool-owner check passes.
  • Restart record. The role hashes every restart input (credentials, both env files, unit, binary) and records the hashes once the daemon is healthy. A later run restarts the daemon after any of these:
    • a credential rotation;
    • an out-of-band edit;
    • an earlier run that failed before its restart handler ran.
  • Chain config. sponsord_network supplies chain_id and the PaymentPool address. It reads roles/sponsord/vars/main/networks.yml, a subset that scripts/sync-network-profiles.py now generates alongside decdn_node's. --check covers both files, so the upstream-drift job needs no change.

grafana_alloy

  • grafana_alloy_sponsord_enabled: scrapes sponsord's /metrics (job="sponsord", service_name="sponsord"). It also re-levels sponsord's journald stream from the plain-text level, because journald files all stdout as info. Its exit errors and panics get level="error".
  • grafana_alloy_node_enabled: turns the decdn-node scrape off on a host without a node.
  • Where the toggles come from: playbooks/group_vars/all.yml derives both from group membership. They are host-scoped, so a co-located host's two plays render one config.
  • ⚠️ Effect on existing Grafana-enabled nodes: their configs render unchanged except the systemd collector's unit list, which now includes sponsord. Expect one Alloy restart on the next deploy.

Test plan

  • make molecule: all 12 scenarios pass.
    • New sponsord scenario, on Debian 12 and Ubuntu 24.04, using a stub daemon that enforces upstream's startup checks. It covers:
      • the standalone playbook path;
      • release mode against a locally signed mirror, including rejection of a bad checksum, a bad signature and a wrong-version binary;
      • idempotence;
      • a restart after credential rotation;
      • the readiness gate failing the deploy;
      • every secret.env ownership hand-off;
      • Alloy scraping sponsord and not the node.
    • grafana-cloud co-locates sponsord with a node.
    • validation has 25 new sponsord cases.
  • make lint-alloy: the real Alloy 1.19.2 binary loads the sponsord-only and co-located configs, and both daemons' log levels are checked through it.
  • make lint; make lint-ansible (production profile).
  • make security (0 HIGH); make test-scripts; make galaxy-check (four roles).
  • scripts/sync-network-profiles.py <decdn> --check: both mirrors are current, and a stale sponsord mirror exits 1.
  • One-off smoke test against a real sponsord build on Ubuntu 24.04 (not committed). With the role's unit, the daemon gets past credential loading, the keystore permission check and decryption. It then stops only at the deliberately unreachable RPC endpoint.

Known follow-ups:

  • a molecule check step;
  • a warning when the pool low-water mark is 0, which disables top-ups.

🤖 Generated with Claude Code

Deploy decdn/sponsord, the onboarding sponsor (treasury signer and
PaymentPool keeper), with a new `sponsord` role and
`playbooks/sponsord.yml` (`make deploy-sponsord`), which `site.yml` also
imports. The role needs no node: hosts go in a new `sponsord_hosts` group,
on their own or also in `decdn_nodes`. It ships in the `decdn.node`
collection.

Role:
- Install: a locally built binary by default (no `sponsord-v*` release
  yet), or a GPG-verified release. The version stamp is written only after
  an exact `--version` match.
- Unit: `DynamicUser`. The API token (generated on the host, never
  replaced), treasury keystore and password arrive as `LoadCredential=`
  credentials. systemd 254+ writes credentials 0440 and sponsord rejects
  a group-readable keystore, so `ExecStartPre` copies it 0600 into the
  unit's RuntimeDirectory. Upstream's reference unit fails on Ubuntu
  24.04 for this reason.
- RPC URL: dual-homed like `decdn_rpc_url`, with the same provenance
  guards. A host-provisioned `secret.env` may hold `SPONSORD_RPC_URL`
  only, because an EnvironmentFile would override every validated setting.
- Loopback listener only. The deploy fails unless `/healthz` answers
  `{"ok":true}` from a listener owned by the running unit.
- A hash record of every restart input (credentials, env files, unit,
  binary), written once the daemon is healthy, restarts it after a
  rotation, an out-of-band edit, or a run that failed before restarting.
- `sponsord_network` takes chain_id and the PaymentPool address from
  `vars/main/networks.yml`, a subset that sync-network-profiles.py now
  generates (and `--check`s) alongside decdn_node's.

grafana_alloy:
- `grafana_alloy_sponsord_enabled` scrapes sponsord's `/metrics` with its
  own job and service_name. It also re-levels its journald stream from the
  plain-text level, and labels its exit errors and panics `error`.
- `grafana_alloy_node_enabled` turns the node scrape off on a host without
  a node. `playbooks/group_vars/all.yml` derives both from group membership.
- Existing configs render unchanged except the systemd collector's unit
  list, which now includes sponsord: expect one Alloy restart.

Tests:
- New molecule scenario `sponsord` (Debian 12 + Ubuntu 24.04, stub daemon
  enforcing upstream's startup checks): the standalone playbook,
  idempotence, credential rotation, the fatal readiness gate and the
  secret.env provenance hand-offs.
- grafana-cloud now co-locates sponsord with a node.
- validation gains 22 sponsord cases.
- lint-alloy renders sponsord-only and co-located configs and runs both
  daemons' log levels through the real binary.

The public sponsord-onramp is not deployed yet. cloud-init stays
node-only.

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

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

Secret-file overwrite protection, API-token validation, listener validation, and release-path coverage need correction.

Review effort: Balanced
Findings: 4 Medium severity

Open (4)
What changed in this PR

Adds a standalone/co-located Ansible deployment path for sponsord, including hardened service management, network profiles, observability, packaging, and tests.

Changes:

  • Adds the sponsord role, playbook, inventory configuration, and Galaxy packaging.
  • Adds credential handling, release/manual installation, readiness checks, and restart tracking.
  • Extends Grafana Alloy monitoring and Molecule coverage for sponsord.
File Description
scripts/​sync-network-profiles.py Generates sponsord network profiles.
README.md Lists the new role and playbook.
docs/​requirements.md Documents tested sponsord platforms.
ansible/​tests/​alloy-config/​validate.sh Tests sponsord Alloy pipelines.
ansible/​tests/​alloy-config/​render.yml Renders sponsord Alloy variants.
ansible/​roles/​sponsord/​vars/​main/​networks.yml Adds generated chain configuration.
ansible/​roles/​sponsord/​vars/​main/​arch.yml Defines release architecture mappings.
ansible/​roles/​sponsord/​templates/​sponsord.service.j2 Adds the hardened systemd unit.
ansible/​roles/​sponsord/​templates/​sponsord.env.j2 Renders non-secret daemon settings.
ansible/​roles/​sponsord/​tasks/​main.yml Implements validation, provisioning, and readiness.
ansible/​roles/​sponsord/​tasks/​install.yml Implements manual and release installation.
ansible/​roles/​sponsord/​README.md Documents setup and operations.
ansible/​roles/​sponsord/​meta/​main.yml Adds role metadata.
ansible/​roles/​sponsord/​handlers/​main.yml Adds restart handling.
ansible/​roles/​sponsord/​files/​sponsord-release-KEYS.asc Adds trusted release keys.
ansible/​roles/​sponsord/​files/​secret.env.example Provides an RPC secret example.
ansible/​roles/​sponsord/​defaults/​main.yml Defines role configuration defaults.
ansible/​roles/​grafana_alloy/​templates/​config.alloy.j2 Adds sponsord metrics and log processing.
ansible/​roles/​grafana_alloy/​tasks/​preflight.yml Validates new Alloy settings.
ansible/​roles/​grafana_alloy/​README.md Documents sponsord observability.
ansible/​roles/​grafana_alloy/​defaults/​main.yml Adds per-daemon monitoring toggles.
ansible/​README.md Documents sponsord deployment.
ansible/​playbooks/​sponsord.yml Adds the standalone sponsord playbook.
ansible/​playbooks/​site.yml Imports sponsord deployment.
ansible/​playbooks/​group_vars/​all.yml Derives monitoring toggles from groups.
ansible/​molecule/​validation/​includes/​sponsord-secret-env-case.yml Adds secret-file validation cases.
ansible/​molecule/​validation/​converge.yml Adds sponsord negative tests.
ansible/​molecule/​sponsord/​verify.yml Verifies deployed sponsord state.
ansible/​molecule/​sponsord/​side_effect.yml Tests rotations and readiness failures.
ansible/​molecule/​sponsord/​prepare.yml Prepares sponsord test hosts.
ansible/​molecule/​sponsord/​molecule.yml Defines the sponsord scenario.
ansible/​molecule/​sponsord/​files/​sponsord-stub Adds the test daemon stub.
ansible/​molecule/​sponsord/​converge.yml Exercises the standalone playbook.
ansible/​molecule/​grafana-cloud/​verify.yml Verifies co-located monitoring.
ansible/​molecule/​grafana-cloud/​prepare.yml Stages sponsord test credentials.
ansible/​molecule/​grafana-cloud/​converge.yml Adds sponsord to the co-located scenario.
ansible/​Makefile Adds sponsord deployment targets.
ansible/​inventory/​hosts.yml.example Adds the optional sponsord group.
ansible/​inventory/​host_vars/​decdn-node-1/​secret.yml.example Documents inventory RPC configuration.
ansible/​inventory/​group_vars/​sponsord_hosts.yml Adds sponsord inventory examples.
ansible/​galaxy/​README.md Lists sponsord in the collection.
ansible/​galaxy/​galaxy.yml Updates collection metadata.
ansible/​galaxy/​CHANGELOG.md Records the new role.
ansible/​galaxy/​build.sh Packages the sponsord role.
AGENTS.md Adds sponsord repository guidance.
.github/​workflows/​upstream-drift.yml Documents the new generated mirror.
.github/​workflows/​molecule.yml Notes the expanded scenario count.

💡 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/tasks/install.yml
Comment thread ansible/roles/sponsord/tasks/main.yml Outdated
Comment thread ansible/roles/sponsord/tasks/main.yml
Comment thread ansible/roles/sponsord/tasks/main.yml Outdated
…e mode

Address the PR #82 review.

- secret.env: an existing file with no provenance record now needs
  sponsord_secret_env_overwrite_host_file before an inventory URL replaces
  it. decdn_node's no-record exemption covers hosts that predate its
  record; this role writes its record straight after the file, so an
  unrecorded file is always the operator's.
- API token: mirror upstream's Secret::resolve. Strip one trailing
  "\n" / "\r\n", then reject any other line break. A token with a line
  break inside can't be sent in an Authorization header, yet upstream
  would start on it.
- sponsord_bind_address: every octet must be 0-255. Before, 127.999.0.1
  passed and only failed at daemon start.
- Release mode now runs in molecule/sponsord against a locally signed
  loopback mirror. It covers:
  - upstream's archive naming;
  - the stamp write, and a re-run with the mirror down;
  - rejection of a bad checksum, a bad signature and a binary that is not
    the pinned version, with no stamp left behind;
  - retained staging dirs;
  - manual mode clearing the stamp.
- validation: cases for the octet, the unrecorded overwrite and the
  multi-line token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dn/sponsord#36

The 0440 credential mode was observed on Ubuntu 24.04's systemd 255
(Debian 12's 252 writes 0400); the exact release that changed it is not
pinned, so stop claiming "254+". Also drop an untested Debian 13 claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thiras
thiras merged commit 22697c0 into main Oct 5, 2026
16 checks passed
@thiras
thiras deleted the feat/sponsord-role branch October 5, 2026 10:21
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