Skip to content

feat(compose): run sponsord, sponsord-onramp and Caddy behind profiles - #88

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

thiras merged 3 commits into
mainfrom
feat/compose-sponsord

Conversation

@thiras

@thiras thiras commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the onboarding sponsor (sponsord + sponsord-onramp) and an optional Caddy to the Docker Compose deploy path, as profiles in compose/compose.yaml:

Profile Starts
node decdn-node
sponsord sponsord
onramp sponsord-onramp + sponsord (the onramp needs its daemon on the same host)
caddy Caddy in front of the onramp (leave it out to bring your own proxy)

Breaking for existing Compose node operators: add COMPOSE_PROFILES=node to compose/.env, or docker compose up -d selects no service.

Design

  • Same hardening as the node: host network with loopback listeners, nothing published, images by digest, read-only rootfs, cap_drop: [ALL], no-new-privileges, a dedicated non-root account, and stop graces matching the units (120 s / 30 s).
  • Listeners and secret paths are set in environment:, which beats env files. The release images default to 0.0.0.0.
  • Credential files keep the roles' /etc/sponsord/ layout, but are owned by a host sponsord account. Bind mounts keep owner and mode, and upstream rejects a group-readable keystore. Each file is mounted read-only into /run/secrets/, and each container gets only the files it needs.
  • Caddy: the official image, digest-pinned, running non-root with only NET_BIND_SERVICE. compose/Caddyfile mirrors the role's: admin off, h1/h2 only, no trusted_proxies.
  • No ${VAR:?}: Compose interpolates disabled services too, so a required variable would break the other profiles. An unset variable now renders a value its service refuses: an invalid image reference, an unknown user, or a domain with a non-numeric port (the onramp and Caddy both reject it).

Lint

make lint-compose now renders with --profile '*' under env -i, twice:

  • compose/tests/invariants.jq, with the example .env:
    • each service may only set its allowed keys, so privileged, pid: host or devices fail;
    • each service's mounts must match its allow-list exactly;
    • security_opt must be exactly no-new-privileges:true;
    • every service runs as a non-root uid:gid;
    • the sponsord daemons may not override command or entrypoint;
    • listeners stay on loopback;
    • *_FILE paths are pinned to /run/secrets/, and inline secrets are refused.
  • compose/tests/fail-closed.jq, with an empty .env: checks that unset variables fail closed.

Other changes to the target:

  • It refuses an empty render.
  • It takes LINT_COMPOSE_FILE instead of Compose's own COMPOSE_FILE.
  • Each violation prints as <service>: <invariant>.

tests/scripts-test.sh adds a positive baseline and pins which invariant each of 47 broken variants trips.

Docs

  • compose/README.md: new sponsord section, upgrade note, sudo docker compose -f compose/compose.yaml throughout.
  • Env examples: sponsord.env, sponsord-secret.env, sponsord-onramp.env. Contract addresses are left blank and point at the generated networks.yml.
  • Updated: docs/requirements.md, docs/lifecycle.md, the root README.md, AGENTS.md, CONTRIBUTING.md and the ci.yml comment.

Test plan

  • make lint (pre-commit)
  • make lint-compose, plus negatives for an empty render (docker shim) and shell-env isolation
  • make test-scripts: baseline + 47 compose variants
  • make security-compose: 0 HIGH/CRITICAL; the MEDIUM/INFO findings are by design, as documented
  • caddy validate / caddy fmt on compose/Caddyfile, and caddy adapt rejects the unset-domain default
  • Smoke test with images built from local decdn/sponsord HEAD, pushed to a loopback registry:
    • sponsord reads its token, password and 0600 keystore as the host account and fails only at the unreachable RPC; its bind stays on loopback even with 0.0.0.0 in its env file;
    • the onramp, against a stub /v1/info, listens on 127.0.0.1:8080 only, reads its files, and serves /healthz and /decdn.sh over https through non-root Caddy;
    • restart sponsord also restarts the onramp, and an env-file change recreates the container on up -d.
  • Not exercised: a real sponsord against a funded pool, and a real ACME certificate

🤖 Generated with Claude Code

