Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ concurrency:
# malicious code — cf. the March 2026 KICS action compromise). The trailing
# comment records the human-readable version; .github/dependabot.yml bumps them.
jobs:
# Detect whether ansible/ changed so the heavy Ansible jobs skip unrelated PRs.
# Detect whether ansible/ or the Helm chart changed so heavy jobs skip unrelated PRs.
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
ansible: ${{ steps.filter.outputs.ansible }}
helm: ${{ steps.filter.outputs.helm }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
Expand All @@ -34,6 +35,12 @@ jobs:
filters: |
ansible:
- 'ansible/**'
# The chart shares the schema-key inventory and checker with molecule.
helm:
- 'charts/**'
- 'ansible/molecule/schema/files/**'
- 'Makefile'
- '.github/workflows/ci.yml'

# Ansible style + best-practice + the production-profile SECURITY rules,
# plus a syntax-check of every playbook. Runs only when ansible/ changed.
Expand Down Expand Up @@ -96,7 +103,29 @@ jobs:
path: ansible/build/decdn-node-*.tar.gz
if-no-files-found: ignore

# Dedicated IaC security scan of the Ansible tree, driven straight from the
# Helm chart: `helm lint --strict`, positive + negative render tests, kubeconform
# (digest-pinned image) and the upstream schema-key check shared with molecule —
# all via `make lint-helm`, the same command developers run locally.
helm:
needs: changes
if: needs.changes.outputs.helm == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Helm version is pinned here AND in the kics job below; bump both.
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.3.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Chart lint + render tests
# yq (mikefarah) and Docker ship on ubuntu-latest. No decdn binary here, so
# the real `decdn config validate` step reports SKIPPED; run it locally with
# DECDN_CLI=... before bumping the decdn version.
run: make lint-helm

# Dedicated IaC security scan of the Ansible tree and the rendered Helm chart, driven straight from the
# digest-pinned KICS *engine* image by `make security` — the exact command
# developers run locally, so CI and local results cannot drift. KICS severities
# are CRITICAL/HIGH/MEDIUM/LOW/INFO; the engine's own `--fail-on high` exit
Expand All @@ -115,19 +144,24 @@ jobs:
# hijacked action). See CONTRIBUTING.md.
kics:
needs: changes
if: needs.changes.outputs.ansible == 'true'
if: needs.changes.outputs.ansible == 'true' || needs.changes.outputs.helm == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: KICS Ansible security scan (fail on HIGH)
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.3.0
- name: KICS security scan of ansible/ + the rendered chart (fail on HIGH)
run: make security
# Replaces the action's `enable_jobs_summary`.
- name: Summarise KICS findings
if: always()
run: |
for scan in ansible helm; do
results=kics-results/results.json
[ "$scan" = helm ] && results=kics-results/helm/results.json
{
echo "### KICS IaC scan"
echo "### KICS IaC scan ($scan)"
echo
if [ -f "$results" ]; then
jq -r '.severity_counters
Expand All @@ -142,7 +176,9 @@ jobs:
else
echo "No results file — the scan did not complete."
fi
echo
} >> "$GITHUB_STEP_SUMMARY"
done
- name: Upload KICS results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
7 changes: 5 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# pip install pre-commit && make hooks # one-time install
# make lint # run on all files
#
# Heavier Ansible checks (ansible-lint, syntax-check, KICS security scan) run in
# CI only — see .github/workflows/. ansible-lint's production profile already
# Heavier checks (ansible-lint, syntax-check, KICS security scan, and the Helm
# chart's `make lint-helm`) run in CI — see .github/workflows/. ansible-lint's production profile already
# carries the Ansible security rules; this file is the fast local gate (hygiene,
# shellcheck, yamllint, markdown).
minimum_pre_commit_version: "3.5.0"
Expand Down Expand Up @@ -38,6 +38,9 @@ repos:
args: [--fix=lf]
- id: check-yaml
args: [--unsafe] # tolerate custom/!vault tags; syntax check only
# Helm templates are Go templates, not YAML; `make lint-helm` renders and
# validates them instead.
exclude: ^charts/[^/]+/templates/
- id: check-json
- id: check-toml

Expand Down
43 changes: 37 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ DevOps repo.

## What this repo is

The official Ansible project for deploying a **deCDN node**: infrastructure, deployment,
and operational tooling. The whole repo is **Ansible-driven** — `ansible/` is the
deployment project.
The official DevOps project for deploying a **deCDN node**: infrastructure, deployment,
and operational tooling. There are two deploy paths: **Ansible** (`ansible/`, VMs/bare
metal, the primary path) and a **Helm chart** (`charts/decdn-node/`, Kubernetes).

This repo is **infrastructure only**. It is *not* a source of truth for protocol or
economic claims — those trace to the deCDN ADRs. If something here states a protocol fact
Expand All @@ -20,12 +20,22 @@ economic claims — those trace to the deCDN ADRs. If something here states a pr
(e.g. the node's eth keystore, or `rpc_url` which may embed an API key) and stored under
`/etc/<svc>/` with `chmod 600` and a dedicated owner. The repo ships `*.example`
templates for secret files only (non-secret config may be committed directly). The root
`.gitignore` is a backstop — do not rely on it; keep secrets out by design.
`.gitignore` is a backstop — do not rely on it; keep secrets out by design. The Helm
chart never creates a Secret: it references operator-provisioned ones
(`existingSecret`), injects only named env keys (never `envFrom` — `DECDN_*` env
overrides `node.toml`), keeps the keystore password off the PVC, and refuses
secret-bearing keys in `config`.
2. **Localhost-only by default.** Service daemons bind `127.0.0.1` (e.g. the node's metrics
and admin RPC). A service that must accept public traffic declares exactly one hole (the
node's QUIC udp/4433) via `baseline_extra_inbound`; if a service ever needs an HTTP-facing
public path, front it with an explicit reverse proxy that terminates auth + TLS. Never
bind a *backend* to `0.0.0.0` or expose its raw port.
**Kubernetes exception (chart only):** the node's metrics bind `0.0.0.0` inside the pod
so kubelet probes and Prometheus can reach them. That is allowed only behind a
ClusterIP-only Service and the chart's NetworkPolicy (metrics ingress limited to
`metrics.networkPolicy.from`); disabling the policy fails the render unless
`networkPolicy.allowUnrestrictedMetrics=true` acknowledges it. Never front metrics with
a LoadBalancer/NodePort/Ingress.
3. **Role templates render to their target paths.** Ansible roles template config directly
onto the host (e.g. `roles/decdn_node/templates/decdn-node.service.j2` →
`/etc/systemd/system/`), with secrets generated on the host at `0600`.
Expand All @@ -42,6 +52,10 @@ ansible/ # the deployment project (DevSec-hardened, lean roles)
playbooks/ # site.yml (decdn node)
roles/ # baseline, decdn_node
inventory/ galaxy/ molecule/ # see ansible/README.md
charts/
decdn-node/ # Helm chart for the node on Kubernetes (see its README.md)
ci/ # CI values files (mirror molecule/schema's three plays)
tests/render-test.sh # positive/negative render tests (`make lint-helm`)
```

## Current services
Expand All @@ -65,6 +79,20 @@ ansible/ # the deployment project (DevSec-hardened, lean roles)
`molecule/schema` scenario checks the rendered key set against a committed inventory of
upstream field names. Re-sync both when bumping the pinned decdn version.

- **`charts/decdn-node/`** — the same node on Kubernetes: a one-replica StatefulSet (one
release = one identity) on the upstream daemon-only image (`ghcr.io/decdn/decdn-node`;
unpublished, so `image.tag`/`image.digest` is required), PVC data dir, a `prepare` init
container that installs the identity files from an `existingSecret` onto the PVC at
`0600` (upstream rejects symlinked or group/world-readable key files) and the password
into an in-memory volume, `DECDN_RPC_URL` via `secretKeyRef` (named keys only), public
UDP Service (LoadBalancer/NodePort/ClusterIP) or `hostPort`, and metrics bound `0.0.0.0`
behind a ClusterIP Service + NetworkPolicy. `values.config` mirrors `node.toml`; the
chart injects the path/port keys and fails on collisions. **The config-schema coupling
above applies here too:** `make lint-helm` runs the same `check-schema-keys.py` +
`schema-keys.txt` on the rendered ConfigMap, so a re-sync covers both paths. Unlike the
role, CI has no real-binary `decdn config validate` for the chart — run
`DECDN_CLI=… make lint-helm` locally when bumping the decdn version.

## Commands

Two Makefiles: the **root** is the hygiene/security/CI mirror; **`ansible/`** drives
Expand All @@ -75,8 +103,9 @@ deploys (its targets must run from `ansible/`). `make help` lists root targets.
make hooks # one-time: install pre-commit git hook (pip install pre-commit first)
make lint # all pre-commit hooks on all files (hygiene, shellcheck, yamllint, markdown)
make lint-ansible # vendor collections + full ansible-lint (production profile)
make security # KICS IaC scan of ansible/ (pinned engine image)
make molecule # containerised converge/verify of the decdn_node role (needs Docker)
make lint-helm # chart: helm lint + render tests + kubeconform + schema keys (needs helm, yq, Docker)
make security # = security-ansible + security-helm (KICS over the rendered chart; needs helm)

# Ansible deploys — run from ansible/ (see ansible/README.md for the full flow)
cd ansible
Expand All @@ -95,7 +124,9 @@ collection tree by `galaxy/build.sh` — there is **no** `galaxy.yml` at the `an

**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 + `galaxy-build` (on `ansible/**`) + `actionlint`. `ansible-lint`
blocking gate and runs `ansible-lint` + `galaxy-build` (on `ansible/**`), `helm` (`make lint-helm`,
on `charts/**`, the shared schema checker/inventory, `Makefile` or `ci.yml`), KICS (on either) +
`actionlint`. `ansible-lint`
is **not** a per-commit hook (it needs collections vendored) — run `make lint-ansible`.
A separate `molecule.yml` workflow runs the containerised converge/verify in CI too, so
`make molecule` is not purely local.
12 changes: 8 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ tree), and `markdownlint`.
|--------|--------------|
| `make lint` | run all pre-commit hooks on every file (the full local hygiene gate) |
| `make lint-ansible` | install Galaxy collections + run `ansible-lint` (its production profile includes the Ansible security rules) |
| `make security` | KICS IaC security scan of `ansible/` (digest-pinned engine image — CI runs this same target) |
| `make lint-helm` | Helm chart: `helm lint --strict`, positive/negative render tests, kubeconform (digest-pinned image), shared schema-key check and its fixtures (needs `helm`, `yq`, `python3` ≥ 3.11, Docker). Set `DECDN_CLI=<path to decdn>` to also run the real `decdn config validate` (CI can't). |
| `make security` | KICS IaC security scan of `ansible/` and the rendered Helm chart (digest-pinned engine image — CI runs this same target) |

`ansible-lint` is **not** a per-commit hook (it needs the collections installed).
Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hook-stage manual`.

## CI overview

- **`ci.yml`** — `ansible-lint` + `galaxy-build` + `kics` (on `ansible/**`) and
`actionlint`. Bash-only PRs skip the Ansible jobs. Hygiene/shellcheck/markdownlint
- **`ci.yml`** — `ansible-lint` + `galaxy-build` (on `ansible/**`), `helm` (on
`charts/**`, the shared schema inventory/checker, the root `Makefile` or `ci.yml`
itself), `kics` (on either) and `actionlint`. Bash-only PRs skip the Ansible jobs. Hygiene/shellcheck/markdownlint
run via **pre-commit locally only** (`make hooks` / `make lint`), not in CI.
- **`molecule.yml`** — containerised converge + idempotence + verify of the `decdn_node`
role (privileged systemd Docker container; scoped to `ansible/**`). Run locally with
Expand All @@ -52,7 +54,9 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo
hijacked action — pinned by a digest verified against Docker Hub, currently
`v2.1.20`. The engine's `--fail-on high` exit code is the gate.
- **Dependabot** (`.github/dependabot.yml`) bumps the other action SHAs weekly.
- **Bump manually** (Dependabot can't): the `KICS_IMAGE` digest in the `Makefile`,
- **Bump manually** (Dependabot can't): the `KICS_IMAGE` and `KUBECONFORM_IMAGE` digests
in the `Makefile`, both `setup-helm` `version:` inputs in `ci.yml` (`helm` and `kics`
jobs),
and the pre-commit hook revs via `pre-commit autoupdate`.

## Solidity
Expand Down
29 changes: 27 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 galaxy-build galaxy-check
.PHONY: help hooks lint lint-ansible lint-helm security security-ansible security-helm molecule galaxy-build galaxy-check
SHELL := /bin/bash

# KICS runs straight from the engine image, pinned by digest. This target IS the
Expand All @@ -10,6 +10,12 @@ SHELL := /bin/bash
# against Docker Hub on each bump. v2.1.20 (March 2026).
KICS_IMAGE := checkmarx/kics:v2.1.20-alpine@sha256:990ae994fbbe59760c8e4f7e89b1193a39a0c2968909058ec29335cb6d80efc1

# kubeconform validates rendered chart manifests against the Kubernetes schemas.
# Digest-pinned for the same reason as KICS. v0.7.0.
KUBECONFORM_IMAGE := ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c

CHART := charts/decdn-node

help: ## list targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort \
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
Expand All @@ -24,16 +30,35 @@ lint-ansible: ## full ansible-lint locally (installs collections first)
$(MAKE) -C ansible deps
$(MAKE) -C ansible lint

# Both scans always run, so a finding in one never hides the other's results.
security: ## KICS IaC security scan of ansible/ and the Helm chart (CI runs this)
@rc=0; $(MAKE) security-ansible || rc=1; $(MAKE) security-helm || rc=1; exit $$rc

# -w /repo so findings carry repo-relative paths (not ../../repo/...), which is
# what the CI job summary prints and what SARIF code-scanning uploads need.
security: ## KICS IaC security scan of ansible/ (pinned engine image; CI runs this)
security-ansible: ## KICS scan of ansible/ (pinned engine image)
mkdir -p kics-results
docker run --rm --user $(shell id -u):$(shell id -g) -w /repo -v "$(CURDIR):/repo" $(KICS_IMAGE) \
scan --path /repo/ansible --type Ansible \
--exclude-paths /repo/ansible/collections \
--report-formats json,sarif --output-path /repo/kics-results \
--no-progress --fail-on high

# The chart cannot render with its defaults (required values fail loud), so KICS
# scans the manifests rendered from the widest CI values file instead of the chart
# directory (which it would try, and fail, to render itself). Findings therefore
# point at kics-results/helm-render/decdn-node.yaml, not at the chart sources.
security-helm: ## KICS scan of the decdn-node chart's rendered manifests (needs helm)
mkdir -p kics-results/helm-render
helm template decdn-node $(CHART) -f $(CHART)/ci/ci-values.yaml > kics-results/helm-render/decdn-node.yaml
docker run --rm --user $(shell id -u):$(shell id -g) -w /repo -v "$(CURDIR):/repo" $(KICS_IMAGE) \
scan --path /repo/kics-results/helm-render --type Kubernetes \
--report-formats json,sarif --output-path /repo/kics-results/helm \
--no-progress --fail-on high

lint-helm: ## helm lint + render tests + kubeconform + shared schema-key check (needs helm, yq, python3>=3.11, docker)
KUBECONFORM="docker run --rm -i $(KUBECONFORM_IMAGE)" $(CHART)/tests/render-test.sh

molecule: ## containerised converge/verify of the decdn_node role (needs Docker)
$(MAKE) -C ansible molecule

Expand Down
Loading
Loading