From b97dccec50a3c53cd3684d80f577431e40d9a902 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Wed, 3 Jun 2026 23:21:18 +0300 Subject: [PATCH 1/2] feat(galaxy): package public roles as the decdn.node collection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage baseline + decdn_node into a distributable Ansible Galaxy collection (`decdn.node`) without disturbing the internal deploy project. The collection overlay lives in `ansible/galaxy/` and is assembled into a clean `ansible_collections/decdn/node/` tree by `galaxy/build.sh` — deliberately NOT a `galaxy.yml` at the project root, so ansible-lint/molecule still treat the deploy project as a plain project (bare role names, roles_path, production profile). Only the two public-facing roles ship; the internal anvil tooling (anvil/caddy/contracts) is excluded by construction. Foundations only — publishing stays a manual `ansible-galaxy collection publish` step (no token in CI). - add LICENSE (MIT) backing the roles' declared license - add galaxy/{galaxy.yml,meta/runtime.yml,README.md,CHANGELOG.md,build.sh} - enrich baseline/decdn_node meta (author, galaxy_tags); add baseline README - add `make build` / `make galaxy-check` (root + ansible) and a CI `galaxy-build` job that builds + validates via galaxy-importer (no publish, no secrets) - exclude generated build/ and the pre-1.0 galaxy/ overlay from the linters - document the collection + manual-publish flow in ansible/README.md and CLAUDE.md Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 36 +++++++++++++ .gitignore | 7 +++ CLAUDE.md | 10 +++- LICENSE | 21 ++++++++ Makefile | 8 ++- ansible/.ansible-lint | 9 ++++ ansible/.yamllint | 1 + ansible/Makefile | 21 +++++++- ansible/README.md | 30 +++++++++++ ansible/galaxy/CHANGELOG.md | 24 +++++++++ ansible/galaxy/README.md | 75 ++++++++++++++++++++++++++ ansible/galaxy/build.sh | 44 +++++++++++++++ ansible/galaxy/galaxy.yml | 44 +++++++++++++++ ansible/galaxy/meta/runtime.yml | 3 ++ ansible/roles/baseline/README.md | 49 +++++++++++++++++ ansible/roles/baseline/meta/main.yml | 2 + ansible/roles/decdn_node/meta/main.yml | 2 + 17 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 LICENSE create mode 100644 ansible/galaxy/CHANGELOG.md create mode 100644 ansible/galaxy/README.md create mode 100755 ansible/galaxy/build.sh create mode 100644 ansible/galaxy/galaxy.yml create mode 100644 ansible/galaxy/meta/runtime.yml create mode 100644 ansible/roles/baseline/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9c5eff..8c21150 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,6 +65,42 @@ jobs: echo "::endgroup::" done + # Build the public `decdn.node` collection and run galaxy-importer's checks — + # the same validation Galaxy runs on upload (metadata, license, README, embedded + # ansible-lint). This is a readiness GATE only: it never publishes and needs no + # token. Runs only when ansible/ changed. + galaxy-build: + needs: changes + if: needs.changes.outputs.ansible == 'true' + runs-on: ubuntu-latest + defaults: + run: + working-directory: ansible + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + - name: Install build + import tooling + run: python -m pip install --upgrade ansible-core ansible-lint galaxy-importer + - name: Vendor collection dependencies + # So galaxy-importer's embedded ansible-lint can resolve the roles' FQCNs + # (devsec.hardening, ansible.posix). Installed under ansible/collections. + run: make deps + env: + ANSIBLE_COLLECTIONS_PATH: collections + - name: Build + validate the decdn.node collection + run: make galaxy-check + env: + ANSIBLE_COLLECTIONS_PATH: collections + - name: Upload collection artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: decdn-node-collection + path: ansible/build/decdn-node-*.tar.gz + if-no-files-found: ignore + # Dedicated IaC security scan of the Ansible tree via the official KICS action. # KICS severities are HIGH/MEDIUM/LOW/INFO (no "critical"); we gate on HIGH. # diff --git a/.gitignore b/.gitignore index db0cc4b..2b60c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,10 @@ # KICS security-scan output (`make security`; CI uploads it as an artifact) kics-results/ + +# Galaxy collection build output (`make build`/`galaxy-check`; CI uploads the +# tarball as an artifact). The collection is staged + built under ansible/build/. +# galaxy-importer drops importer_result.json in its cwd (ansible/) when validating. +ansible/build/ +*.tar.gz +ansible/importer_result.json diff --git a/CLAUDE.md b/CLAUDE.md index 2ae3d7c..a8bdf25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,9 +87,17 @@ make deps # vendor pinned Galaxy collections into ./collections make check / deploy # deCDN node (site.yml): dry-run / provision make check-anvil / deploy-anvil # anvil devnet (anvil.yml) make add-dev USER_NAME=alice # mint + reveal an anvil basic-auth dev user +make build / galaxy-check # stage + build the decdn.node collection, then validate it ``` +**Galaxy collection (`decdn.node`).** The public roles (`baseline` + `decdn_node`) ship as +a distributable collection; the internal anvil tooling does not. The overlay lives in +`ansible/galaxy/` and is staged into a clean collection tree by `galaxy/build.sh` — there is +**no** `galaxy.yml` at the `ansible/` root (that would make ansible-lint/molecule treat the +deploy project as a collection). Build/validate with `make build` / `make galaxy-check`; +**publishing is a manual step** (`ansible-galaxy collection publish`), not automated. + **Gotcha — pre-commit is local-only.** Hygiene/shellcheck/yamllint/markdown run via `make hooks`/`make lint` on your machine, **not** in CI. CI (`.github/workflows/`) is the -blocking gate and runs `ansible-lint` + KICS (on `ansible/**`) + `actionlint`. `ansible-lint` +blocking gate and runs `ansible-lint` + KICS + `galaxy-build` (on `ansible/**`) + `actionlint`. `ansible-lint` is **not** a per-commit hook (it needs collections vendored) — run `make lint-ansible`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9812c1e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright 2026 deCDN Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index a5efdcb..0dbb422 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Convenience targets for the deCDN DevOps monorepo. # Run from the repo root. Ansible-specific work is delegated to ansible/Makefile. -.PHONY: help hooks lint lint-ansible security molecule +.PHONY: help hooks lint lint-ansible security molecule galaxy-build galaxy-check SHELL := /bin/bash # Local KICS runs use the engine image pinned by digest. CI runs the official @@ -32,3 +32,9 @@ security: ## KICS IaC security scan of ansible/ (CI runs the official molecule: ## containerised converge/verify of the anvil stack $(MAKE) -C ansible molecule + +galaxy-build: ## stage + build the decdn.node Galaxy collection artifact + $(MAKE) -C ansible build + +galaxy-check: ## build + validate the decdn.node collection (galaxy-importer) + $(MAKE) -C ansible galaxy-check diff --git a/ansible/.ansible-lint b/ansible/.ansible-lint index ac981cb..a5ebbf3 100644 --- a/ansible/.ansible-lint +++ b/ansible/.ansible-lint @@ -8,3 +8,12 @@ # 'production' profile. skip_list: - var-naming[no-role-prefix] + +# build/ is the generated collection staging tree (`make build`). galaxy/ is the +# `decdn.node` collection overlay (a nested galaxy.yml + manifest) — it is built and +# validated separately by galaxy-importer (the `galaxy-build` CI job), and its pre-1.0 +# version would otherwise trip galaxy[version-incrementing] here. Neither is part of +# the deploy project's lint scope. +exclude_paths: + - build/ + - galaxy/ diff --git a/ansible/.yamllint b/ansible/.yamllint index db665ef..629f9db 100644 --- a/ansible/.yamllint +++ b/ansible/.yamllint @@ -16,3 +16,4 @@ rules: max-spaces-inside: 1 # allow "{{ var }}" Jinja spacing ignore: | collections/ + build/ diff --git a/ansible/Makefile b/ansible/Makefile index 589e55d..d691c4e 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -1,6 +1,6 @@ # Convenience targets for the deCDN Ansible project. # Always run from the ansible/ directory. -.PHONY: deps lint check deploy check-anvil deploy-anvil add-dev molecule +.PHONY: deps lint check deploy check-anvil deploy-anvil add-dev molecule build galaxy-check SHELL := /bin/bash # Install the required Galaxy collections (>= constraints in requirements.yml) @@ -37,3 +37,22 @@ add-dev: # Containerised converge + verify + idempotence for the anvil stack (requires docker). molecule: molecule test + +# --- Galaxy collection (decdn.node) ------------------------------------------ +# Stage baseline + decdn_node into a clean collection tree and build the artifact +# under build/. Only those two roles ship; see galaxy/README.md. Publishing stays +# a manual step (ansible-galaxy collection publish build/decdn-node-*.tar.gz). +build: + ./galaxy/build.sh + +# Validate the built artifact with galaxy-importer — the same checks Galaxy runs +# on upload (metadata, license file, README, embedded ansible-lint). Fails on +# importer ERRORs; warnings are kept in build/importer.log for review. (make +# recipes don't run with pipefail, so we gate on the log, not the piped exit.) +galaxy-check: build + @tarball=$$(ls build/decdn-node-*.tar.gz); \ + echo "galaxy-importer: $$tarball"; \ + python -m galaxy_importer.main "$$tarball" 2>&1 | tee build/importer.log; \ + if grep -Eiq '(^|:)error|traceback' build/importer.log; then \ + echo "FAIL: galaxy-importer reported errors (see build/importer.log)"; exit 1; \ + fi diff --git a/ansible/README.md b/ansible/README.md index 4b8423e..cdf1174 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -131,6 +131,36 @@ Defaults live in each role (`roles/*/defaults/main.yml`); override in `group_var --- +## Packaging as a Galaxy collection (`decdn.node`) + +The two public-facing roles (`baseline` + `decdn_node`) are also packaged as the +distributable **`decdn.node`** collection — deployment options for external node +operators. The internal anvil tooling (`anvil`/`caddy`/`contracts`) does **not** ship. + +The collection overlay lives in [`galaxy/`](galaxy/) (`galaxy.yml`, the collection +`README.md`/`CHANGELOG.md`, `meta/runtime.yml`, `build.sh`). It is deliberately **not** +a `galaxy.yml` at the project root: `galaxy/build.sh` stages only the two roles into a +clean `ansible_collections/decdn/node/` tree and builds the artifact, so this project +stays a plain Ansible project (the internal `make deploy`/`lint`/`molecule` flow is +unchanged). + +```bash +make build # stage + build -> build/decdn-node-.tar.gz +make galaxy-check # build + validate with galaxy-importer (the checks Galaxy runs) +``` + +Publishing is a **manual** step (no auto-publish workflow, no token in CI yet): + +```bash +ansible-galaxy collection publish build/decdn-node-*.tar.gz --api-key "$GALAXY_TOKEN" +``` + +Bump `version:` in `galaxy/galaxy.yml` and add a `galaxy/CHANGELOG.md` entry per release. +CI's `galaxy-build` job builds + validates the collection on every `ansible/**` change but +never publishes. + +--- + ## Appendix — public path for the anvil devnet (Cloudflare Tunnel, manual) Out of scope for the playbook (browser SSO can't be scripted). Expose the loopback caddy diff --git a/ansible/galaxy/CHANGELOG.md b/ansible/galaxy/CHANGELOG.md new file mode 100644 index 0000000..19f4389 --- /dev/null +++ b/ansible/galaxy/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog — `decdn.node` + +All notable changes to the `decdn.node` Ansible collection are documented here. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the +collection adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] — unreleased + +Initial packaging of the public deCDN node roles as a distributable collection. +Not yet published to Galaxy (pre-1.0; the published shape may still change). + +### Added + +- `decdn.node.baseline` — Debian/Ubuntu host baseline: nftables default-deny + inbound, fail2ban, unattended-upgrades, chrony, an admin sudo account, and DevSec + OS + SSH hardening applied last. +- `decdn.node.decdn_node` — the `decdn-node` daemon, installed from a pinned GitHub + Release tarball under a hardened systemd unit; public QUIC udp/4433, loopback + metrics + admin RPC. + +[Unreleased]: https://github.com/decdn/devops/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/decdn/devops/releases/tag/v0.1.0 diff --git a/ansible/galaxy/README.md b/ansible/galaxy/README.md new file mode 100644 index 0000000..0055878 --- /dev/null +++ b/ansible/galaxy/README.md @@ -0,0 +1,75 @@ +# Ansible Collection — `decdn.node` + +Deploy and harden a **public [deCDN](https://decdn.org) node**. This collection is +the public, reusable slice of the [`decdn/devops`](https://github.com/decdn/devops) +repository — two roles and nothing else: + +| Role | Purpose | +|------|---------| +| `decdn.node.baseline` | Debian host baseline — nftables default-deny inbound, fail2ban, unattended-upgrades, chrony, an admin sudo user, then DevSec OS + SSH hardening (applied last). | +| `decdn.node.decdn_node` | The `decdn-node` daemon — installed from a pinned GitHub Release tarball under a hardened systemd unit; public QUIC udp/4433, loopback metrics + admin RPC. | + +> The repo's internal team tooling (the anvil devnet — `anvil`, `caddy`, `contracts` +> roles) is **not** part of this collection. + +## Requirements + +- **ansible-core ≥ 2.15** on the control machine. +- Target: **Debian (bookworm)** or **Ubuntu (jammy/noble)** over SSH with a sudo user. +- Collection dependencies (installed automatically with this collection): + `devsec.hardening (>=10.0.0)`, `ansible.posix (>=1.5.0)`. + +## Install + +```bash +ansible-galaxy collection install decdn.node +``` + +Or pin it in a `requirements.yml`: + +```yaml +collections: + - name: decdn.node + version: ">=0.1.0" +``` + +## Usage + +A minimal node playbook — baseline first (so the admin key lands before SSH +hardening), then the node: + +```yaml +- name: Provision a hardened deCDN node + hosts: decdn_nodes + become: true + roles: + - role: decdn.node.baseline + vars: + ssh_admin_user: deploy + ssh_admin_pubkey: "ssh-ed25519 AAAA... you@host" # REQUIRED — lockout guard + baseline_extra_inbound: + - { proto: udp, port: 4433, comment: "deCDN QUIC" } + - role: decdn.node.decdn_node + # decdn_node_version + rpc_url + the three contract addresses + region are + # REQUIRED — set them per host (host_vars). Contract addresses/chain-id are + # protocol facts: source them from the deployment / an ADR, never guess. +``` + +The node serves paid traffic only **after** on-chain stake + registration — an +operator step, not automated by this collection. See each role's README for the +full variable list, the eth-keystore prerequisite, and day-2 ops: + +- [`roles/baseline`](https://github.com/decdn/devops/tree/main/ansible/roles/baseline) +- [`roles/decdn_node`](https://github.com/decdn/devops/tree/main/ansible/roles/decdn_node) + +## Security model + +Backends bind `127.0.0.1`; the node opens exactly one public hole (QUIC udp/4433). +No secrets ship in the collection or are committed — the eth keystore is +operator-provisioned on the host, and `rpc_url` (which may embed an API key) renders +to a `0600` file. SSH hardening is applied last, after the admin key is in place, so +you cannot lock yourself out. + +## License + +MIT © deCDN Contributors. Protocol facts trace to the deCDN ADRs, never invented here. diff --git a/ansible/galaxy/build.sh b/ansible/galaxy/build.sh new file mode 100755 index 0000000..10a527f --- /dev/null +++ b/ansible/galaxy/build.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Stage and build the public `decdn.node` Galaxy collection. +# +# Only the roles/baseline + roles/decdn_node sources ship. The internal anvil +# devnet (anvil/caddy/contracts roles) and all deploy machinery (inventory, +# molecule, Makefile, ansible.cfg) are excluded BY CONSTRUCTION — they are simply +# never copied into the staging tree. This keeps the artifact clean and leaves the +# internal project untouched (no galaxy.yml at the project root, so ansible-lint / +# ansible / molecule still see a plain project). +# +# Output: ansible/build/decdn-node-.tar.gz +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ansible/galaxy +ansible_dir="$(cd "$here/.." && pwd)" # ansible/ +repo_root="$(cd "$ansible_dir/.." && pwd)" # repo root + +build_dir="$ansible_dir/build" +stage="$build_dir/ansible_collections/decdn/node" +roles=(baseline decdn_node) + +echo "staging decdn.node -> $stage" +rm -rf "$stage" +mkdir -p "$stage/roles" "$stage/meta" + +# Canonical role sources (shared with the internal project). +for role in "${roles[@]}"; do + cp -R "$ansible_dir/roles/$role" "$stage/roles/$role" +done + +# Collection overlay + license (the artifact must be self-contained). +cp "$here/galaxy.yml" "$stage/galaxy.yml" +cp "$here/README.md" "$stage/README.md" +cp "$here/CHANGELOG.md" "$stage/CHANGELOG.md" +cp "$here/meta/runtime.yml" "$stage/meta/runtime.yml" +cp "$repo_root/LICENSE" "$stage/LICENSE" + +# ansible-galaxy validates galaxy.yml (required keys, semver, tag charset) here. +ansible-galaxy collection build "$stage" --output-path "$build_dir" --force + +shopt -s nullglob +for tarball in "$build_dir"/decdn-node-*.tar.gz; do + echo "built: $tarball" +done diff --git a/ansible/galaxy/galaxy.yml b/ansible/galaxy/galaxy.yml new file mode 100644 index 0000000..2d9958f --- /dev/null +++ b/ansible/galaxy/galaxy.yml @@ -0,0 +1,44 @@ +--- +# Manifest for the `decdn.node` Ansible collection — the public, distributable +# slice of this DevOps repo: deploy + harden a deCDN node. +# +# This file is an OVERLAY, not the project root manifest. It is assembled into a +# clean collection tree (with only the baseline + decdn_node roles) by +# galaxy/build.sh; it is deliberately NOT placed at ansible/ root, so the +# internal deploy project stays a plain Ansible project (bare role names, +# roles_path, production-profile ansible-lint) rather than being reinterpreted as +# a collection. See galaxy/README.md. +namespace: decdn +name: node +version: 0.1.0 # pre-1.0: published shape not yet stable +readme: README.md +authors: + - deCDN Contributors +description: Deploy and harden a public deCDN node — DevSec host baseline + decdn_node. +license: + - MIT +# Galaxy tags must match ^[a-z0-9]+$ (no hyphens/underscores). +tags: + - decdn + - cdn + - node + - systemd + - hardening + - debian + - devsec + - web3 +# Collection-level dependencies — only what the SHIPPED roles use: +# baseline -> devsec.hardening (os_hardening + ssh_hardening), ansible.posix +# (authorized_key) +# decdn_node -> ansible.builtin only +# community.general is NOT used by either shipped role (it backs the internal +# anvil/caddy roles, which do not ship), so it is intentionally absent here. +dependencies: + devsec.hardening: ">=10.0.0" + ansible.posix: ">=1.5.0" +repository: https://github.com/decdn/devops +documentation: https://github.com/decdn/devops/tree/main/ansible +homepage: https://decdn.org +issues: https://github.com/decdn/devops/issues +build_ignore: + - "*.example" diff --git a/ansible/galaxy/meta/runtime.yml b/ansible/galaxy/meta/runtime.yml new file mode 100644 index 0000000..0d597e0 --- /dev/null +++ b/ansible/galaxy/meta/runtime.yml @@ -0,0 +1,3 @@ +--- +# Minimum ansible-core for the collection. Matches the roles' min_ansible_version. +requires_ansible: ">=2.15.0" diff --git a/ansible/roles/baseline/README.md b/ansible/roles/baseline/README.md new file mode 100644 index 0000000..33b6cd0 --- /dev/null +++ b/ansible/roles/baseline/README.md @@ -0,0 +1,49 @@ +# roles/baseline + +Hardened **Debian/Ubuntu host baseline**, shared by every deCDN deployment in this +repo (the public node via `playbooks/site.yml`, the internal anvil devnet via +`playbooks/anvil.yml`). Run it first; it prepares the host and then locks it down. + +## What this role does + +In order — the ordering matters: + +1. **Base packages** — `curl`, `git`, `jq`, `openssl`, `nftables`, `fail2ban`, + `unattended-upgrades`, `chrony`, … (override `baseline_packages`). +2. **Admin sudo user** — creates `ssh_admin_user` and installs `ssh_admin_pubkey` + **before** SSH is hardened, so you keep a way in. +3. **Firewall** — nftables **default-deny inbound**; SSH is the only universally-open + port. Extra public listeners are declared explicitly via `baseline_extra_inbound`. +4. **Auto-patching** — `unattended-upgrades` for security updates. +5. **fail2ban** — aggressive `sshd` jail. +6. **Time sync** — `chrony`. +7. **DevSec hardening (LAST)** — `devsec.hardening.os_hardening` + + `devsec.hardening.ssh_hardening` (key-only SSH, no root login, kernel/sysctl/PAM + hardening). Applied last so the admin key is already in place. + +## Lockout guard + +`ssh_hardening` disables root and password auth. The role **asserts** that +`ssh_admin_user` and `ssh_admin_pubkey` are set before it runs — set both for any +real deploy, or you will lock yourself out. (Set `ssh_admin_user: ""` to skip the +admin account + SSH hardening entirely, as the Molecule container does.) + +## Key variables + +| Var | Default | Notes | +|-----|---------|-------| +| `ssh_admin_user` | `deploy` | Admin sudo account; created before SSH hardening. `""` skips it. | +| `ssh_admin_pubkey` | `""` | **Required** for a real deploy — the lockout guard asserts it. | +| `ssh_allow_cidrs` | `[]` | Optional inbound-SSH source allowlist (CIDRs). Empty = any source. | +| `baseline_extra_inbound` | `[]` | Extra public inbound ports. Each item `{proto, port, comment}`. Loopback services need nothing here; the deCDN node opens udp/4433. | +| `baseline_packages` | see `defaults/main.yml` | Base package set. | + +## Dependencies + +- Collection: [`devsec.hardening`](https://galaxy.ansible.com/ui/repo/published/devsec/hardening/) + (`>=10.0.0`) and [`ansible.posix`](https://galaxy.ansible.com/ui/repo/published/ansible/posix/) + (`>=1.5.0`, for `authorized_key`). + +## Platforms + +Debian (bookworm), Ubuntu (jammy, noble). diff --git a/ansible/roles/baseline/meta/main.yml b/ansible/roles/baseline/meta/main.yml index d10c19c..2e52ef9 100644 --- a/ansible/roles/baseline/meta/main.yml +++ b/ansible/roles/baseline/meta/main.yml @@ -1,9 +1,11 @@ --- galaxy_info: role_name: baseline + author: deCDN Contributors description: Debian host baseline — firewall, auto-patching, fail2ban, time sync, DevSec OS/SSH hardening. license: MIT min_ansible_version: "2.15" + galaxy_tags: [decdn, hardening, firewall, fail2ban, devsec, debian] platforms: - name: Debian versions: [bookworm] diff --git a/ansible/roles/decdn_node/meta/main.yml b/ansible/roles/decdn_node/meta/main.yml index b4886c6..399dfa2 100644 --- a/ansible/roles/decdn_node/meta/main.yml +++ b/ansible/roles/decdn_node/meta/main.yml @@ -1,9 +1,11 @@ --- galaxy_info: role_name: decdn_node + author: deCDN Contributors description: Public deCDN node — release-tarball install, hardened systemd unit, loopback metrics/admin, public QUIC. license: MIT min_ansible_version: "2.15" + galaxy_tags: [decdn, cdn, node, systemd, web3, debian] platforms: - name: Debian versions: [bookworm] From 16a89ac0bf088e7ea72a275a0c1c8252baf64187 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Wed, 3 Jun 2026 23:31:17 +0300 Subject: [PATCH 2/2] fix(galaxy): satisfy meta schema + harden galaxy-check - add author + galaxy_tags to anvil/caddy role metas. The latest ansible-lint meta JSON-schema makes galaxy_info.author REQUIRED; adding it to baseline/ decdn_node surfaced that anvil/caddy still lacked it, failing the ansible-lint CI job. All role metas now carry it. - galaxy-check: gate on `set -o pipefail` AND the importer's "completed successfully" line. tee was swallowing the pipe exit, and the old error|traceback grep missed startup failures (e.g. "No module named galaxy_importer") while risking false positives on benign "0 errors" lines. - build.sh: rm stale build/decdn-node-*.tar.gz so the galaxy-check glob stays unambiguous as versions change. - CHANGELOG: point footnote links at pages that resolve before the first tag. Co-Authored-By: Claude Opus 4.8 (1M context) --- ansible/Makefile | 17 ++++++++++------- ansible/galaxy/CHANGELOG.md | 6 ++++-- ansible/galaxy/build.sh | 3 +++ ansible/roles/anvil/meta/main.yml | 2 ++ ansible/roles/caddy/meta/main.yml | 2 ++ 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ansible/Makefile b/ansible/Makefile index d691c4e..a81ded3 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -46,13 +46,16 @@ build: ./galaxy/build.sh # Validate the built artifact with galaxy-importer — the same checks Galaxy runs -# on upload (metadata, license file, README, embedded ansible-lint). Fails on -# importer ERRORs; warnings are kept in build/importer.log for review. (make -# recipes don't run with pipefail, so we gate on the log, not the piped exit.) +# on upload (metadata, license file, README, embedded ansible-lint). build.sh +# leaves exactly one tarball, so the glob is unambiguous. We gate on BOTH the +# pipeline exit (set -o pipefail catches a crash / "No module named …" that prints +# no error keyword) AND the importer's explicit success line (it can log content +# errors while still exiting 0). Warnings are kept in build/importer.log. galaxy-check: build - @tarball=$$(ls build/decdn-node-*.tar.gz); \ + @set -o pipefail; \ + tarball=$$(ls -1 build/decdn-node-*.tar.gz); \ echo "galaxy-importer: $$tarball"; \ - python -m galaxy_importer.main "$$tarball" 2>&1 | tee build/importer.log; \ - if grep -Eiq '(^|:)error|traceback' build/importer.log; then \ - echo "FAIL: galaxy-importer reported errors (see build/importer.log)"; exit 1; \ + if ! python -m galaxy_importer.main "$$tarball" 2>&1 | tee build/importer.log \ + || ! grep -q "Importer processing completed successfully" build/importer.log; then \ + echo "FAIL: galaxy-importer reported errors or did not complete (see build/importer.log)"; exit 1; \ fi diff --git a/ansible/galaxy/CHANGELOG.md b/ansible/galaxy/CHANGELOG.md index 19f4389..264e442 100644 --- a/ansible/galaxy/CHANGELOG.md +++ b/ansible/galaxy/CHANGELOG.md @@ -20,5 +20,7 @@ Not yet published to Galaxy (pre-1.0; the published shape may still change). Release tarball under a hardened systemd unit; public QUIC udp/4433, loopback metrics + admin RPC. -[Unreleased]: https://github.com/decdn/devops/compare/v0.1.0...HEAD -[0.1.0]: https://github.com/decdn/devops/releases/tag/v0.1.0 + +[Unreleased]: https://github.com/decdn/devops/commits/main +[0.1.0]: https://github.com/decdn/devops/releases diff --git a/ansible/galaxy/build.sh b/ansible/galaxy/build.sh index 10a527f..973f04a 100755 --- a/ansible/galaxy/build.sh +++ b/ansible/galaxy/build.sh @@ -21,6 +21,9 @@ roles=(baseline decdn_node) echo "staging decdn.node -> $stage" rm -rf "$stage" +# Drop stale artifacts from earlier builds so the output dir holds exactly the +# tarball we are about to produce (galaxy-check globs build/decdn-node-*.tar.gz). +rm -f "$build_dir"/decdn-node-*.tar.gz mkdir -p "$stage/roles" "$stage/meta" # Canonical role sources (shared with the internal project). diff --git a/ansible/roles/anvil/meta/main.yml b/ansible/roles/anvil/meta/main.yml index b0ee776..14a1bf1 100644 --- a/ansible/roles/anvil/meta/main.yml +++ b/ansible/roles/anvil/meta/main.yml @@ -1,9 +1,11 @@ --- galaxy_info: role_name: anvil + author: deCDN Contributors description: Foundry Anvil EVM devnet — dedicated user, on-host mnemonic, hardened systemd unit, loopback-only. license: MIT min_ansible_version: "2.15" + galaxy_tags: [decdn, anvil, ethereum, evm, devnet, systemd] platforms: - name: Debian versions: [bookworm] diff --git a/ansible/roles/caddy/meta/main.yml b/ansible/roles/caddy/meta/main.yml index 018873e..bb7f394 100644 --- a/ansible/roles/caddy/meta/main.yml +++ b/ansible/roles/caddy/meta/main.yml @@ -1,9 +1,11 @@ --- galaxy_info: role_name: caddy + author: deCDN Contributors description: Caddy reverse proxy with per-dev HTTP basic auth in front of the loopback anvil RPC. license: MIT min_ansible_version: "2.15" + galaxy_tags: [decdn, caddy, proxy, basicauth, systemd] platforms: - name: Debian versions: [bookworm]