compose.release.yaml
Rama main · réplica de lectura del repositorio original.
name: rootguard
# RootGuard 1.0.0 public evaluation stack.
# Every release image is pinned to its verified multi-architecture manifest.
# Copy .env.release.example to .env and replace all placeholder secrets.
services:
# The one, narrow, auditable bridge letting cosign's own attestation
# checks (see core's/updater's ROOTGUARD_ATTESTATION_PROXY_URL below)
# reach GHCR/Sigstore from the fully internet-isolated `control`
# network - found live, cutting 1.0.0-rc.2: Core/the Updater never had
# any route to the internet at all, so `RequireAttestation`'s cosign
# calls could never succeed once wired into the real deploy/update
# path. See rootguard-attestation-proxy/README.md for the full
# rationale and its hardcoded, 4-host allowlist (the 4th host,
# api.github.com, was added for Core's GitHub Releases self-update
# discovery - found in review that it ran on this same isolated
# network but was never actually routed through this proxy).
attestation-proxy:
# Bootstrap pin: this component's very first published image, pushed
# by hand (real, public, multi-arch manifest - no real release has
# cut it yet) so `docker compose pull` and the "Clean install" CI
# check have something real to fetch before release-alpha.yml's own
# update-alpha-pins has ever run for it. Gets superseded by that
# step's own real digest the moment a real release actually
# promotes this component, same as every other component's own
# first-ever pin.
image: "${ROOTGUARD_ATTESTATION_PROXY_IMAGE:-ghcr.io/foxly-it/rootguard-attestation-proxy:1.0.12@sha256:0a8e65d95e62a07149989a98d9db71b68e5ff39da32b10b54bc53aeee80a6aaf}"
container_name: rootguard-attestation-proxy
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
networks:
- control
- egress
healthcheck:
test: ["CMD", "/rootguard-attestation-proxy", "healthcheck"]
interval: 15s
timeout: 3s
retries: 5
# The only container holding the real Docker socket - Core and the
# Updater reach it over `control` instead (see their DOCKER_HOST/
# ROOTGUARD_DOCKER_PROXY_URL below), never mounting the socket
# themselves. See docs/threat-model.md, actor 1 ("Docker socket
# holders"), and rootguard-docker-proxy/README.md for the full
# rationale - an allow-listed, request-body-validated subset of the
# Docker Engine API, not a caller-authentication boundary between Core
# and the Updater (they share the same allowlist). Unlike
# attestation-proxy, this only ever dials the local socket, never the
# internet, so it stays off `egress` entirely.
docker-proxy:
# Bootstrap pin: this component just joined release-alpha.yml's own
# publish matrix (#666) - before this, nothing automatically refreshed
# this pin, which went stale twice (1.0.2, then again 1.0.4; see
# docs/release-history.md for both). Gets superseded by
# update-alpha-pins' own real digest the moment the next real release
# promotes it, same as every other component's own first-ever pin.
image: "${ROOTGUARD_DOCKER_PROXY_IMAGE:-ghcr.io/foxly-it/rootguard-docker-proxy:1.0.12@sha256:a584efc34eb93dfff71d4a4258946124f499572044c7ea46499f7fe3f5724f74}"
container_name: rootguard-docker-proxy
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
volumes:
# Defaults to the rootful daemon's real socket. Rootless Docker's own
# socket lives at a per-user path instead (typically
# /run/user/<uid>/docker.sock) - install.sh detects a rootless
# daemon and sets ROOTGUARD_DOCKER_SOCKET_PATH accordingly; a manual
# deploy sets it directly. See docs/rootless-docker.md for the full
# rootless-Docker findings (a real client-source-IP risk with the
# default networking driver, resolved by using `pasta` instead).
- ${ROOTGUARD_DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
networks:
- control
healthcheck:
test: ["CMD", "/rootguard-docker-proxy", "healthcheck"]
interval: 15s
timeout: 3s
retries: 5
core:
image: "${ROOTGUARD_CORE_IMAGE:-ghcr.io/foxly-it/rootguard-core:1.0.12@sha256:03873550b521e99656ab7a16684f2a234c91d788407b5c4ff0c39099cad8938d}"
container_name: rootguard-core
restart: unless-stopped
depends_on:
updater:
condition: service_healthy
attestation-proxy:
condition: service_healthy
docker-proxy:
condition: service_healthy
environment:
PORT: "8081"
# See rootguard-core/internal/stack/attestation.go's
# runAttestationCommand doc comment for the full rationale - scoped
# to cosign's own subprocess only, never Core's ambient environment.
ROOTGUARD_ATTESTATION_PROXY_URL: http://attestation-proxy:8888
# Redirects every `docker`/`docker compose` call dockercli.Run shells
# out to (rootguard-core/internal/dockercli) at the real socket
# docker-proxy itself holds - the Docker CLI already honors
# DOCKER_HOST from the environment, so this needs no code change.
# ROOTGUARD_DOCKER_PROXY_URL is this same endpoint, http:// instead
# of tcp://, for CheckDockerProxyReachable's own /healthz check at
# startup (rootguard-core/internal/stack/dockerproxy.go) - unset on
# an installation whose compose.release.yaml predates this change,
# which keeps mounting the real socket directly and keeps working
# exactly as before (see that file's doc comment).
DOCKER_HOST: tcp://docker-proxy:2375
ROOTGUARD_DOCKER_PROXY_URL: http://docker-proxy:2375
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_CORE_CONTAINER: rootguard-core
# Test-only escape hatch - see cmd/rootguard/main.go's own doc
# comment on ROOTGUARD_SKIP_ATTESTATION for what it disables and
# why. Never set outside an E2E fixture; defaults to enforcing.
ROOTGUARD_SKIP_ATTESTATION: "${ROOTGUARD_SKIP_ATTESTATION:-false}"
ROOTGUARD_INSTALLATION_DIR: /var/lib/rootguard/installation
ROOTGUARD_UNBOUND_IMAGE: "${ROOTGUARD_UNBOUND_IMAGE:-ghcr.io/foxly-it/rootguard-unbound:1.0.12@sha256:d1f257426769c54f6a89fcef0a40f4dfb2ab98ea47874d3074dad6c86c7a51e7}"
ROOTGUARD_ADGUARD_IMAGE: "${ROOTGUARD_ADGUARD_IMAGE:-adguard/adguardhome:v0.107.80@sha256:e2e1b48c832f7705be915873f95de0d5114de573736441d7f2923a8a6293027c}"
ROOTGUARD_ADGUARD_BETA_IMAGE: "${ROOTGUARD_ADGUARD_BETA_IMAGE:-adguard/adguardhome:beta@sha256:2b77703b27730d5c0c7045fcd6c98834169cd5c69af5f86a43947425f2d367fd}"
ROOTGUARD_ADGUARD_UPDATE_IMAGE: "${ROOTGUARD_ADGUARD_UPDATE_IMAGE:-adguard/adguardhome:v0.107.80@sha256:e2e1b48c832f7705be915873f95de0d5114de573736441d7f2923a8a6293027c}"
# Digest-pinned like every other RootGuard-built component now -
# found in review: this was unset here entirely before, so Core
# silently fell back to its own code default of a mutable :latest
# tag with no digest pin and no attestation ever checked, unlike
# core/webapp/unbound/the updater.
ROOTGUARD_BLOCKPAGE_IMAGE: "${ROOTGUARD_BLOCKPAGE_IMAGE:-ghcr.io/foxly-it/rootguard-blockpage:1.0.12@sha256:d1b39a97f940598b60b0dd0b7a1efa2150fc6020d09bff1615f26c59dfa3d447}"
# Found in review: Blockpage had no ServiceSpec in either update
# manager at all, so an installation could only ever get a
# Blockpage fix via reinstall or a manual compose refresh - unlike
# every other RootGuard-built component. Same digest-pinned,
# refreshed-every-release treatment as ROOTGUARD_UNBOUND_UPDATE_IMAGE
# right below (live GitHub Releases discovery is the primary
# source; this pin is only the fallback for when that lookup fails).
ROOTGUARD_BLOCKPAGE_UPDATE_IMAGE: "${ROOTGUARD_BLOCKPAGE_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-blockpage:1.0.12@sha256:d1b39a97f940598b60b0dd0b7a1efa2150fc6020d09bff1615f26c59dfa3d447}"
ROOTGUARD_UNBOUND_UPDATE_IMAGE: "${ROOTGUARD_UNBOUND_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-unbound:1.0.12@sha256:d1f257426769c54f6a89fcef0a40f4dfb2ab98ea47874d3074dad6c86c7a51e7}"
ROOTGUARD_UPDATE_DIR: /var/lib/rootguard/updates
ROOTGUARD_CONTROL_PLANE_UPDATER_URL: http://updater:8082
ROOTGUARD_CONTROL_PLANE_UPDATER_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
# Found in review: this comment used to claim the value below stays
# an unpinned :latest fallback, but it's digest-pinned right here
# like every other value above and refreshed by the same
# update-alpha-pins step (it iterates every managed component
# generically, this one included) - stale ever since it was first
# digest-pinned. Live GitHub Releases discovery
# (ROOTGUARD_SELF_UPDATE_DIR below) is still the primary source;
# this pin is only the fallback for when that lookup fails.
ROOTGUARD_UPDATER_UPDATE_IMAGE: "${ROOTGUARD_UPDATER_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-updater:1.0.12@sha256:c4cd325070ba9a7442e6b06705c621edcf581cd8a8683076c510c7b87c3c8317}"
# Same fallback rationale as ROOTGUARD_UPDATER_UPDATE_IMAGE above -
# live GitHub Releases discovery is the primary source
# (rootguard#481: the attestation proxy joined self-update
# management, no longer static/manually-pinned only).
ROOTGUARD_ATTESTATION_PROXY_UPDATE_IMAGE: "${ROOTGUARD_ATTESTATION_PROXY_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-attestation-proxy:1.0.12@sha256:0a8e65d95e62a07149989a98d9db71b68e5ff39da32b10b54bc53aeee80a6aaf}"
ROOTGUARD_SELF_UPDATE_DIR: /var/lib/rootguard/updater-self-update
# Core replaces the updater's own container the same way it already
# replaces AdGuard/Unbound - via `docker compose ... up -d --no-deps
# updater` against this same control-plane project. The mounted file
# below is intentionally exposed as compose.yaml (not
# compose.release.yaml) regardless of this stack's real filename,
# since Core's generic update manager always looks for a file
# literally named compose.yaml in whichever directory it's pointed at.
ROOTGUARD_COMPOSE_FILE: /opt/rootguard/compose.yaml
ROOTGUARD_COMPOSE_PROJECT: rootguard
# Core never reads these five - they exist only so `docker compose -f
# compose.yaml ... up -d --no-deps updater` (and, one hop further,
# `... up -d --no-deps core webapp`) can interpolate the whole
# compose file, which also references them for the webapp service.
# Same reason the updater service carries them. Found in review:
# only the two `${VAR:?...}` ones were carried here - the three
# `${VAR:-default}` ones below were missed, so a self-update-
# triggered `up -d --no-deps webapp` silently re-created it with
# ROOTGUARD_WEB_BIND=0.0.0.0/ROOTGUARD_WEB_PORT=8080/
# ROOTGUARD_ADMIN_USER=admin regardless of whatever an operator had
# actually set in .env (e.g. binding to 127.0.0.1 behind a reverse
# proxy, see docs/https-reverse-proxy.md) - it fails loudly instead
# for the two required ones, silently for these three optional ones.
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_WEB_BIND: "${ROOTGUARD_WEB_BIND:-0.0.0.0}"
ROOTGUARD_WEB_PORT: "${ROOTGUARD_WEB_PORT:-8080}"
# Propagates the real host checkout directory into `docker compose -f
# /opt/rootguard/compose.yaml ...` re-invocations (self-update and the
# control-plane update both do this from inside a container). Without
# this, `${PWD}` below re-resolves against the wrong process's cwd and
# silently mounts an empty directory instead of the real file - see the
# matching comment on the volumes line.
PWD: "${PWD}"
UNBOUND_CONFIG_DIR: /var/lib/rootguard/unbound
UNBOUND_CONTAINER_CONFIG_DIR: /etc/unbound/unbound.d
UNBOUND_CONTAINER_NAME: rootguard-unbound
ADGUARD_INSTALLER_URL: http://rootguard-adguard:3000
ADGUARD_API_URL: http://rootguard-adguard:80
ADGUARD_DATA_DIR: /var/lib/rootguard/adguard
ADGUARD_UPSTREAM: 172.29.53.2:5335
ADGUARD_DNS_ADDRESS: rootguard-adguard:53
ROOTGUARD_BLOCKPAGE_AUTH_DIR: /var/lib/rootguard/adguard-auth
volumes:
- rootguard-data:/var/lib/rootguard
- unbound-config:/var/lib/rootguard/unbound
- adguard-auth:/var/lib/rootguard/adguard-auth
# ${PWD}, not a relative "./compose.release.yaml": see the matching
# PWD/${PWD} comments on this file's core service, above - the same
# self-referential-mount hazard applies here.
- ${PWD}/compose.release.yaml:/opt/rootguard/compose.yaml:ro
networks:
- control
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8081/api/health"]
interval: 15s
timeout: 5s
retries: 5
webapp:
image: "${ROOTGUARD_WEBAPP_IMAGE:-ghcr.io/foxly-it/rootguard-webapp:1.0.12@sha256:efbf103319426f06f84018d97b22803afb47b6a40f6d13a0f2cf0e1f030eeb7c}"
container_name: rootguard-webapp
restart: unless-stopped
depends_on:
core:
condition: service_healthy
environment:
PORT: "8080"
ROOTGUARD_CORE_URL: http://core:8081
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_SESSION_FILE: /var/lib/rootguard-sessions/sessions.json
volumes:
- rootguard-sessions:/var/lib/rootguard-sessions
ports:
- "${ROOTGUARD_WEB_BIND:-0.0.0.0}:${ROOTGUARD_WEB_PORT:-8080}:8080/tcp"
networks:
- control
- edge
# Found in review: missing here despite compose.yaml (dev) and
# compose.integration.yaml both having it - the one public entrypoint
# had no health state at all in the file this release actually ships.
# The distroless runtime image has neither wget nor a shell - unlike
# core/updater's docker:29-cli base, there's nothing here for a
# "CMD wget ..." style check to exec. rootguard-webapp itself
# implements -healthcheck for exactly this (see cmd/rootguard-webapp).
healthcheck:
test: ["CMD", "/app/rootguard-webapp", "-healthcheck"]
interval: 15s
timeout: 5s
retries: 5
updater:
image: "${ROOTGUARD_UPDATER_IMAGE:-ghcr.io/foxly-it/rootguard-updater:1.0.12@sha256:c4cd325070ba9a7442e6b06705c621edcf581cd8a8683076c510c7b87c3c8317}"
container_name: rootguard-updater
restart: unless-stopped
depends_on:
attestation-proxy:
condition: service_healthy
docker-proxy:
condition: service_healthy
environment:
# See rootguard-updater/attestation.go's verifyAttestation doc
# comment for the full rationale - scoped to cosign's own
# subprocess only, never the Updater's ambient environment.
ROOTGUARD_ATTESTATION_PROXY_URL: http://attestation-proxy:8888
# See core's identical comment above - same redirection, same
# backward-compat behavior when unset.
DOCKER_HOST: tcp://docker-proxy:2375
ROOTGUARD_DOCKER_PROXY_URL: http://docker-proxy:2375
ROOTGUARD_UPDATER_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_UPDATER_DATA_DIR: /var/lib/rootguard/control-plane-updater
ROOTGUARD_COMPOSE_FILE: /opt/rootguard/compose.release.yaml
ROOTGUARD_COMPOSE_PROJECT: rootguard
ROOTGUARD_CORE_UPDATE_IMAGE: "${ROOTGUARD_CORE_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-core:1.0.12@sha256:03873550b521e99656ab7a16684f2a234c91d788407b5c4ff0c39099cad8938d}"
ROOTGUARD_WEBAPP_UPDATE_IMAGE: "${ROOTGUARD_WEBAPP_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-webapp:1.0.12@sha256:efbf103319426f06f84018d97b22803afb47b6a40f6d13a0f2cf0e1f030eeb7c}"
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_WEB_BIND: "${ROOTGUARD_WEB_BIND:-0.0.0.0}"
ROOTGUARD_WEB_PORT: "${ROOTGUARD_WEB_PORT:-8080}"
ROOTGUARD_CORE_IMAGE: "${ROOTGUARD_CORE_IMAGE:-ghcr.io/foxly-it/rootguard-core:1.0.12@sha256:03873550b521e99656ab7a16684f2a234c91d788407b5c4ff0c39099cad8938d}"
ROOTGUARD_WEBAPP_IMAGE: "${ROOTGUARD_WEBAPP_IMAGE:-ghcr.io/foxly-it/rootguard-webapp:1.0.12@sha256:efbf103319426f06f84018d97b22803afb47b6a40f6d13a0f2cf0e1f030eeb7c}"
ROOTGUARD_SESSION_DIR: /var/lib/rootguard-sessions
# See the matching PWD/${PWD} comments on the core service above - the
# same self-referential-mount hazard applies here now that Core can
# recreate this very service (self-update).
PWD: "${PWD}"
volumes:
- rootguard-data:/var/lib/rootguard
- rootguard-sessions:/var/lib/rootguard-sessions
- ${PWD}/compose.release.yaml:/opt/rootguard/compose.release.yaml:ro
networks:
- control
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8082/health"]
interval: 15s
timeout: 5s
retries: 5
networks:
edge:
control:
internal: true
# Not internal - real internet access, but only
# rootguard-attestation-proxy sits here. Core/the Updater stay on
# `control` only; they never join `egress` directly.
egress:
volumes:
unbound-config:
name: rootguard-unbound-config
rootguard-data:
name: rootguard-data
rootguard-sessions:
name: rootguard-sessions
adguard-auth:
name: rootguard-adguard-auth