opencode UI on 443 via traefik (Host routing), nginx basic auth on the docker bridge
Operator: standard 443, routed by domain. Traefik already owns 443 with the *.ci.commoninternet.net cert, so opencode-ui-route.service drops a dynamic config into the traefik stack's watched file-providers volume routing oc.ci.commoninternet.net -> http://172.18.0.1:8080, where nginx (reachable only on docker_gwbridge) enforces the basic auth and logs 401s with the real client IP (real_ip from X-Forwarded-For). The fail2ban nginx jail bans in DOCKER-USER, since that traffic is forwarded, not INPUT. 8443 and the ACME-host variant are gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqkQq3CDmFWcQ7u1LzoyRz
This commit is contained in:
@@ -29,12 +29,25 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
opencodeUiPort = lib.mkOption {
|
||||
opencodeUiBackendPort = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 8443;
|
||||
default = 8080;
|
||||
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 host decides whether to open it in the firewall.
|
||||
Plain-HTTP port nginx listens on for the opencode UI, reachable ONLY from the docker
|
||||
bridge (firewall rule on docker_gwbridge). Traefik — which owns the public 443 on the
|
||||
combined host — terminates TLS for opencodeUiHost and forwards here; nginx adds the basic
|
||||
auth and logs failures for fail2ban with the real client IP.
|
||||
'';
|
||||
};
|
||||
|
||||
opencodeUiTraefikProvidersDir = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/var/lib/docker/volumes/traefik_ci_commoninternet_net_file-providers/_data";
|
||||
description = ''
|
||||
Host path of the traefik stack's file-providers directory (watched by traefik). When set,
|
||||
a dynamic-config file routing opencodeUiHost on the `web-secure` entrypoint to nginx is
|
||||
written there after deploy-proxy. null = no traefik route (serve some other way).
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -44,17 +57,6 @@ in
|
||||
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.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
@@ -188,36 +190,61 @@ SSHCFG
|
||||
path = [ pkgs.bash pkgs.coreutils pkgs.git pkgs.python3 pkgs.openssh pkgs.tmux pkgs.nettools ];
|
||||
};
|
||||
|
||||
# ---- 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
|
||||
# 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 ];
|
||||
|
||||
# ---- nginx: basic auth for the opencode UI, behind traefik -----------------------------
|
||||
# Traefik (public 443, the CI wildcard cert) routes opencodeUiHost to this plain-HTTP vhost
|
||||
# on the docker bridge address. nginx enforces HTTP basic auth (the opencode web UI has no
|
||||
# auth of its own and can drive agent sessions), and — via real_ip from traefik's
|
||||
# X-Forwarded-For — logs the CLIENT address on a 401, which is what the fail2ban jail bans.
|
||||
# The htpasswd is created out of band (a store path would be world readable); nginx FAILS TO
|
||||
# START without it, and its config check runs as the nginx user:
|
||||
# /etc/nginx/oc-htpasswd root:nginx 0640 (`oc:<bcrypt>`; plaintext kept in /secrets)
|
||||
# Rotate with: printf 'oc:%s\n' "$(mkpasswd -m bcrypt "$P")" > /etc/nginx/oc-htpasswd && systemctl reload nginx
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
virtualHosts.${cfg.opencodeUiHost} = {
|
||||
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;
|
||||
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";
|
||||
listen = [ { addr = "0.0.0.0"; port = cfg.opencodeUiBackendPort; } ];
|
||||
basicAuthFile = "/etc/nginx/oc-htpasswd";
|
||||
extraConfig = ''
|
||||
# traefik sits on the docker networks (ingress 10.0.0.0/24, gwbridge 172.18.0.0/16)
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
real_ip_header X-Forwarded-For;
|
||||
'';
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:4096";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
# 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)";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "deploy-proxy.service" "docker.service" ];
|
||||
wants = [ "deploy-proxy.service" ];
|
||||
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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user