cc-ci host: no tailscale; fail2ban; opencode UI public on 8443 with the LE wildcard

Operator 2026-09-07: the combined host stays off the tailnet. ssh is
key-only (password + keyboard-interactive auth off), the opencode UI is
served as oc.ci.commoninternet.net on 8443 — TLS from the CI server's
*.ci.commoninternet.net ACME cert (new orchestrator-host option
opencodeUiAcmeHost; nginx joins the acme group) behind the existing basic
auth — and fail2ban watches sshd plus nginx's 401s (nginx-http-auth jail).
tailscaled ordering dropped from the orchestrator units. README updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqkQq3CDmFWcQ7u1LzoyRz
This commit is contained in:
2026-09-07 20:45:59 +00:00
co-authored by Claude Fable 5.1
parent e5b0177c93
commit 8f7f125fc0
4 changed files with 86 additions and 44 deletions
+32 -12
View File
@@ -1,7 +1,7 @@
# cc-ci — ONE Hetzner Cloud host running both the cc-ci CI server and the cc-ci orchestrator.
#
# This file is only what is physical or identity about the machine: hardware, networking, the
# tailscale node, root SSH keys, swap, stateVersion. Everything functional comes from modules:
# This file is only what is physical or identity about the machine: hardware, networking,
# root SSH keys, firewall + fail2ban, swap, stateVersion. Everything functional comes from modules:
# cc-ci.nixosModules.cc-ci-server recipe-maintainers/cc-ci — swarm, traefik, drone,
# runner, bridge, dashboard, reports, acme-dns, harness
# self.nixosModules.cc-ci-orchestrator nix/modules/cc-ci.nix — loops, orchestrator, timers
@@ -27,13 +27,13 @@
# ---- orchestrator identity --------------------------------------------------------------
# The CI server is this very host, so `ssh cc-ci` goes to loopback (the module default).
cc-ci-orchestrator.ciSshHost = "127.0.0.1";
# The opencode UI rides the CI server's Let's Encrypt wildcard (*.ci.commoninternet.net).
cc-ci-orchestrator.opencodeUiHost = "oc.ci.commoninternet.net";
cc-ci-orchestrator.opencodeUiAcmeHost = "ci.commoninternet.net";
# ---- tailscale — auth key staged out of band at /etc/ts-auth-key -----------------------
services.tailscale = {
enable = true;
authKeyFile = "/etc/ts-auth-key";
extraUpFlags = [ "--hostname=cc-ci" ];
};
# ---- 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,
# and the opencode UI on 8443 behind TLS + basic auth. fail2ban below guards the two logins.
# ---- ssh ----------------------------------------------------------------------------------
services.openssh = {
@@ -46,15 +46,35 @@
# The loops user can also be reached directly (same keys) — handy for rsync of its workspace.
users.users.loops.openssh.authorizedKeys.keys =
builtins.filter (s: s != "") (lib.splitString "\n" (builtins.readFile ./ssh-keys));
services.openssh.settings.PasswordAuthentication = false;
services.openssh.settings.KbdInteractiveAuthentication = false;
# ---- firewall -------------------------------------------------------------------------------
# 80/443 (traefik) and 53 (acme-dns) are opened by the cc-ci-server module. The tailscale
# interface is trusted, which is what makes the opencode UI on 8443 tailnet-only.
# 80/443 (traefik) and 53 (acme-dns) are opened by the cc-ci-server module. 8443 is the
# opencode UI (nginx, LE cert, basic auth — see orchestrator-host.nix).
networking.firewall = {
enable = true;
trustedInterfaces = [ "tailscale0" ];
allowedTCPPorts = [ 22 ];
allowedTCPPorts = [ 22 8443 ];
};
# ---- fail2ban: sshd (password auth is off, this stops the log noise and slow brute force) and
# the opencode UI's basic auth (nginx logs 401s to its error log; the built-in
# nginx-http-auth filter matches them).
services.fail2ban = {
enable = true;
maxretry = 5;
bantime = "1h";
bantime-increment = { enable = true; maxtime = "48h"; factor = "4"; };
ignoreIP = [ "127.0.0.0/8" "::1" ];
jails.nginx-http-auth.settings = {
enabled = true;
filter = "nginx-http-auth";
logpath = "/var/log/nginx/error.log";
backend = "auto";
port = "8443";
};
};
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
# ---- memory: 8 GB RAM shared by the swarm (recipe deploys) and 36 agent sessions ---------