The reported bug was the Hub being reachable from outside the local
network when Server + Desktop is active. c33457f guards the Caddy site
for sovransystemsos.local, but the Hub is an application that also
listens on a port of its own (8937), and a check in Caddy does nothing
for a client that never goes through Caddy. Whether a client could reach
the Hub depended on which door it used.
The Hub now asks the question itself. LanOnlyMiddleware is registered
outermost, so a client that is not on this computer or the local network
gets a bare 403 before authentication is considered; it never sees the
login page.
- Local means loopback, 10/8, 172.16/12, 192.168/16, 100.64/10
(Tailscale and other CGNAT/VPN ranges) and 169.254/16 over IPv4, and
::1, fc00::/7 and fe80::/10 over IPv6.
- IPv6 global addresses are not on the list. A global address belonging
to a laptop on the LAN cannot be told apart from a stranger's by the
address alone, and 2000::/3 is every public IPv6 address there is.
- ::ffff:a.b.c.d is read as the IPv4 address inside it.
- sovran_systemsOS.hub.extraLanNetworks adds networks (IPv4 or IPv6 CIDR)
for setups whose own devices use addresses outside those ranges. It is
checked at build time. The app ignores an entry it cannot parse and
refuses 0.0.0.0/0 and ::/0: it must never widen its policy by
guessing, and "everyone" is hub.lanOnly = false, asked for by name.
- sovran_systemsOS.hub.lanOnly (default true) turns the check off.
- The first refusal from each address is logged, naming the option to
change, so an operator whose own device is refused can find out why.
The list is capped so a scanner cannot fill the journal or memory.
Behind Caddy the policy applies to the real client, not to Caddy: uvicorn
takes the address from X-Forwarded-For only when the peer is 127.0.0.1.
Checked, not only reviewed:
- The real app with the config.json the module really generates (nix
eval on the nixpkgs revision flake.lock pins, read back from the
derivation), on a real socket with the source address chosen per
request: 127.0.0.1, 192.168/16, 100.64/10 and a declared extra
network are served; 203.0.113.9, 8.8.4.4 and an address just outside
the declared /28 get 403 on /login, / and /api/ping. /auto-login still
answers 303 to loopback and 403 to everyone else.
- With c33457f's Caddy guard in front, an IPv6 client at 2001:db8::9
passes Caddy (it is inside 2000::/3) and is refused by the Hub; an
IPv4 stranger has the connection closed by Caddy; a LAN client is
served.
- hub.extraLanNetworks accepts 203.0.113.0/28, 2001:db8:abcd::/48 and
bare hosts, and fails the build for /33, 300.1.1.1/8, 0.0.0.0/0, ::/0,
2001:db8::/129 and junk, with a message that says what to write.
Add tests/test_lan_policy.py and tests/test_hub_lan_only.py, and a note
in SECURITY.md.
181 lines
7.2 KiB
Nix
Executable File
181 lines
7.2 KiB
Nix
Executable File
{ config, lib, ... }:
|
|
|
|
{
|
|
options.sovran_systemsOS = {
|
|
roles = {
|
|
server_plus_desktop = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = !config.sovran_systemsOS.roles.desktop && !config.sovran_systemsOS.roles.node;
|
|
};
|
|
desktop = lib.mkEnableOption "Desktop Role";
|
|
node = lib.mkEnableOption "Bitcoin Node Only Role";
|
|
};
|
|
|
|
# ── Services (default ON — user can disable in custom.nix) ──
|
|
services = {
|
|
synapse = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Matrix Synapse homeserver";
|
|
};
|
|
bitcoin = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Bitcoin Ecosystem (bitcoind, electrs, lnd, rtl, btcpay)";
|
|
};
|
|
vaultwarden = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Vaultwarden password manager";
|
|
};
|
|
wordpress = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "WordPress (raw PHP served by Caddy)";
|
|
};
|
|
nextcloud = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Nextcloud (raw PHP served by Caddy)";
|
|
};
|
|
};
|
|
|
|
# ── Features (default OFF — user can enable in custom.nix) ──
|
|
features = {
|
|
haven = lib.mkEnableOption "Haven NOSTR relay";
|
|
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
|
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
|
bitcoin-tor-gossip = lib.mkEnableOption "Advertise the Bitcoin Core onion service through Bitcoin peer gossip";
|
|
# Compatibility shim for Hub-managed settings from releases where Core
|
|
# was an optional replacement for the default node. Core is now always
|
|
# selected when the Bitcoin service is enabled.
|
|
bitcoin-core = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.bool;
|
|
default = null;
|
|
internal = true;
|
|
visible = false;
|
|
description = "Deprecated no-op: Bitcoin Core is the default node implementation.";
|
|
};
|
|
"nwc-wallets" = lib.mkEnableOption "Lightning Wallet Connections";
|
|
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
|
sshd = lib.mkEnableOption "SSH remote access";
|
|
};
|
|
|
|
# ── Hub ───────────────────────────────────────────────────
|
|
hub = {
|
|
lanOnly = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Refuse Hub requests from clients that are not on this computer or on
|
|
the local network: loopback, private (10.0.0.0/8, 172.16.0.0/12,
|
|
192.168.0.0/16), VPN/CGNAT (100.64.0.0/10) and link-local addresses,
|
|
plus anything listed in sovran_systemsOS.hub.extraLanNetworks.
|
|
|
|
The Hub runs as root and can display stored credentials and reboot
|
|
the machine. Whether a packet may reach its port is up to the
|
|
firewall and your router; this check is the second lock, so that a
|
|
port forward or a firewall mistake does not put the Hub's login page
|
|
in front of the internet.
|
|
|
|
Set it to false only if this computer sits on a network that hands
|
|
out public addresses to your own devices and you would rather not
|
|
list them.
|
|
'';
|
|
};
|
|
|
|
extraLanNetworks = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [ "203.0.113.0/28" ];
|
|
description = ''
|
|
Extra networks, in CIDR notation, that the Hub should treat as local
|
|
in addition to the built-in ranges. Needed only if devices on your
|
|
local network use addresses outside the private ranges, for example a
|
|
public IPv4 block your provider routes onto your LAN.
|
|
|
|
Keep each entry as narrow as you can: every address inside it is let
|
|
through. To let everything through, set sovran_systemsOS.hub.lanOnly
|
|
to false instead; 0.0.0.0/0 and ::/0 are not accepted here.
|
|
'';
|
|
};
|
|
};
|
|
|
|
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
|
web = {
|
|
btcpayserver = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Expose BTCPay Server via Caddy";
|
|
};
|
|
};
|
|
|
|
# ── Caddy customisation ───────────────────────────────────
|
|
caddy = {
|
|
extraVirtualHosts = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
description = "Additional raw Caddyfile blocks appended to the generated Caddy config. Use this in custom.nix to add custom domains and reverse proxies.";
|
|
};
|
|
};
|
|
|
|
# ── Element Calling (video/audio) tuning ──────────────────
|
|
elementCalling = {
|
|
fullAccessHomeservers = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [ "matrix.peer.example.com" ];
|
|
description = ''
|
|
Additional Matrix server_names (beyond this server itself) that may
|
|
trigger LiveKit room creation on this server's SFU via lk-jwt-service.
|
|
|
|
Not needed for the common federated setup: each participant's client
|
|
always obtains its token from its own homeserver's JWT service and
|
|
publishes to its own SFU, and the participant who starts a call
|
|
creates the room on their own SFU — the remote user merely joins
|
|
(joining does not require full access).
|
|
|
|
Only set this for asymmetric cases: e.g. a peer homeserver that has
|
|
no focus of its own, or calls whose first participant lands on this
|
|
server's SFU but belongs to the peer.
|
|
'';
|
|
};
|
|
externalIP = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "203.0.113.10";
|
|
description = ''
|
|
Optional pin: force LiveKit to advertise this public IPv4 in its
|
|
host/TURN ICE candidates. Not required in normal operation — the
|
|
address is the one Njal.la reports for the DDNS update (set up in
|
|
the Hub's Domains page), and nothing on this system looks it up
|
|
anywhere else. Set it for a fixed public address with no Njal.la
|
|
DDNS entry, or to override the reported one (e.g. multi-WAN/VPN
|
|
setups).
|
|
'';
|
|
};
|
|
};
|
|
|
|
# ── Domain setup registry ─────────────────────────────────
|
|
domainRequirements = lib.mkOption {
|
|
type = lib.types.listOf (lib.types.submodule {
|
|
options = {
|
|
name = lib.mkOption { type = lib.types.str; };
|
|
label = lib.mkOption { type = lib.types.str; };
|
|
example = lib.mkOption { type = lib.types.str; };
|
|
needsDDNS = lib.mkOption { type = lib.types.bool; default = true; };
|
|
};
|
|
});
|
|
default = [];
|
|
description = "Domain requirements registered by each module";
|
|
};
|
|
|
|
nostr_npub = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Nostr public key (npub1...) for Haven relay";
|
|
};
|
|
};
|
|
|
|
}
|