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
45 changes: 45 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Keeps the pins in docs/supply-chain.md from rotting: one pull request a week per ecosystem,
# everything of a kind grouped, and nothing newer than a week (a release that is pulled or found
# malicious within days never reaches us; security updates ignore the wait).
version: 2
updates:
# Actions are pinned by full commit SHA with a `# vX.Y.Z` comment; Dependabot moves both.
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
groups:
actions:
patterns: ["*"]
commit-message:
prefix: "ci"
# The workspace names ../network/crates/api by path (see launcher above): Dependabot may fail to
# resolve it. The fuzz crate has the same dependency.
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
groups:
cargo-minor-and-patch:
update-types: [minor, patch]
commit-message:
prefix: "deps"
# Base images of deploy/Dockerfile: digest and tag move together.
- package-ecosystem: docker
directory: /deploy
schedule:
interval: weekly
day: monday
cooldown:
default-days: 7
groups:
images:
patterns: ["*"]
commit-message:
prefix: "deploy"
33 changes: 33 additions & 0 deletions .github/workflows/advisories.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: advisories

# New security advisories appear without a commit, so the check that every pull request gets from
# the shared rules workflow (cargo-deny: advisories, bans, licences, sources) also runs every
# Monday and on demand. A red run here means a dependency of the lockfile on main has a new
# advisory; the fix is an update, or an entry in deny.toml with the reason it does not apply.

on:
schedule:
- cron: "17 5 * * 1"
workflow_dispatch:

permissions:
contents: read

jobs:
deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: repo
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with:
manifest-path: repo/Cargo.toml
arguments: --all-features
command: check
84 changes: 72 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ permissions:

env:
CARGO_TERM_COLOR: always
# Trivy for the image report, checked against its published SHA-256 before it runs
# (trivy_<version>_checksums.txt of the release).
TRIVY_VERSION: 0.75.0
TRIVY_SHA256: c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f

# The cloud depends on the network's API crate by relative path (`../network/crates/api`), so
# every job checks out both repositories side by side.
Expand All @@ -25,18 +29,20 @@ jobs:
run:
working-directory: cloud
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: cloud
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- uses: dtolnay/rust-toolchain@v1
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: stable
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2.9.3
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: cloud
- name: Format
Expand All @@ -56,7 +62,8 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18-alpine
# postgres:18-alpine, pinned by digest.
image: postgres:18-alpine@sha256:898c60cb390d1085bc97bdd6a1ae968757421df394b071848b52b1c09c89d6a5
env:
POSTGRES_PASSWORD: test
ports:
Expand All @@ -67,7 +74,8 @@ jobs:
--health-timeout 5s
--health-retries 30
nats:
image: nats:2.15-alpine
# nats:2.15-alpine, pinned by digest.
image: nats:2.15-alpine@sha256:7ae34c25dfd6fe2e01c6483eabef20d796690fdcac6c4d94ca780a6cf9877fde
ports:
- 4222:4222
defaults:
Expand All @@ -77,17 +85,19 @@ jobs:
ANYPLAY_TEST_DATABASE_URL: postgres://postgres:test@127.0.0.1:5432/postgres
ANYPLAY_TEST_NATS_URL: nats://127.0.0.1:4222
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: cloud
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- uses: dtolnay/rust-toolchain@v1
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: stable
- uses: Swatinem/rust-cache@v2.9.3
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: cloud
- name: Test on PostgreSQL, and as a cluster of two nodes
Expand All @@ -96,11 +106,13 @@ jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: cloud
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- name: Build the cloud image
Expand Down Expand Up @@ -140,6 +152,54 @@ jobs:
docker logs smoke-cloud || true
docker logs smoke-relay || true

