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:
@@ -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