Add the onboarding sponsor to the Compose deploy path as profiles in
compose/compose.yaml: node, sponsord, onramp (also starts sponsord) and caddy.
Each daemon keeps the node's hardening: host network with loopback listeners
set in environment:, nothing published, image by digest, read-only rootfs, all
capabilities dropped, a dedicated non-root account, and the units' stop graces.
Credential files keep the roles' /etc/sponsord layout but belong to a sponsord
account (bind mounts keep owner and mode; upstream rejects a group-readable
keystore) and are mounted read-only one by one. The optional Caddy runs the
official image as non-root with only NET_BIND_SERVICE and mirrors the role's
Caddyfile.

Compose interpolates disabled services, so no variable can be `:?`-required:
an unset one now renders a value its service refuses (invalid image reference,
unknown user, a domain with a non-numeric port).

lint-compose renders every profile under env -i, twice: against
compose/tests/invariants.jq with the example .env (allowed keys and exact
mounts per service, exact security_opt, non-root users, loopback listeners,
pinned secret files) and against compose/tests/fail-closed.jq with an empty
one. It refuses an empty render and takes LINT_COMPOSE_FILE, not Compose's own
COMPOSE_FILE. tests/scripts-test.sh adds a positive baseline and pins which
invariant each of 47 broken variants trips.

Breaking for existing Compose node operators: add COMPOSE_PROFILES=node to .env.

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

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

The safety gate does not enforce that RPC secrets remain outside tracked Compose YAML.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Adds profile-based Docker Compose deployment for sponsord, sponsord-onramp, and optional Caddy alongside the existing node.

Changes:

  • Adds hardened, digest-pinned Compose services and configuration examples.
  • Introduces security and fail-closed invariant checks with negative tests.
  • Updates operational documentation and CI guidance.
File Description
compose/​compose.yaml Defines profile-based services and hardening.
compose/​Caddyfile Adds TLS reverse proxy configuration.
compose/​.env.example Documents profiles, images, and service identities.
compose/​decdn.env.example Clarifies env-file quoting.
compose/​sponsord.env.example Adds non-secret daemon settings.
compose/​sponsord-secret.env.example Adds RPC secret template.
compose/​sponsord-onramp.env.example Adds public onramp settings.
compose/​tests/​invariants.jq Validates rendered security invariants.
compose/​tests/​fail-closed.jq Validates unset-variable behavior.
compose/​README.md Documents setup and operations.
tests/​scripts-test.sh Expands Compose regression variants.
Makefile Reworks the Compose lint target.
.github/​workflows/​ci.yml Updates Compose CI documentation.
README.md Advertises expanded Compose support.
docs/​requirements.md Documents requirements and firewall ports.
docs/​lifecycle.md Updates Compose shutdown guidance.
CONTRIBUTING.md Describes the expanded lint gate.
AGENTS.md Records Compose architecture and conventions.

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

Comment thread compose/README.md Outdated
Comment thread compose/tests/invariants.jq
thiras and others added 2 commits October 5, 2026 18:29
…ponsor

lint-compose's rendered environment has the env files merged in, so it could
not tell a value written in the tracked compose.yaml from one in a root-only
env file: an inline DECDN_RPC_URL or SPONSORD_RPC_URL passed. A third render
with --no-env-resolution now checks compose/tests/inline-env.jq, an exact
allow-list of the environment keys each service may set inline, with negative
variants for both RPC URLs.

The README's sponsor stop command left Caddy holding tcp/80 and tcp/443,
answering with errors; it now stops caddy first.

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

The CI runner's Compose still merges the env files under
`config --no-env-resolution` (and v2.33 does not know the flag), so the
inline-env check saw every env-file key as inline and failed. Render with each
service env file pointed at /dev/null instead: only keys written in
compose.yaml remain, on any Compose release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thiras
thiras merged commit 5e742b8 into main Oct 5, 2026
15 checks passed
@thiras
thiras deleted the feat/compose-sponsord branch October 5, 2026 15:37
thiras added a commit that referenced this pull request Oct 5, 2026
The squash of #88 predates the AGENTS.md follow-up, so main still described
the inline-env render as `config --no-env-resolution`; it uses /dev/null env
files. Also record: the caddy profile's operator-opened tcp/80 + tcp/443 in
hard rule 2, compose/'s layout, that compose/Caddyfile is a hand-kept copy of
the role's Caddyfile.j2, that the lint's allow-lists are deliberate friction
tied to upstream sponsord's env names, and that CI's Compose is older than a
dev box's (try new `config` flags under docker:27-cli first).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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