diff --git a/README.md b/README.md index 44df895..fe4945c 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,12 @@ Everything in this section is **outside git**. Do it as root over SSH, in this o The combined host is NOT on the tailnet (operator decision 2026-09-07): ssh is key-only on the public IP, the CI front doors are public via traefik, and the opencode UI is public on the same -443 — traefik routes `oc.ci.commoninternet.net` (a dynamic-config file that -`opencode-ui-route.service` drops into the traefik stack's file-providers volume) to an nginx -vhost on the docker bridge that enforces basic auth. fail2ban guards sshd and that login +443 — traefik routes `oc.ci.commoninternet.net` to an nginx vhost on the docker bridge that +enforces basic auth, via traefik labels on a tiny swarm relay stack (`opencode-ui`) that +`opencode-ui-route.service` deploys. (Not via traefik's file *directory* provider: enabling +that in the coop-cloud traefik recipe REPLACES its provider file, which holds the wildcard cert +and the `security` middleware, and every front door goes down — it did, for two minutes, on +2026-09-07.) fail2ban guards sshd and that login (`nix/hosts/cc-ci/configuration.nix`; the nginx jail bans in the DOCKER-USER chain because the traffic is docker-forwarded, not host INPUT). diff --git a/nix/hosts/cc-ci/configuration.nix b/nix/hosts/cc-ci/configuration.nix index 66607bb..1f4ef94 100644 --- a/nix/hosts/cc-ci/configuration.nix +++ b/nix/hosts/cc-ci/configuration.nix @@ -29,8 +29,7 @@ cc-ci-orchestrator.ciSshHost = "127.0.0.1"; # The opencode UI: traefik (public 443, the *.ci.commoninternet.net cert) → nginx basic auth. cc-ci-orchestrator.opencodeUiHost = "oc.ci.commoninternet.net"; - cc-ci-orchestrator.opencodeUiTraefikProvidersDir = - "/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data"; + cc-ci-orchestrator.opencodeUiTraefikNetwork = "proxy"; # ---- no tailscale on this host (operator 2026-09-07) -------------------------------------- # Everything is reached over the public IP: ssh (keys only), the CI front doors via traefik, diff --git a/nix/modules/orchestrator-host.nix b/nix/modules/orchestrator-host.nix index 90e9db1..b03ad73 100644 --- a/nix/modules/orchestrator-host.nix +++ b/nix/modules/orchestrator-host.nix @@ -40,17 +40,18 @@ in ''; }; - opencodeUiTraefikProvidersDir = lib.mkOption { + opencodeUiTraefikNetwork = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; - example = "/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data"; + example = "proxy"; description = '' - Host path of the traefik stack's file-providers directory. When set, a dynamic-config - file routing opencodeUiHost on the `web-secure` entrypoint to nginx is written there after - deploy-proxy. Traefik only watches that directory when the coop-cloud traefik recipe is - deployed with FILE_PROVIDER_DIRECTORY_ENABLED=1 in its app env - (/root/.abra/servers/default/.env) — see README "Stage the workspace". - null = no traefik route. + Name of the swarm overlay network the cc-ci traefik watches (cc-ci's swarm.nix creates + `proxy`). When set, a one-container swarm stack `opencode-ui` (a socat TCP relay to + nginx on the docker bridge) is deployed with traefik labels routing opencodeUiHost on + the `web-secure` entrypoint — the same label mechanism every cc-ci service and recipe + uses, so it coexists with the traefik recipe's own file provider (the wildcard cert and + the `security` middleware live there; switching traefik to a file *directory* replaces + that file and takes every front door down — learned 2026-09-07). null = no route. ''; }; @@ -223,30 +224,37 @@ SSHCFG # Only docker's bridge may reach the plain-HTTP backend; the public interface stays closed. networking.firewall.interfaces.docker_gwbridge.allowedTCPPorts = [ cfg.opencodeUiBackendPort ]; - # The traefik side of the route: a dynamic-config file in the stack's watched providers dir. - systemd.services.opencode-ui-route = lib.mkIf (cfg.opencodeUiTraefikProvidersDir != null) { - description = "traefik route: ${cfg.opencodeUiHost} -> nginx (opencode UI basic auth)"; + # The traefik side of the route: a swarm service carrying the router labels. It is a plain + # TCP relay (socat) from the overlay network to nginx on the docker bridge; traefik's + # X-Forwarded-For passes through untouched, which is what nginx's real_ip reads. + systemd.services.opencode-ui-route = lib.mkIf (cfg.opencodeUiTraefikNetwork != null) { + description = "swarm stack opencode-ui: traefik labels ${cfg.opencodeUiHost} -> nginx basic auth"; wantedBy = [ "multi-user.target" ]; - after = [ "deploy-proxy.service" "docker.service" ]; + after = [ "deploy-proxy.service" "docker.service" "nginx.service" ]; wants = [ "deploy-proxy.service" ]; + path = [ pkgs.docker ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' - install -d -m 0755 "${cfg.opencodeUiTraefikProvidersDir}" - install -m 0644 ${pkgs.writeText "opencode-ui.yml" '' - # Written by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit. - http: - routers: - opencode-ui: - rule: Host(`${cfg.opencodeUiHost}`) - entryPoints: [web-secure] - service: opencode-ui - tls: {} - services: - opencode-ui: - loadBalancer: - servers: - - url: http://172.18.0.1:${toString cfg.opencodeUiBackendPort} - ''} "${cfg.opencodeUiTraefikProvidersDir}/opencode-ui.yml" + docker stack deploy --detach=true -c ${pkgs.writeText "opencode-ui-stack.yml" '' + # Deployed by opencode-ui-route.service (nix/modules/orchestrator-host.nix). Do not edit. + version: "3.8" + services: + relay: + image: alpine/socat:1.8.0.3 + command: ["TCP-LISTEN:${toString cfg.opencodeUiBackendPort},fork,reuseaddr", "TCP:172.18.0.1:${toString cfg.opencodeUiBackendPort}"] + networks: [ ${cfg.opencodeUiTraefikNetwork} ] + deploy: + replicas: 1 + labels: + - "traefik.enable=true" + - "traefik.http.routers.opencode-ui.rule=Host(`${cfg.opencodeUiHost}`)" + - "traefik.http.routers.opencode-ui.entrypoints=web-secure" + - "traefik.http.routers.opencode-ui.tls=true" + - "traefik.http.services.opencode-ui.loadbalancer.server.port=${toString cfg.opencodeUiBackendPort}" + networks: + ${cfg.opencodeUiTraefikNetwork}: + external: true + ''} opencode-ui ''; }; };