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
6 changes: 0 additions & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,3 @@ updates:
groups:
actions:
patterns: ["*"]
ignore:
# Pinned to the post-remediation hardened HEAD (see ci.yml). The newest
# RELEASE tag (v2.1.20) points at an older commit that predates the April
# 2026 base-image digest-pinning, so an automated bump would DOWNGRADE
# security. Re-pin manually only after verifying a newer clean commit/tag.
- dependency-name: "Checkmarx/kics-github-action"
59 changes: 39 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,34 +96,53 @@ jobs:
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.
# Dedicated IaC security scan of the Ansible tree, 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
# code is the gate.
#
# SUPPLY-CHAIN NOTE: this action's git tags were hijacked in the March 2026
# TeamPCP attack (CISA KEV). It has since been remediated — tags restored to
# their legitimate pre-hijack commits and explicit hardening applied (base
# images digest-pinned, workflows SHA-pinned, StepSecurity best practices). We
# pin to the post-remediation hardened HEAD by SHA; the `v2.1.20` *tag* points
# at the older Mar-04 commit that predates the April base-image digest-pinning,
# so we deliberately do NOT use the tag (and Dependabot is told not to bump it
# — see .github/dependabot.yml). Re-verify the SHA before any change.
# SUPPLY-CHAIN NOTE: we deliberately do NOT use Checkmarx/kics-github-action.
# Its git tags were hijacked in the March 2026 TeamPCP attack (CISA KEV), and
# beyond that history its entrypoint `apk add`s nodejs/npm at *run* time inside
# its digest-pinned base image and then executes the result — an unpinned fetch
# that defeats the pinning it advertises. That fetch also breaks the action
# outright today: Chainguard's current nodejs wants a newer glibc than the
# pinned base ships, so `node dist/index.js` dies and the step exits non-zero
# no matter what the scan found. Driving the engine image ourselves drops the
# Node layer entirely and leaves one pinned artifact — `KICS_IMAGE` in the
# Makefile, pinned by Docker Hub digest (a different artifact from the
# hijacked action). See CONTRIBUTING.md.
kics:
needs: changes
if: needs.changes.outputs.ansible == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: KICS Ansible security scan (fail on HIGH)
# master @ 2026-05-22 "[StepSecurity] Apply security best practices (#157)"
uses: Checkmarx/kics-github-action@7117906d8779ecaf5180f34c4931a774f10d7625
with:
path: ansible
platform_type: Ansible
exclude_paths: ansible/collections
fail_on: high
output_formats: json,sarif
output_path: kics-results
enable_jobs_summary: true
run: make security
# Replaces the action's `enable_jobs_summary`.
- name: Summarise KICS findings
if: always()
run: |
results=kics-results/results.json
{
echo "### KICS IaC scan"
echo
if [ -f "$results" ]; then
jq -r '.severity_counters
| "| CRITICAL | HIGH | MEDIUM | LOW | INFO |",
"|---|---|---|---|---|",
"| \(.CRITICAL) | \(.HIGH) | \(.MEDIUM) | \(.LOW) | \(.INFO) |"' "$results"
echo
jq -r 'if (.total_counter // 0) == 0 then "No findings."
else (.queries[] | .query_name as $q | .severity as $s
| .files[] | "- **\($s)** \($q) — `\(.file_name):\(.line)`")
end' "$results"
else
echo "No results file — the scan did not complete."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload KICS results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/molecule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
run: make deps
- name: molecule test
# --all runs every scenario under ansible/molecule/: default
# (operator-provisioned keystore), generate-keystore (opt-in host-side
# wallet generation), and slow-readiness (advisory /metrics probe timeout).
# (operator-provisioned keystore + rendered-value assertions), schema
# (config key-set drift against the upstream field list), validation
# (bad knobs must be rejected by the role's own asserts), generate-keystore
# (opt-in host-side wallet generation), and slow-readiness (advisory
# /metrics probe timeout).
run: molecule test --all
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ ansible/build/
*.tar.gz
ansible/importer_result.json

# ansible-compat scaffolding (a .lock plus empty modules/collections/roles dirs),
# created in the project root by ansible-lint and molecule — so `make lint-ansible`
# and `make molecule` both leave one behind. Regenerated on every run.
.ansible/

# Python bytecode (e.g. from the molecule stub daemon or any local tooling)
__pycache__/
*.pyc
18 changes: 14 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,20 @@ ansible/ # the deployment project (DevSec-hardened, lean roles)

- **`ansible/`** — the declarative deployment project. **The public deCDN node**
(`playbooks/site.yml` → baseline + `decdn-node`), installed from a pinned GitHub release
tarball under a hardened systemd unit; public QUIC udp/4433, loopback metrics/admin,
operator-provisioned eth keystore, required chain knobs (no baked protocol facts — sourced
from ADRs), over a shared DevSec-hardened `baseline`. See `ansible/README.md`. (On-chain
node stake/registration, ADR 019 Phase 2, is an operator step, not automated.)
tarball — verified against the release's GPG-signed `SHA256SUMS` — or, while upstream
has no release tag cut (the current default), from locally-built binaries; under a hardened
systemd unit; public QUIC udp/4433, loopback metrics/admin, operator-provisioned eth
keystore, required chain knobs (no baked protocol facts — sourced from ADRs), over a
shared DevSec-hardened `baseline`. See `ansible/README.md`. (On-chain node
stake/registration, ADR 019 Phase 2, is a manual operator step, driven by `decdn setup`.)

**Config-schema coupling.** `roles/decdn_node/templates/node.toml.j2` renders against
`decdn/crates/common/src/config/types.rs`, where every section is
`#[serde(deny_unknown_fields)]` with **no** serde aliases — a key the role emits that
the installed binary does not know is a startup crash-loop. Two guards: the role runs
`decdn config validate` against the real binary after templating, and the
`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.

## Commands

Expand Down
20 changes: 11 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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/` (engine image; CI uses the official KICS action) |
| `make security` | KICS IaC security scan of `ansible/` (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`.
Expand All @@ -41,14 +41,16 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo

- **Third-party actions are pinned to a full commit SHA** with a version comment
— a mutable tag can be re-pointed to malicious code.
- **`Checkmarx/kics-github-action`** was hijacked in the March 2026 TeamPCP attack
(CISA KEV) and has since been remediated. It is pinned to the **post-remediation
hardened HEAD** by SHA — *not* a release tag, because the newest tag (`v2.1.20`)
predates the April hardening. Dependabot is told **not** to bump it
(`.github/dependabot.yml`); re-pin manually only after verifying a newer clean
commit. The KICS engine image used locally (`make security`) is the **Docker Hub**
engine (a different artifact than the hijacked action) and is pinned by a
digest verified against Docker Hub — currently `v2.1.20`.
- **`Checkmarx/kics-github-action` is deliberately not used.** Its git tags were
hijacked in the March 2026 TeamPCP attack (CISA KEV), and even post-remediation
its entrypoint `apk add`s `nodejs`/`npm` at *run* time inside its digest-pinned
base image and executes the result — an unpinned fetch that defeats the pinning.
(That fetch also breaks it outright today: Chainguard's current nodejs needs a
newer glibc than the pinned base ships, so the action's Node reporter dies and
the step fails regardless of findings.) CI instead runs `make security`, which
drives the **Docker Hub** KICS engine image — a different artifact from the
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`,
and the pre-commit hook revs via `pre-commit autoupdate`.
Expand Down
18 changes: 10 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
.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
# Checkmarx/kics-github-action instead (a GitHub Action can't run outside CI).
# Pinning by digest means a re-pointed tag can't ship malicious code (cf. the
# March 2026 KICS action compromise); the digest is verified against Docker Hub
# on each bump. v2.1.20 (March 2026).
# KICS runs straight from the engine image, pinned by digest. This target IS the
# CI security gate (.github/workflows/ci.yml calls it), so local and CI runs are
# byte-identical. Pinning by digest means a re-pointed tag can't ship malicious
# code (cf. the March 2026 KICS action compromise); the digest is verified
# against Docker Hub on each bump. v2.1.20 (March 2026).
KICS_IMAGE := checkmarx/kics:v2.1.20-alpine@sha256:990ae994fbbe59760c8e4f7e89b1193a39a0c2968909058ec29335cb6d80efc1

help: ## list targets
Expand All @@ -24,12 +24,14 @@ lint-ansible: ## full ansible-lint locally (installs collections first)
$(MAKE) -C ansible deps
$(MAKE) -C ansible lint

security: ## KICS IaC security scan of ansible/ (CI runs the official action)
# -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)
mkdir -p kics-results
docker run --rm --user $(shell id -u):$(shell id -g) -v "$(CURDIR):/repo" $(KICS_IMAGE) \
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 --output-path /repo/kics-results \
--report-formats json,sarif --output-path /repo/kics-results \
--no-progress --fail-on high

molecule: ## containerised converge/verify of the decdn_node role (needs Docker)
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ baseline host hardening — DevSec os/ssh, nftables default-deny inbound,
fail2ban, unattended-upgrades, chrony, an admin sudo user
│
└─ site.yml → decdn-node public QUIC udp/4433; metrics+admin loopback;
release-tarball install; hardened systemd unit
signed-tarball or local-build install;
hardened systemd unit
```

On-chain node stake + registration (ADR 019 Phase 2) is an **operator step**, not
automated here — the node serves paid traffic only after it is staked and registered.
Upstream's `decdn setup` walks that phase end to end (with `--dry-run`); this repo
stops at host prep and startup.

## Repository layout

Expand Down
4 changes: 4 additions & 0 deletions ansible/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ deploy:
# --- Tests -------------------------------------------------------------------
# Containerised converge + idempotence + verify of the decdn_node role against a
# stub daemon (needs Docker; a privileged systemd container). See molecule/.
# Scenarios: default (rendered values + idempotence), schema (config key-set drift
# against the upstream field list), validation (bad knobs must be rejected by the
# role's own asserts), generate-keystore (opt-in host-side wallet), slow-readiness
# (advisory /metrics probe timeout).
# `--all` runs every scenario: `default` (operator-provisioned keystore) and
# `generate-keystore` (opt-in host-side wallet generation).
molecule:
Expand Down
22 changes: 13 additions & 9 deletions ansible/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ unless you listed one).
all, use the **`manual`** install method (`decdn_node_install_method: manual` +
`decdn_node_manual_bin_src` / `decdn_cli_manual_bin_src`).
2. Per-node config in `inventory/host_vars/<node>/main.yml` (committed) —
`decdn_node_version`, the three contract addresses, `decdn_region`, cache origin, … — plus
the one secret, `decdn_rpc_url`, in a sibling git-ignored `secret.yml` (copy the shipped
`host_vars/decdn-node-1/secret.yml.example`). The committed `main.yml` already carries the
Arbitrum Sepolia genesis contract addresses; edit `decdn_region` + cache origin for your
node. Contract addresses are protocol facts — source them from the deCDN contract
deployment / an ADR, never guess.
the install method + binary sources, the **four** required contract addresses,
`decdn_region`, cache origin, … — plus the one secret, `decdn_rpc_url`, in a sibling
git-ignored `secret.yml` (copy the shipped `host_vars/decdn-node-1/secret.yml.example`).
The committed `main.yml` already carries the current Arbitrum Sepolia addresses; edit
`decdn_region`, the binary paths and the cache origin for your node. Contract addresses
are protocol facts — copy them from the upstream deployment manifest
`decdn/contracts/deployments/<chainId>.json`, never guess.
3. The **eth keystore + password file** provisioned on the host (operator step — the wallet
must be funded + staked per the deCDN node-onboarding ADR, 019). As the `decdn` user,
create the password file FIRST (`key-gen` reads it, never creates it), then generate the
Expand All @@ -113,11 +114,14 @@ ss -lun | grep 4433 # public QUIC listener
ss -ltn | grep -E '9090|9191' # metrics + admin — 127.0.0.1 ONLY
curl -s 127.0.0.1:9090/metrics # 200 once up
decdn node health # admin RPC; full readiness needs on-chain registration
decdn config validate --config /etc/decdn/node.toml # the role runs this too, on every deploy
```

The node serves paid traffic only **after** on-chain stake + registration (the
`CapacityBond` txns of ADR 019 §2.2–2.3) — an operator action, not automated here, and
with no turnkey CLI yet (see `roles/decdn_node/README.md`).
The node serves paid traffic only **after** on-chain stake + registration (ADR 019
§2.2–2.3) — an operator action, not automated here, but no longer a raw contract call:
`decdn setup` walks the whole of Phase 2, and `decdn node bond` / `register` are the
primitives underneath it. All take `--dry-run`. See
[`roles/decdn_node/README.md`](roles/decdn_node/README.md#on-chain-onboarding).

---

Expand Down
Loading
Loading