Context
The repo now has three deploy paths: Ansible, Docker Compose and Helm (#69). All three assume the operator already has something set up: a control machine with Ansible, a Docker host, or a cluster.
The lowest-friction path for many operators is paste a user-data file into a VPS provider's "create server" form. Almost every provider accepts cloud-init there (Hetzner, DigitalOcean, OVHcloud, Vultr, AWS, Scaleway, …). This was listed as optional in the #69 review and deferred.
This also fits #48 (Terraform for DigitalOcean): the same user-data can be the Droplet's user_data, so Terraform provisions and cloud-init bootstraps without a separate hand-off.
Proposal
Add cloud-init/ with a documented user-data.yaml template that brings a fresh Debian 12/13 or Ubuntu 24.04/26.04 host (x86_64 or aarch64) to "node installed, waiting for its secret".
Recommended approach: ansible-pull of this repo's site.yml against localhost. It reuses the baseline hardening (nftables, DevSec, fail2ban, unattended-upgrades) and the decdn_node role unchanged, so there is no second copy of the node's configuration. The alternative, cloud-init installing Docker and running compose/, is simpler but skips host hardening. Decide in the PR.
Constraints (AGENTS.md hard rules)
- No secrets in user-data. User-data is readable from the instance metadata service by any local process, and providers keep it in their console and API. The RPC URL (which may embed an API key) must not go in it.
- The bootstrap should stop at the role's existing "secret env file missing" gate, or leave the unit disabled.
- The operator then provisions
0600 /etc/decdn/decdn.env over SSH, the path the role already prefers.
- The keystore should be generated on the host (
decdn_node_generate_keystore: true), never passed in.
- Pinned and verified.
- Pin this repo by tag or commit and the collections by version.
- Install the node from the GPG-verified release tarball (
release mode).
- No
curl | bash of unpinned content.
- Localhost-only default and one public hole (udp/4433): unchanged, because the roles enforce them.
- Lockout safety. The SSH key must come from the provider's key injection or an explicit
baseline_sudo_users entry, since baseline disables root and password login.
Blocked on
An upstream decdn/decdn release. manual install mode copies binaries from a control machine, which a cloud-init bootstrap doesn't have. Until upstream tags a release, this path can only be prototyped against a locally hosted tarball mirror (decdn_node_release_base).
Acceptance criteria
Open questions
ansible-pull (hardening included) or Compose (no Ansible on the host)?
- How the operator is told the host is ready for its secret: a MOTD line,
cloud-init status, or a marker file.
- Whether to take the region (
decdn_region) as a templated user-data variable, since it isn't a secret.
Context
The repo now has three deploy paths: Ansible, Docker Compose and Helm (#69). All three assume the operator already has something set up: a control machine with Ansible, a Docker host, or a cluster.
The lowest-friction path for many operators is paste a user-data file into a VPS provider's "create server" form. Almost every provider accepts cloud-init there (Hetzner, DigitalOcean, OVHcloud, Vultr, AWS, Scaleway, …). This was listed as optional in the #69 review and deferred.
This also fits #48 (Terraform for DigitalOcean): the same user-data can be the Droplet's
user_data, so Terraform provisions and cloud-init bootstraps without a separate hand-off.Proposal
Add
cloud-init/with a documenteduser-data.yamltemplate that brings a fresh Debian 12/13 or Ubuntu 24.04/26.04 host (x86_64 or aarch64) to "node installed, waiting for its secret".Recommended approach:
ansible-pullof this repo'ssite.ymlagainst localhost. It reuses thebaselinehardening (nftables, DevSec, fail2ban, unattended-upgrades) and thedecdn_noderole unchanged, so there is no second copy of the node's configuration. The alternative, cloud-init installing Docker and runningcompose/, is simpler but skips host hardening. Decide in the PR.Constraints (AGENTS.md hard rules)
0600 /etc/decdn/decdn.envover SSH, the path the role already prefers.decdn_node_generate_keystore: true), never passed in.releasemode).curl | bashof unpinned content.baseline_sudo_usersentry, since baseline disables root and password login.Blocked on
An upstream
decdn/decdnrelease.manualinstall mode copies binaries from a control machine, which a cloud-init bootstrap doesn't have. Until upstream tags a release, this path can only be prototyped against a locally hosted tarball mirror (decdn_node_release_base).Acceptance criteria
cloud-init/user-data.yamlpassescloud-init schema --config-fileand pre-commit, and contains no secret.cloud-init/README.md:decdn.env;decdn_chain setup(docs/lifecycle.md).ansible-pullinvocation is syntax-checked.docs/requirements.mdand the root README.user_dataoption.Open questions
ansible-pull(hardening included) or Compose (no Ansible on the host)?cloud-init status, or a marker file.decdn_region) as a templated user-data variable, since it isn't a secret.