c33457fput an address check (sovran_lan_only) on the Hub, RTL and Mempool sites. It was written for ports 80/443 being forwarded and a Host header selecting a site, and that only ever applied to the Hub. RTL and Mempool are sites on ports of their own (:3051, :60847): a request on 80/443 cannot select them, whatever Host it carries. Checked with Caddy 2.9.1 and the Caddyfile this module generates for Server + Desktop with every domain configured, serving the public sites on a stand-in port: Host: sovransystemsos.local, localhost:8937, 127.0.0.1 and x:3051 all get an empty 200, and Host: matrix.example.org gets the Synapse stand-in. With the Hub off Caddy the guard has nothing left to guard, and it could not be made right for the two sites that remain: - IPv6. A laptop's global address on the LAN looks exactly like a stranger's. The choice was between letting all of 2000::/3 through, which is the whole IPv6 internet and is whatc33457fdoes, and refusing every LAN device that connects over a global address unless the operator copies the ISP's prefix into a Nix option. - They do not need it. RTL has a random 20-character password (pwgen -s 20, about 119 bits) and, since 0.15.12, which Sovran_Bitcoin pins, a 30-minute lockout keyed on the client address. Mempool shows public chain data. If someone forwards 3051 or 60847 that is the same exposure as any other port on the machine, and SECURITY.md says not to. Remove the snippet and its two imports, and tests/test_caddy_lan_only.py with them. What is still worth pinning moves to test_hub_direct.py: no address filter anywhere in caddy.nix, the two sites are plain proxies to their loopback ports, and no option for a declared prefix is left half-wired. Behaviour change: RTL and Mempool answer any client that can reach :3051 or :60847, as they did beforec33457f. In practice that is the local network, because nothing asks you to forward those ports. Checked with the real generator and Caddy 2.9.1: Node Only generates `auto_https off` and the two plain sites and validates. Run live next to the real Hub, a LAN client gets the Hub, RTL and Mempool; a stranger's address gets RTL and Mempool (by design) and a 403 from the Hub; port 80 is not listening on Node Only.
259 lines
8.0 KiB
Nix
Executable File
259 lines
8.0 KiB
Nix
Executable File
{ config, pkgs, lib, ... }:
|
|
|
|
let
|
|
exposeBtcpay = config.sovran_systemsOS.web.btcpayserver;
|
|
extraVhosts = config.sovran_systemsOS.caddy.extraVirtualHosts;
|
|
|
|
# True when any service needs HTTPS/ACME (domain-based vhosts)
|
|
needsHttpsPorts =
|
|
config.sovran_systemsOS.web.btcpayserver
|
|
|| config.sovran_systemsOS.services.synapse
|
|
|| config.sovran_systemsOS.services.wordpress
|
|
|| config.sovran_systemsOS.services.nextcloud
|
|
|| config.sovran_systemsOS.services.vaultwarden
|
|
|| config.sovran_systemsOS.features.haven
|
|
|| config.sovran_systemsOS.features."nwc-wallets"
|
|
|| config.sovran_systemsOS.features.element-calling;
|
|
|
|
# RTL and Mempool listen on loopback only: Sovran_Bitcoin binds them to
|
|
# 127.0.0.1, and RTL's unit is sandboxed to loopback besides. Caddy is how
|
|
# the local network reaches them (:3051 and :60847), so it has to run
|
|
# wherever they do. That includes Bitcoin Node Only, which has no
|
|
# domain-based service and so no other reason to run Caddy.
|
|
#
|
|
# The Hub is not one of these. It listens on 0.0.0.0:8937 itself, so it is
|
|
# served on its own port rather than through Caddy: the one service that
|
|
# runs as root has nothing in front of it that it does not need, and the
|
|
# public sites on ports 80/443 cannot be asked for it by Host header.
|
|
servesRtl = config.sovran_systemsOS.services.bitcoin;
|
|
servesMempool = servesRtl && config.sovran_systemsOS.features.mempool;
|
|
|
|
caddyEnabled = needsHttpsPorts || extraVhosts != "" || servesRtl;
|
|
|
|
# Sites for the local network, one per loopback-only service. Written after
|
|
# the public domain sites; each exists only where its service does.
|
|
#
|
|
# They do not filter by client address. A request can only reach them on
|
|
# their own ports, which no setup step asks you to forward, so forwarding
|
|
# 80/443 for public services does not expose them (a Host header on those
|
|
# ports cannot select a site that listens elsewhere). RTL has its own
|
|
# password and lockout, and Mempool shows public chain data. An address
|
|
# check here could not be made right for IPv6 anyway: a laptop's global
|
|
# address on the LAN looks exactly like a stranger's.
|
|
bitcoinUiSites =
|
|
lib.optionalString servesRtl ''
|
|
|
|
:3051 {
|
|
reverse_proxy :3050
|
|
encode gzip zstd
|
|
}
|
|
''
|
|
+ lib.optionalString servesMempool ''
|
|
|
|
:60847 {
|
|
reverse_proxy :60845
|
|
encode gzip zstd
|
|
}
|
|
'';
|
|
in
|
|
{
|
|
services.caddy = {
|
|
# Caddy runs when a domain-based service needs it, when the operator has
|
|
# defined custom vhosts, or when it is the way to reach RTL and Mempool.
|
|
# Desktop Only has none of those, so Caddy stays off there.
|
|
enable = caddyEnabled;
|
|
user = "caddy";
|
|
group = "root";
|
|
};
|
|
|
|
# Only open ports 80/443 when at least one domain-based service is active
|
|
networking.firewall.allowedTCPPorts = lib.mkIf needsHttpsPorts [ 80 443 ];
|
|
networking.firewall.allowedUDPPorts = lib.mkIf needsHttpsPorts [ 80 443 ];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/domains 0755 caddy root -"
|
|
];
|
|
|
|
# Override ExecStart + ExecReload to point at the runtime-generated Caddyfile
|
|
systemd.services.caddy.serviceConfig = {
|
|
ExecStart = lib.mkForce [
|
|
""
|
|
"${pkgs.caddy}/bin/caddy run --config /run/caddy/Caddyfile --adapter caddyfile"
|
|
];
|
|
ExecReload = lib.mkForce [
|
|
""
|
|
"${pkgs.caddy}/bin/caddy reload --config /run/caddy/Caddyfile --adapter caddyfile --force"
|
|
];
|
|
};
|
|
|
|
systemd.services.caddy-generate-config = {
|
|
description = "Generate Caddyfile from /var/lib/domains at runtime";
|
|
before = [ "caddy.service" ];
|
|
requiredBy = [ "caddy.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
RuntimeDirectory = "caddy";
|
|
};
|
|
path = [ pkgs.coreutils ];
|
|
script = ''
|
|
read_domain() {
|
|
if [ -f "/var/lib/domains/$1" ]; then
|
|
cat "/var/lib/domains/$1"
|
|
else
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
MATRIX=$(read_domain matrix)
|
|
WORDPRESS=$(read_domain wordpress)
|
|
NEXTCLOUD=$(read_domain nextcloud)
|
|
BTCPAY=$(read_domain btcpayserver)
|
|
VAULTWARDEN=$(read_domain vaultwarden)
|
|
HAVEN=$(read_domain haven)
|
|
LIGHTNING=$(read_domain lightning)
|
|
ACME_EMAIL=$(read_domain sslemail)
|
|
|
|
# Start with global config — use ACME only when domain-based services are active
|
|
${if needsHttpsPorts then ''
|
|
cat > /run/caddy/Caddyfile <<EOF
|
|
{
|
|
email $ACME_EMAIL
|
|
}
|
|
EOF
|
|
'' else ''
|
|
cat > /run/caddy/Caddyfile <<EOF
|
|
{
|
|
auto_https off
|
|
}
|
|
EOF
|
|
''}
|
|
|
|
# ── Matrix ──────────────────────────────────────
|
|
if [ -n "$MATRIX" ]; then
|
|
if [ -f /run/caddy/element-calling.snippet ]; then
|
|
cat /run/caddy/element-calling.snippet >> /run/caddy/Caddyfile
|
|
else
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$MATRIX {
|
|
reverse_proxy /_matrix/* http://localhost:8008
|
|
reverse_proxy /_synapse/client/* http://localhost:8008
|
|
handle /.well-known/matrix/server {
|
|
header Content-Type application/json
|
|
respond \`{"m.server":"$MATRIX:443"}\` 200
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
fi
|
|
|
|
# ── WordPress ───────────────────────────────────
|
|
if [ -n "$WORDPRESS" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$WORDPRESS {
|
|
encode gzip zstd
|
|
root * /var/lib/www/wordpress
|
|
php_fastcgi unix//run/phpfpm/wordpress.sock
|
|
file_server browse
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── Nextcloud ───────────────────────────────────
|
|
if [ -n "$NEXTCLOUD" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$NEXTCLOUD {
|
|
encode gzip zstd
|
|
root * /var/lib/www/nextcloud
|
|
php_fastcgi unix//run/phpfpm/nextcloud.sock {
|
|
trusted_proxies private_ranges
|
|
}
|
|
file_server
|
|
redir /.well-known/carddav /remote.php/dav/ 301
|
|
redir /.well-known/caldav /remote.php/dav/ 301
|
|
header {
|
|
Strict-Transport-Security max-age=31536000;
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── BTCPay (only if web exposure is enabled) ────
|
|
${if exposeBtcpay then ''
|
|
if [ -n "$BTCPAY" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$BTCPAY {
|
|
reverse_proxy http://localhost:23000
|
|
encode gzip zstd
|
|
}
|
|
EOF
|
|
fi
|
|
'' else ''
|
|
# BTCPay web exposure disabled by sovran_systemsOS.web.btcpayserver = false
|
|
''}
|
|
|
|
# ── Vaultwarden ─────────────────────────────────
|
|
if [ -n "$VAULTWARDEN" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$VAULTWARDEN {
|
|
reverse_proxy http://localhost:8777
|
|
encode gzip zstd
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── Haven ───────────────────────────────────────
|
|
if [ -n "$HAVEN" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$HAVEN {
|
|
reverse_proxy localhost:3355 {
|
|
header_up Host {host}
|
|
header_up X-Real-IP {remote_host}
|
|
header_up X-Forwarded-For {remote_host}
|
|
header_up X-Forwarded-Proto {scheme}
|
|
transport http {
|
|
versions 1.1
|
|
}
|
|
}
|
|
request_body {
|
|
max_size 100MB
|
|
}
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── Lightning Wallet Connections LNURL ──────────────────────
|
|
if [ -n "$LIGHTNING" ]; then
|
|
cat >> /run/caddy/Caddyfile <<EOF
|
|
|
|
$LIGHTNING {
|
|
# LNURL discovery and callback are served by the dedicated
|
|
# nwc-lnurl service on loopback port 8181. Only these paths
|
|
# are proxied; the Alby Hub management port (18080) is never exposed.
|
|
reverse_proxy /.well-known/lnurlp/* http://127.0.0.1:8181
|
|
reverse_proxy /lnurlp/* http://127.0.0.1:8181
|
|
}
|
|
EOF
|
|
fi
|
|
|
|
# ── RTL and Mempool (local network) ─────────────
|
|
# Only where those services run; see bitcoinUiSites above.
|
|
cat >> /run/caddy/Caddyfile <<'LAN_SITES_EOF'
|
|
${bitcoinUiSites}
|
|
LAN_SITES_EOF
|
|
|
|
# ── Custom vhosts from custom.nix ──────────────
|
|
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
|
|
${extraVhosts}
|
|
CUSTOM_VHOSTS_EOF
|
|
'';
|
|
};
|
|
}
|