# Nothing pushes these images, so there is no registry to attach an SBOM or a provenance
# attestation to; the first workflow that pushes one adds both (docs/supply-chain.md). What
# CI can do today is look at what it built: a CycloneDX SBOM of each image (kept as an
# artifact) and a Trivy report of fixable HIGH and CRITICAL findings in the job summary.
#
# Report only, on purpose: a new CVE in a Debian package would turn every unrelated pull
# request red, and nobody has seen the baseline yet. To make it block, set TRIVY_EXIT_CODE
# to "1" below, drop `continue-on-error`, and accept known findings in a `.trivyignore`
# with a reason for each.
- name: Install Trivy
run: |
curl -fsSL -o trivy.tar.gz \
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
echo "${TRIVY_SHA256} trivy.tar.gz" | sha256sum -c -
tar -xzf trivy.tar.gz trivy
sudo install trivy /usr/local/bin/trivy
- name: SBOM and vulnerability report of the images
continue-on-error: true
env:
TRIVY_EXIT_CODE: "0"
run: |
mkdir -p image-reports
status=0
{
echo "## Image scan (report only)"
echo
echo "HIGH and CRITICAL findings that have a fix. Not blocking yet: docs/supply-chain.md."
} >> "$GITHUB_STEP_SUMMARY"
for image in anyplay-cloud anyplay-relay anyplay-caddy; do
trivy image --quiet --format cyclonedx --output "image-reports/$image.cdx.json" "$image"
trivy image --quiet --severity HIGH,CRITICAL --ignore-unfixed \
--exit-code "$TRIVY_EXIT_CODE" --format table --output "image-reports/$image.txt" "$image" || status=$?
{
echo
echo "### $image"
echo '```'
cat "image-reports/$image.txt"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
done
exit "$status"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: always()
with:
name: image-reports
path: image-reports/*
if-no-files-found: ignore

# Links, title IDs in code, licences and advisories: the rules every AnyPlay repository shares.
rules:
uses: anyplay-gg/.github/.github/workflows/rules.yml@main
Expand Down
14 changes: 8 additions & 6 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,24 @@ jobs:
run:
working-directory: cloud
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: cloud
- uses: actions/checkout@v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- uses: dtolnay/rust-toolchain@v1
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # v1
with:
toolchain: nightly
- uses: Swatinem/rust-cache@v2.9.3
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: cloud/fuzz
- name: Install cargo-fuzz
run: cargo install cargo-fuzz --version 0.13.2 --locked
- uses: actions/cache@v6.1.0
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: cloud/fuzz/corpus/${{ matrix.target }}
key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
Expand All @@ -67,7 +69,7 @@ jobs:
if: github.event_name != 'pull_request'
run: cargo fuzz cmin ${{ matrix.target }}
- if: failure()
uses: actions/upload-artifact@v7.0.2
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: crash-${{ matrix.target }}
path: cloud/fuzz/artifacts/${{ matrix.target }}
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/kubernetes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ env:
KIND_VERSION: v0.33.0
# The official NATS chart, https://github.com/nats-io/k8s (appVersion 2.15.1).
NATS_CHART_VERSION: 2.15.1
CURL_IMAGE: curlimages/curl:8.21.0
# curlimages/curl:8.21.0, pinned by the digest of its multi-platform index.
CURL_IMAGE: curlimages/curl:8.21.0@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13

# The cloud depends on the network's API crate by relative path, so both repositories are
# checked out side by side, like in ci.yml. The chart lives in cloud/deploy/helm.
Expand All @@ -41,6 +42,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: ${{ env.HELM_VERSION }}
Expand Down Expand Up @@ -93,9 +96,11 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
path: cloud
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
repository: anyplay-gg/network
path: network
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The services behind the [AnyPlay network](https://github.com/anyplay-gg/network)
| `deploy/` | Dockerfile, `compose.yaml`, Caddyfile and an example `.env` |
| `deploy/helm/` | the [Helm chart](docs/kubernetes.md) that runs the cloud as a cluster on Kubernetes, with an example CloudNativePG database and the files for the kind test |
| `fuzz/` | [fuzz targets](fuzz/README.md) for the token check, run every night |
| `docs/` | [architecture](docs/ARCHITECTURE.md), [settings](docs/configuration.md), [decisions](docs/adr) and [EU compliance drafts](docs/compliance/README.md) |
| `docs/` | [architecture](docs/ARCHITECTURE.md), [settings](docs/configuration.md), [decisions](docs/adr), [EU compliance drafts](docs/compliance/README.md) and [supply chain](docs/supply-chain.md) |

## Host your own

Expand Down
13 changes: 10 additions & 3 deletions deploy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
# docker build -f cloud/deploy/Dockerfile --target relay -t anyplay-relay .
#
# `compose.yaml` does this for you. Dockerfile.dockerignore keeps everything else out of the context.
#
# Every base image is pinned by the digest of its multi-platform index, with the tag it was taken
# from in the comment above it; Dependabot (the `docker` ecosystem in .github/dependabot.yml) moves
# both together. Nothing here pushes an image: CI only builds and scans them (docs/supply-chain.md).

FROM rust:1-bookworm AS build
# rust:1-bookworm
FROM rust:1-bookworm@sha256:48b7f435b8fc6378ce43b44f15e3a47fd8919b2825f835c39bd4bd3da50af5b8 AS build
WORKDIR /src
COPY network ./network
COPY cloud ./cloud
WORKDIR /src/cloud
RUN cargo build --release --locked -p anyplay-cloud -p anyplay-relay

FROM debian:bookworm-slim AS runtime
# debian:bookworm-slim
FROM debian:bookworm-slim@sha256:7ff6ba31527b9e336162fa061746c366c4b2032f4d2cff2078bea43f78efe3f8 AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
Expand Down Expand Up @@ -43,7 +49,8 @@ ENTRYPOINT ["anyplay-relay"]

# Caddy as user 10001, so that the relay, which runs as the same user, can read the certificates
# Caddy fetches. Caddy keeps its files in /data and /config.
FROM caddy:2 AS caddy
# caddy:2
FROM caddy:2@sha256:504a44d7d74bd2bc28a66afc4774912fc21223ca8a67734af1c54ab2f999b489 AS caddy
RUN mkdir -p /data /config \
&& chown -R 10001:10001 /data /config
USER 10001:10001
45 changes: 45 additions & 0 deletions docs/supply-chain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Supply chain

What is pinned in this repository, how updates arrive, what CI says about the container images, and what is not covered yet. The cloud has no public release or registry image yet ([ARCHITECTURE.md](ARCHITECTURE.md)); the images are built in CI and by operators with [deploy/Dockerfile](../deploy/Dockerfile).

## What is pinned

| What | How |
|---|---|
| GitHub Actions | Every third-party action is pinned by full commit SHA with the release it was taken from in a `# vX.Y.Z` comment. The SHA is looked up from the release tag with `gh api repos/<owner>/<action>/git/ref/tags/<tag>`. |
| Workflow permissions | `contents: read` for every workflow; nothing here writes to the repository. |
| Checkouts | `persist-credentials: false`. Nothing in these workflows pushes. |
| Base images | `rust`, `debian` and `caddy` in the Dockerfile, and `postgres`, `nats` and `curl` in the workflows, are pinned by the digest of their multi-platform index, with the tag in a comment. The image build uses `cargo build --locked`. |
| Tools downloaded in CI | `helm`, `kubeconform` and Trivy are installed at an exact version; kubeconform and Trivy are checked against a SHA-256 in the workflow before they run. `cargo-fuzz` is installed with `--locked` at an exact version. |
| Licences, advisories, sources | `cargo deny` with [deny.toml](../deny.toml) on every pull request (the shared `rules` workflow) and every Monday (`advisories.yml`). The licences must allow use under AGPL-3.0; unknown registries and git sources are refused. |

The shared `rules` workflow comes from `anyplay-gg/.github@main`, which is the organisation's own code and is not pinned to a commit. The Helm chart's test fixtures under `deploy/helm` (a plain PostgreSQL and the NATS chart) are not digest-pinned yet.

## How updates arrive

[Dependabot](../.github/dependabot.yml) opens one pull request a week for the actions (all in one), one for the crates of the workspace and of `fuzz/` (minor and patch updates in one; a major update by itself) and one for the base images of the Dockerfile, where it changes tag and digest together. It waits a week after a release before it proposes it, so a release that is pulled or found malicious in its first days never reaches us. Security updates ignore that wait.

The workspace names the network's API crate by path (`../network/crates/api`). Dependabot sees only this repository, so it may not be able to resolve the crates and report an error instead of a pull request; update the crates by hand (`cargo update`, then the checks CI runs) until that dependency is a version.

## The images

CI builds the three images (`cloud`, `relay`, `caddy`) and does not push them, so there is no registry to attach an SBOM or a provenance attestation to. What the `docker` job does instead:

- It writes a CycloneDX SBOM of each built image and keeps them as the artifact `image-reports`.
- It runs [Trivy](https://trivy.dev/) on each image and writes the HIGH and CRITICAL findings that have a fix into the job summary.

**The scan does not block, on purpose.** Nobody has seen yet what the baseline of three Debian-based images looks like, and a new CVE in a Debian package would turn every unrelated pull request red until someone updated the base image. Look at the first reports, update the base images (Dependabot does), and accept what remains with a reason. Then make it block: in `.github/workflows/ci.yml` set `TRIVY_EXIT_CODE` of the step "SBOM and vulnerability report of the images" to `"1"`, remove its `continue-on-error`, and put the findings you accept in a `.trivyignore` file with a comment for each.

**When a workflow starts to push an image**, it should build with `docker/build-push-action` with `sbom: true` and `provenance: mode=max`, so the registry holds the SBOM and a SLSA provenance next to the image, and attest the pushed digest with `actions/attest-build-provenance` (`subject-name` the image name, `subject-digest` the build's digest, `push-to-registry: true`). That job needs `packages: write`, `id-token: write` and `attestations: write`, and nothing else. Pin those actions like all others. Operators then verify with `gh attestation verify oci://<image> --repo anyplay-gg/cloud`.

## Verify

Nothing is released yet. To check a source tree: `cargo deny --all-features check`; to check an image you built, `trivy image <image>`.

## What is not covered yet

- **Pushed images, their SBOM, provenance and signature**, as above: there is no pushing workflow.
- **Reproducible builds.** The build stage compiles on `rust:1-bookworm` (pinned by digest, which Dependabot moves) and the runtime stage runs `apt-get install ca-certificates`, which takes whatever Debian serves that day.
- **`cargo auditable` in the images**: the binaries do not carry their dependency list yet (the launcher's do).
- **Commit and tag signing**, and branch protection, are repository settings and are not switched on.
- **The Rust toolchain** in CI is `stable` as installed by `dtolnay/rust-toolchain`, not a pinned version.
Loading