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:
@@ -17,7 +17,7 @@
|
||||
systemd.services.cc-ci-loops = {
|
||||
description = "cc-ci Builder/Adversary loops + watchdog (launch.sh start)";
|
||||
wantedBy = [ "multi-user.target" ]; # enabled after workspace staged (Hetzner cutover)
|
||||
after = [ "network-online.target" "tailscaled.service" "claude-install.service" ];
|
||||
after = [ "network-online.target" "claude-install.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
# KillMode=process: this unit only LAUNCHES the tmux server, it does not own it. With the
|
||||
@@ -58,7 +58,7 @@
|
||||
systemd.services.cc-ci-orchestrator = {
|
||||
description = "cc-ci orchestrator (operator steering session) — agents.py up orchestrator";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" "tailscaled.service" "opencode-web.service" ];
|
||||
after = [ "network-online.target" "opencode-web.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
# KillMode=process: see the note on cc-ci-loops — a rebuild that merely touches this unit
|
||||
@@ -85,7 +85,7 @@
|
||||
# wantedBy multi-user.target) so it never runs on boot/activation — only on the schedule.
|
||||
systemd.services.cc-ci-upgrade-all = {
|
||||
description = "cc-ci weekly /upgrade-all run (recipe upgrade survey + PRs, never merges)";
|
||||
after = [ "network-online.target" "tailscaled.service" "claude-install.service" ];
|
||||
after = [ "network-online.target" "claude-install.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot"; # launch-upgrader.py spawns the cc-ci-upgrader tmux session and returns
|
||||
@@ -129,7 +129,7 @@
|
||||
# short-lived glm-5.2 agent that diagnoses the blockage and drives the run to a clean DONE.
|
||||
systemd.services.cc-ci-upgrade-supervisor = {
|
||||
description = "cc-ci hourly weekly-run supervisor (glm-5.2 — drives a stalled /upgrade-all to completion)";
|
||||
after = [ "network-online.target" "tailscaled.service" ];
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot"; # launch-supervisor.py check: gate now, spawn the agent into tmux, return
|
||||
|
||||
@@ -34,15 +34,26 @@ in
|
||||
default = 8443;
|
||||
description = ''
|
||||
TLS port of the nginx front door for the opencode web UI. Not 443: on the combined host
|
||||
Traefik (docker swarm) owns 80/443. The port is not opened in the firewall, so it is
|
||||
reachable only over the trusted tailscale interface.
|
||||
Traefik (docker swarm) owns 80/443. The host decides whether to open it in the firewall.
|
||||
'';
|
||||
};
|
||||
|
||||
opencodeUiHost = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "oc.commoninternet.net";
|
||||
description = "nginx server_name for the opencode web UI (self-signed, basic auth).";
|
||||
description = "nginx server_name for the opencode web UI (TLS + basic auth).";
|
||||
};
|
||||
|
||||
opencodeUiAcmeHost = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "ci.commoninternet.net";
|
||||
description = ''
|
||||
Name of a `security.acme.certs` entry whose certificate covers opencodeUiHost (the
|
||||
combined host has the *.ci.commoninternet.net wildcard). When set, nginx serves that
|
||||
certificate; when null, the self-signed pair /etc/nginx/oc-selfsigned.{crt,key} staged
|
||||
out of band is used.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -158,7 +169,7 @@ SSHCFG
|
||||
systemd.services.opencode-web = {
|
||||
description = "opencode web server for cc-ci agents";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network-online.target" "tailscaled.service" "opencode-install.service" ];
|
||||
after = [ "network-online.target" "opencode-install.service" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
@@ -177,12 +188,20 @@ SSHCFG
|
||||
path = [ pkgs.bash pkgs.coreutils pkgs.git pkgs.python3 pkgs.openssh pkgs.tmux pkgs.nettools ];
|
||||
};
|
||||
|
||||
# ---- tailnet-only nginx front door for the opencode UI -------------------------------
|
||||
# Self-signed cert + basic auth, both created out of band (a store path would be world
|
||||
# readable) — see README "Secrets to stage". nginx FAILS TO START if they are missing.
|
||||
# /etc/nginx/oc-selfsigned.crt root:nginx 0644
|
||||
# ---- nginx front door for the opencode UI --------------------------------------------
|
||||
# TLS (LE via opencodeUiAcmeHost, else the self-signed pair below) + HTTP basic auth. The
|
||||
# opencode web UI has no auth of its own and can drive agent sessions, so the htpasswd is
|
||||
# mandatory. Files created out of band (a store path would be world readable) — nginx
|
||||
# FAILS TO START without them, and its config check runs as the nginx user:
|
||||
# /etc/nginx/oc-htpasswd root:nginx 0640 (`oc:<bcrypt>`; plaintext kept in /secrets)
|
||||
# /etc/nginx/oc-selfsigned.crt root:nginx 0644 (only when opencodeUiAcmeHost is null)
|
||||
# /etc/nginx/oc-selfsigned.key root:nginx 0640
|
||||
# /etc/nginx/oc-htpasswd root:nginx 0640 (`oc:<bcrypt>`; plaintext in /secrets)
|
||||
# Rotate the password with:
|
||||
# printf 'oc:%s\n' "$(mkpasswd -m bcrypt "$P")" > /etc/nginx/oc-htpasswd && systemctl reload nginx
|
||||
# nginx must be able to read the ACME-issued key (the acme group owns it).
|
||||
users.users.nginx.extraGroups = lib.mkIf (cfg.opencodeUiAcmeHost != null)
|
||||
[ config.security.acme.certs.${cfg.opencodeUiAcmeHost}.group ];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
@@ -190,8 +209,9 @@ SSHCFG
|
||||
listen = [ { addr = "0.0.0.0"; port = cfg.opencodeUiPort; ssl = true; } ];
|
||||
# onlySSL flags the vhost as SSL so the module renders ssl_certificate for the listener.
|
||||
onlySSL = true;
|
||||
sslCertificate = "/etc/nginx/oc-selfsigned.crt";
|
||||
sslCertificateKey = "/etc/nginx/oc-selfsigned.key";
|
||||
useACMEHost = cfg.opencodeUiAcmeHost;
|
||||
sslCertificate = lib.mkIf (cfg.opencodeUiAcmeHost == null) "/etc/nginx/oc-selfsigned.crt";
|
||||
sslCertificateKey = lib.mkIf (cfg.opencodeUiAcmeHost == null) "/etc/nginx/oc-selfsigned.key";
|
||||
basicAuthFile = "/etc/nginx/oc-htpasswd";
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:4096";
|
||||
|
||||
Reference in New Issue
Block a user