diff options
| -rw-r--r-- | assets/vpn.nix | 33 | ||||
| -rw-r--r-- | machines/taupe/default.nix | 5 | ||||
| -rw-r--r-- | machines/taureau/default.nix | 5 | ||||
| -rw-r--r-- | machines/truite/default.nix | 5 | ||||
| -rw-r--r-- | modules/foundation/default.nix | 1 | ||||
| -rw-r--r-- | modules/foundation/vpn.nix | 90 | ||||
| -rw-r--r-- | modules/tunnel/definition.nix | 31 | ||||
| -rw-r--r-- | modules/tunnel/ingress.nix | 6 | ||||
| -rw-r--r-- | secrets/secrets.nix | 8 | ||||
| -rw-r--r-- | secrets/wg-private-key.age (renamed from secrets/tunnel/ingress-key.age) | 0 |
10 files changed, 148 insertions, 36 deletions
diff --git a/assets/vpn.nix b/assets/vpn.nix new file mode 100644 index 0000000..e30438b --- /dev/null +++ b/assets/vpn.nix @@ -0,0 +1,33 @@ +# shared vpn definitions for wireguard, used by both the tunnel +# and the plain vpn module. + +{ + # the public key of the shared wireguard key used by all vpn hosts. + # the matching private key is the secret `wg-private-key`. + public = "s5yyPCJiN0uqW0jzKIbYCF7I9TthymiRzpNt466XeWk="; + + # users allowed to connect to any wireguard-based service. + # their ip is always a template, with 'X' representing the + # network subnet they are connecting to. + users = { + mel = { + key = "vnZoHXapCLLUhZ8A8R5W0iJ8LpWVLve29z41kkoT0BU="; + ip = "10.123.X.101"; + }; + + andrei = { + key = "qqU4uYImLfUohIwl4KBshPtTINFcs0JVALjbmwpfxRg="; + ip = "10.123.X.102"; + }; + + sergo = { + key = "qbZGMNIDZFCJC6SHtlyNIlIdGWHELceXClJCcagrj2Y="; + ip = "10.123.X.103"; + }; + + fedor = { + key = "tEO9r8+jTpu8TBRmZ+/v087IgD/QfmofLUKs249i/F0="; + ip = "10.123.X.104"; + }; + }; +} diff --git a/machines/taupe/default.nix b/machines/taupe/default.nix index 51d84b4..1c69254 100644 --- a/machines/taupe/default.nix +++ b/machines/taupe/default.nix @@ -22,6 +22,11 @@ }; monitoring.client.enable = false; + + vpn = { + enable = true; + externalInterface = "enp1s0"; + }; }; system.stateVersion = "25.05"; diff --git a/machines/taureau/default.nix b/machines/taureau/default.nix index d9d5636..ef234c5 100644 --- a/machines/taureau/default.nix +++ b/machines/taureau/default.nix @@ -22,6 +22,11 @@ }; monitoring.client.enable = false; + + vpn = { + enable = true; + externalInterface = "enp6s16"; + }; }; system.stateVersion = "25.05"; diff --git a/machines/truite/default.nix b/machines/truite/default.nix index 6b08235..4cfec25 100644 --- a/machines/truite/default.nix +++ b/machines/truite/default.nix @@ -27,6 +27,11 @@ "tailnet" ]; }; + + vpn = { + enable = true; + externalInterface = "ens18"; + }; }; system.stateVersion = "25.11"; diff --git a/modules/foundation/default.nix b/modules/foundation/default.nix index 3905eb8..fa84750 100644 --- a/modules/foundation/default.nix +++ b/modules/foundation/default.nix @@ -6,5 +6,6 @@ ./services ./monitoring ./www + ./vpn.nix ]; } diff --git a/modules/foundation/vpn.nix b/modules/foundation/vpn.nix new file mode 100644 index 0000000..1a7524d --- /dev/null +++ b/modules/foundation/vpn.nix @@ -0,0 +1,90 @@ +{ + config, + lib, + ... +}: + +let + inherit (lib) + mkEnableOption + mkOption + types + mkIf + attrValues + replaceString + ; + + cfg = config.foundation.vpn; + + inherit (import ../../assets/vpn.nix) users; + + interface = "vpn0"; + port = 51820; + + # while the tunnel uses the lower subnets for the paths, + # the vpn always uses subnet number 10. + subnetIndex = 10; + + addressFromTemplate = + template: prefix: "${replaceString "X" (toString subnetIndex) template}/${toString prefix}"; +in +{ + options.foundation.vpn = { + enable = mkEnableOption "WireGuard VPN server"; + + externalInterface = mkOption { + type = types.str; + description = "External network interface"; + }; + }; + + config = mkIf cfg.enable { + age.secrets.wg-private-key = { + file = ../../secrets/wg-private-key.age; + owner = "systemd-network"; + }; + + networking.firewall.allowedUDPPorts = [ port ]; + + systemd.network = { + netdevs."30-${interface}" = { + netdevConfig = { + Kind = "wireguard"; + Name = interface; + }; + wireguardConfig = { + PrivateKeyFile = config.age.secrets.wg-private-key.path; + ListenPort = port; + }; + wireguardPeers = map (user: { + PublicKey = user.key; + AllowedIPs = [ (addressFromTemplate user.ip 32) ]; + }) (attrValues users); + }; + + networks."30-${interface}" = { + name = interface; + address = [ (addressFromTemplate "10.123.X.1" 24) ]; + linkConfig = { + RequiredForOnline = "no"; + }; + }; + }; + + networking.nftables.tables.vpn-nat = { + family = "ip"; + content = '' + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + iifname "${interface}" oifname "${cfg.externalInterface}" masquerade + } + + chain forward { + type filter hook forward priority 0; policy accept; + iifname "${interface}" oifname "${cfg.externalInterface}" accept + iifname "${cfg.externalInterface}" oifname "${interface}" ct state established,related accept + } + ''; + }; + }; +} diff --git a/modules/tunnel/definition.nix b/modules/tunnel/definition.nix index 74ae268..82534bf 100644 --- a/modules/tunnel/definition.nix +++ b/modules/tunnel/definition.nix @@ -36,40 +36,11 @@ } ]; - # there are our users who are allowed to connect to any of our "paths". - # their ip is always a template, with 'X' representing the path index. - users = { - mel = { - key = "vnZoHXapCLLUhZ8A8R5W0iJ8LpWVLve29z41kkoT0BU="; - ip = "10.123.X.101"; - }; - - andrei = { - key = "qqU4uYImLfUohIwl4KBshPtTINFcs0JVALjbmwpfxRg="; - ip = "10.123.X.102"; - }; - - sergo = { - key = "qbZGMNIDZFCJC6SHtlyNIlIdGWHELceXClJCcagrj2Y="; - ip = "10.123.X.103"; - }; - - fedor = { - key = "tEO9r8+jTpu8TBRmZ+/v087IgD/QfmofLUKs249i/F0="; - ip = "10.123.X.104"; - }; - }; + inherit (import ../../assets/vpn.nix) users; # we use a website as a "mask" for vless/reality, which will tell our peers # to pretend as if they're a user and a well-known website communicating with # each other, even though they know that the keys don't actually match up, # it's not possible to see that on the outside. mask = "microsoft.com"; - - # we don't actually need this to configure the tunnel, but this is - # the public key of the ingress interface. - # when creating wireguard vpn configurations for the users, this - # is the public key of the server peer at `tunnel.rnrd.eu`. - # the matching private key of the pair is the secret `tunnel/ingress-key`. - ingress.public = "s5yyPCJiN0uqW0jzKIbYCF7I9TthymiRzpNt466XeWk="; } diff --git a/modules/tunnel/ingress.nix b/modules/tunnel/ingress.nix index 1ea1613..f609f3d 100644 --- a/modules/tunnel/ingress.nix +++ b/modules/tunnel/ingress.nix @@ -38,8 +38,8 @@ in checkReversePath = "loose"; }; - age.secrets.ingress-key = { - file = ../../secrets/tunnel/ingress-key.age; + age.secrets.wg-private-key = { + file = ../../secrets/wg-private-key.age; owner = "systemd-network"; }; @@ -52,7 +52,7 @@ in Name = ingressName index; }; wireguardConfig = { - PrivateKeyFile = config.age.secrets.ingress-key.path; + PrivateKeyFile = config.age.secrets.wg-private-key.path; ListenPort = path.port; }; wireguardPeers = map (user: { diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 7332beb..c56e2bd 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -42,15 +42,17 @@ in fourmi ] ++ allAdmins; - "vpn/ingress-key.age".publicKeys = [ + "wg-private-key.age".publicKeys = [ truite + taureau + taupe ] ++ allAdmins; - "vpn/egress-key-taureau.age".publicKeys = [ + "tunnel/egress-key-taureau.age".publicKeys = [ taureau ] ++ allAdmins; - "vpn/egress-key-taupe.age".publicKeys = [ + "tunnel/egress-key-taupe.age".publicKeys = [ taupe ] ++ allAdmins; } diff --git a/secrets/tunnel/ingress-key.age b/secrets/wg-private-key.age index 2e83ec2..2e83ec2 100644 --- a/secrets/tunnel/ingress-key.age +++ b/secrets/wg-private-key.age |
