G7: route CI jobs via the public surface, not the forge network
Some checks failed
check / gate (push) Failing after 13s

The first CI run failed with 'Could not resolve host: gitea' — job containers
live on dind's private network and cannot see the forge network. Two ways out,
and they are not equivalent:

  (a) put job containers on the forge network. Easy, one line, and it leaves
      untrusted workflow code one DNS name from the forge's Postgres. It quietly
      repeals I-5.
  (b) send jobs to the PUBLIC forge surface over the tunnel, exactly like any
      stranger on the internet.

Took (b). The runner no longer needs the forge network at all, so there is now
NO private route from any CI container to anything — a better posture than this
file started with. Cost is a hairpin through Cloudflare plus its ~100s ceiling
per fetch, which for 0.63 GB of objects across 61 repos and depth=1 checkouts is
nowhere near binding.

Test upgraded to assert the stronger property: no CI container joins the forge
network.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Grant Whitmer
2026-08-12 10:51:00 -04:00
parent 5f717ef74b
commit 53dea84ab1
2 changed files with 39 additions and 6 deletions

View File

@@ -415,6 +415,19 @@ def test_i05_jobs_cannot_bind_mount_from_the_daemon_host():
assert 'docker_host: "-"' in cfg
def test_i05_no_ci_container_can_reach_the_forge_network():
"""The first CI run failed with "Could not resolve host: gitea" because job
containers sit on dind's private network. The easy fix — putting jobs on the
forge network — would have left untrusted workflow code one DNS name from
the forge's Postgres. Instead jobs reach the PUBLIC forge surface, so no CI
container has a private route to anything."""
compose = (ROOT / "deploy" / "runner" / "docker-compose.yml").read_text()
active = [ln for ln in compose.splitlines() if ln.strip() and not ln.strip().startswith("#")]
joined = "\n".join(active)
assert "windy-git_default" not in joined, "I-5: no CI container joins the forge network"
assert "https://app.windygit.com" in joined
def test_i05_runner_is_a_separate_compose_project_from_the_forge():
"""Runners restart, crash, get starved and get killed. None of that should
ever touch the thing serving repositories."""

View File

@@ -55,14 +55,37 @@ services:
environment:
# The runner reaches its OWN daemon. Never the host's.
DOCKER_HOST: tcp://dind:2375
GITEA_INSTANCE_URL: http://gitea:3000
# ⚠️ THE PUBLIC URL, deliberately — not http://gitea:3000.
#
# Job containers run inside the dind daemon's own private network, so they
# cannot resolve `gitea`, which lives on the forge network. The first CI
# run failed exactly here: "Could not resolve host: gitea".
#
# There were two ways out, and they are not equivalent:
# (a) put job containers on the forge network — untrusted workflow code
# would then sit one DNS name away from the forge's Postgres. This
# is the easy fix and it quietly repeals I-5.
# (b) send jobs to the PUBLIC forge surface, over the tunnel, exactly
# like any stranger on the internet. Untrusted code gets no private
# network route at all.
#
# (b) is strictly better and it is what this is. The cost is a hairpin —
# container -> tunnel -> Cloudflare -> back to this box — plus Cloudflare's
# ~100s proxy ceiling on any single fetch (G4A.5). For repos measured at
# 0.63 GB of objects across 61 repos, with depth=1 checkouts, that ceiling
# is nowhere near being a problem. Revisit if a model repo ever needs CI.
GITEA_INSTANCE_URL: https://app.windygit.com
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN:?set RUNNER_TOKEN}
GITEA_RUNNER_NAME: veron-1
CONFIG_FILE: /config.yaml
volumes:
- ./config.yaml:/config.yaml:ro
- runner-data:/data
networks: [jobs, forge]
# Only `jobs`. The runner no longer needs the forge network at all, because
# it collects work over the public surface too — so there is now NO path
# from any CI container to the forge's database. That is a better posture
# than the one this file started with.
networks: [jobs]
cpus: 2.0
mem_limit: 4g
restart: unless-stopped
@@ -71,11 +94,8 @@ networks:
jobs:
# Untrusted job containers live here. No route to the forge.
internal: false # jobs legitimately need to fetch dependencies
forge:
# Pre-existing network owned by the forge compose project.
external: true
name: windy-git_default
volumes:
dind-storage:
runner-data: