From a4ab740d07706d86503b84d36addcbbbf7a4944b Mon Sep 17 00:00:00 2001 From: Mel Date: Fri, 1 May 2026 18:40:37 +0200 Subject: Re-add simple VPN and simplify module with shared users with tunnel Signed-off-by: Mel --- assets/vpn.nix | 33 ++++++++++++++++ machines/taupe/default.nix | 5 +++ machines/taureau/default.nix | 5 +++ machines/truite/default.nix | 5 +++ modules/foundation/default.nix | 1 + modules/foundation/vpn.nix | 90 ++++++++++++++++++++++++++++++++++++++++++ modules/tunnel/definition.nix | 31 +-------------- modules/tunnel/ingress.nix | 6 +-- secrets/secrets.nix | 8 ++-- secrets/tunnel/ingress-key.age | 21 ---------- secrets/wg-private-key.age | 21 ++++++++++ 11 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 assets/vpn.nix create mode 100644 modules/foundation/vpn.nix delete mode 100644 secrets/tunnel/ingress-key.age create mode 100644 secrets/wg-private-key.age 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/tunnel/ingress-key.age deleted file mode 100644 index 2e83ec2..0000000 --- a/secrets/tunnel/ingress-key.age +++ /dev/null @@ -1,21 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 xiGqLQ p1N3PDNDTnCAkqZhUMOr9urq1VMWTEGxjFHwiDdcvmE -5NYgD3rhnkiHD26WJ9wp4GmpBjk0zr2QlPUK/uL5Mnw --> ssh-ed25519 ztr2Fw AQfk8kKe/3C4ukzkir+pbZrkmnefZY6NB21D0aqQbn0 -j6Lnoon4j9EWtCB1wTziaul3tfKAIX7QmhmPodC6QXg --> ssh-ed25519 lYrVNQ VahYJ6WbeVgKSz/FTrhVlS1pXZpNn97L9ErHaD9onAk -19sDuT8hIQmhcP8FuF03G5wH6CJYUfE181gXzPWAzrM --> ssh-ed25519 COVM9Q aLRtcxGWCoohk/yDdwOIuT67CTVUnip/KeMsgJa3RGo -Bhpi0biJRONWe/c3g5xb0vky+aRcpDGsw/6S4epaE4A --> ssh-ed25519 nsdjwQ ZEI8BxBnTZLWDflnJNUGajWhZsEzcf76mcJ5WnYmBQ0 -3paoPQPbApCxN4/4lg+BCPmfTjzSZ74tbDdPskfvaCc --> ssh-ed25519 aV3pTQ prqqDR4/NFtVW14bqOYQWo/TWA9o/53oWi9wV1cKqE8 -9OjIrwI2MV1QIogbzahvYaSMB/oB5O5obAyCAiyAoAQ --> ssh-ed25519 jWuO5g hT2kzw8vCmMYK4FUnNn+NUBeRoGHTSaw14w8gb3ET2I -eXbhCji05+0KAB74+r6ZszIy/iOYCOLsmjF9Q9XiNjQ --> ssh-ed25519 77YXTQ FiTDgl4Sc1gQya5+wzeVc16QTigKTHYwVOgK5sdVW1I -69qWGBuDiAXOM3413frPrccPXJOxBq9TZnpiCcMFbZw --> ssh-ed25519 5yNkVQ sHxSA0EI58e5bNiREh5si92aDyti0wBrtavfTkth8ik -Dg5+vGoFwLyG3Oxl6yBV6QVWQxoJl8ieuRcHAnaHZ8o ---- keoCBDjcgb4VUtjzo/YD++ZS7aVYj/iyOdgrd5+2LHE -+ø”1péÃ[ÈzÞ'yäS<|Oá”–";@ké;y£í¦³­zfÊK¿Y{À<Ò7O5XÐh%âEº‡RÆRÊ‚kžÝ$ \ No newline at end of file diff --git a/secrets/wg-private-key.age b/secrets/wg-private-key.age new file mode 100644 index 0000000..2e83ec2 --- /dev/null +++ b/secrets/wg-private-key.age @@ -0,0 +1,21 @@ +age-encryption.org/v1 +-> ssh-ed25519 xiGqLQ p1N3PDNDTnCAkqZhUMOr9urq1VMWTEGxjFHwiDdcvmE +5NYgD3rhnkiHD26WJ9wp4GmpBjk0zr2QlPUK/uL5Mnw +-> ssh-ed25519 ztr2Fw AQfk8kKe/3C4ukzkir+pbZrkmnefZY6NB21D0aqQbn0 +j6Lnoon4j9EWtCB1wTziaul3tfKAIX7QmhmPodC6QXg +-> ssh-ed25519 lYrVNQ VahYJ6WbeVgKSz/FTrhVlS1pXZpNn97L9ErHaD9onAk +19sDuT8hIQmhcP8FuF03G5wH6CJYUfE181gXzPWAzrM +-> ssh-ed25519 COVM9Q aLRtcxGWCoohk/yDdwOIuT67CTVUnip/KeMsgJa3RGo +Bhpi0biJRONWe/c3g5xb0vky+aRcpDGsw/6S4epaE4A +-> ssh-ed25519 nsdjwQ ZEI8BxBnTZLWDflnJNUGajWhZsEzcf76mcJ5WnYmBQ0 +3paoPQPbApCxN4/4lg+BCPmfTjzSZ74tbDdPskfvaCc +-> ssh-ed25519 aV3pTQ prqqDR4/NFtVW14bqOYQWo/TWA9o/53oWi9wV1cKqE8 +9OjIrwI2MV1QIogbzahvYaSMB/oB5O5obAyCAiyAoAQ +-> ssh-ed25519 jWuO5g hT2kzw8vCmMYK4FUnNn+NUBeRoGHTSaw14w8gb3ET2I +eXbhCji05+0KAB74+r6ZszIy/iOYCOLsmjF9Q9XiNjQ +-> ssh-ed25519 77YXTQ FiTDgl4Sc1gQya5+wzeVc16QTigKTHYwVOgK5sdVW1I +69qWGBuDiAXOM3413frPrccPXJOxBq9TZnpiCcMFbZw +-> ssh-ed25519 5yNkVQ sHxSA0EI58e5bNiREh5si92aDyti0wBrtavfTkth8ik +Dg5+vGoFwLyG3Oxl6yBV6QVWQxoJl8ieuRcHAnaHZ8o +--- keoCBDjcgb4VUtjzo/YD++ZS7aVYj/iyOdgrd5+2LHE ++ø”1péÃ[ÈzÞ'yäS<|Oá”–";@ké;y£í¦³­zfÊK¿Y{À<Ò7O5XÐh%âEº‡RÆRÊ‚kžÝ$ \ No newline at end of file -- cgit v1.3.1