The README grew alongside the tool and ended up in the order the work happened rather than the order a reader needs. Three things were wrong with it. The two-tier split was implicit, so a reader had to sort out which paragraphs applied to the host they were building. The opening defined the whole tool as "no Docker socket in job containers, and deny-by-default egress", which describes one tier only - someone building the other was told it does things it deliberately will not do. And long-form justification sat between the reader and the steps. So the README is now tier-first: pick a tier from a comparison table, then follow only that tier's half. Everything that is background rather than instruction moved to docs/reference.md, with docs/README.md indexing it and the Proxmox runbook. The runbook stays in proxmox/ beside the firewall template it explains. The tier names changed with it. `ci` and `runtime` were dejima's words for these hosts: `ci` distinguishes nothing on a tool whose whole subject is CI, and `runtime` only means something if you already know dejima's runtime tests are the workload that forced the tier to exist. They are `confined` and `privileged` now, after what a job on each is granted. The distinction that generalises is where the security boundary sits - inside the host for one, on the hypervisor for the other - and the table leads with it. `unprivileged`/`privileged` would have been the more standard pairing and was rejected: two characters apart on a flag where a misread silently produces a host that looks hardened and is not. The old --role values still work and say what they are now called rather than translating silently. examples/ is renamed to match on both sides, so no file's tier is implied by the absence of a suffix, and the config mounts in the compose files follow. Written down explicitly for the first time: the privileged tier has no egress allowlist by design. Its jobs hold the socket and run in the host's network namespace, so any filter installed there is one they can flush. Installing it anyway would leave a host that looks like it has an allowlist, which is worse than one that plainly does not. Also adds GPL-3.0-or-later to match dejima: LICENSE plus SPDX headers on the scripts. bats test/ passes, 21/21. All four --role values and an invalid one behave as intended; the compose and act_runner examples still parse.
110 lines
4.7 KiB
YAML
110 lines
4.7 KiB
YAML
# The confined runners: several on one host, sharing one config file and
|
|
# therefore one label, so a job naming that label lands on whichever is free.
|
|
#
|
|
# The privileged runner is NOT here. It has its own compose file, because it
|
|
# belongs on its own host - see examples/docker-compose-privileged.yml.
|
|
#
|
|
# The thing to understand before reading the rest: compose's networks and the
|
|
# job network are different worlds. Every service below is an act_runner
|
|
# process. The containers that actually execute agent-authored code are
|
|
# created by the Docker daemon, on the network named by `container.network` in
|
|
# each runner's config.yaml, and compose knows nothing about them. Attaching a
|
|
# service here to a network does not put its jobs there, and vice versa. See
|
|
# docs/reference.md for what that means for isolating jobs from each other.
|
|
#
|
|
# Not runnable as written: substitute your own instance URL, image tag and
|
|
# subnet, and create the job network first - install.sh does it for you.
|
|
|
|
name: dejima-runners
|
|
|
|
# Shared by every confined runner. Volumes are per-service rather than here,
|
|
# because each runner needs its own /data - and YAML merge keys merge mappings
|
|
# but not lists, so a volumes: in a service replaces this one rather than
|
|
# adding to it. Repeating them is the honest form.
|
|
x-ci-runner: &ci-runner
|
|
# Pin the tag. These processes execute untrusted code on every pull request,
|
|
# and :latest means the thing doing that can change without you deciding it
|
|
# should.
|
|
image: gitea/act_runner:0.6.1
|
|
restart: unless-stopped
|
|
networks: [runners]
|
|
|
|
x-ci-env: &ci-env
|
|
# Without this the config file is not read AT ALL - the runner starts
|
|
# happily on built-in defaults, which means the Docker socket goes into
|
|
# every job container and nothing anywhere says so. If you change one thing
|
|
# in a config file and see no effect, check this first.
|
|
CONFIG_FILE: /config.yaml
|
|
GITEA_INSTANCE_URL: https://gitea.example.com
|
|
# Needed only until each runner has registered; the identity then lives in
|
|
# that runner's own /data volume. Comes from a .env file beside this one
|
|
# (chmod 600) so it is not in the file you commit, and is worth deleting
|
|
# once registration succeeds - anyone holding a registration token can
|
|
# register a runner and start receiving jobs, secrets included.
|
|
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN:-}
|
|
|
|
services:
|
|
# --- confined runners -----------------------------------------------------
|
|
# All three share one config file, and therefore one label and one job
|
|
# network. That is what makes them a pool: a job naming dejima-ci lands on
|
|
# whichever is free. Only the name and the data volume differ.
|
|
ci-1:
|
|
<<: *ci-runner
|
|
container_name: dejima-ci-1
|
|
environment:
|
|
<<: *ci-env
|
|
GITEA_RUNNER_NAME: dejima-ci-1
|
|
volumes:
|
|
# The runner's OWN access to the daemon, so it can create job
|
|
# containers. NOT the same setting as container.docker_host, which
|
|
# decides whether job containers get the socket - see docs/reference.md.
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
# Read-only: a runner has no reason to rewrite the file deciding what
|
|
# its jobs are granted.
|
|
- ./act-runner-confined.yaml:/config.yaml:ro
|
|
# Its registration identity. One per runner, never shared - two runners
|
|
# cannot share a .runner.
|
|
- ci-1-data:/data
|
|
|
|
ci-2:
|
|
<<: *ci-runner
|
|
container_name: dejima-ci-2
|
|
environment:
|
|
<<: *ci-env
|
|
GITEA_RUNNER_NAME: dejima-ci-2
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
- ./act-runner-confined.yaml:/config.yaml:ro
|
|
- ci-2-data:/data
|
|
|
|
ci-3:
|
|
<<: *ci-runner
|
|
container_name: dejima-ci-3
|
|
environment:
|
|
<<: *ci-env
|
|
GITEA_RUNNER_NAME: dejima-ci-3
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
- ./act-runner-confined.yaml:/config.yaml:ro
|
|
- ci-3-data:/data
|
|
|
|
networks:
|
|
# The runner containers' own network, and nothing else's. They need it only
|
|
# to reach your Gitea instance; they never talk to each other. Sharing one
|
|
# is fine because these are all equally trusted processes - every one of
|
|
# them holds the Docker socket already, so putting them on separate networks
|
|
# would protect nothing.
|
|
#
|
|
# The job network is deliberately absent. It is created by hand with a fixed
|
|
# bridge name so the firewall can match on it (install.sh does this), and
|
|
# act_runner attaches job containers to it by name. Declaring it here would
|
|
# invite attaching a service to it, which is exactly what must not happen: a
|
|
# process holding the Docker socket has no business sharing a network with
|
|
# containers running untrusted code.
|
|
runners:
|
|
|
|
volumes:
|
|
ci-1-data:
|
|
ci-2-data:
|
|
ci-3-data:
|