Commit 47742f8

HPCesia <me@hpcesia.com>
2025-07-15 13:33:30
refactor: network config
1 parent 6bfd293
Changed files (13)
hosts/chaser-kevin/default.nix
@@ -9,9 +9,7 @@
 #  My main computer, with I7-14650HX + RTX4060 Laptop GPU + 48GB memory, for daily use.
 #
 #############################################################
-let
-  hostName = "kevin"; # Define your hostname.
-in {
+{
   imports = [
     nixos-hardware.nixosModules.common-cpu-intel
     nixos-hardware.nixosModules.common-hidpi
@@ -23,13 +21,6 @@ in {
     ./boot.nix
   ];
 
-  modules.my-hosts.${hostName}.network = {
-    enable = "networkmanager";
-    iface = "wlp0s20f3";
-    useDHCP = true;
-    nameservers = myvars.defaultNameservers;
-  };
-
   # This value determines the NixOS release from which the default
   # settings for stateful data, like file locations and database versions
   # on your system were taken. It‘s perfectly fine and recommended to leave
hosts/chaser-pardofelis/default.nix
@@ -10,27 +10,13 @@
 #  My main server hosted by Yecaoyun.
 #
 #############################################################
-let
-  hostName = "pardofelis"; # Define your hostname.
-in {
+{
   imports =
     (mylib.scanModules ./.)
     ++ [
       disko.nixosModules.default
     ];
 
-  modules.my-hosts.${hostName} = {
-    network = {
-      enable = "networkd";
-      iface = "eth0";
-      useDHCP = false;
-      nameservers = ["172.16.36.100"] ++ myvars.defaultNameservers;
-      search = ["local"];
-    };
-    hostPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO56HKTdzGulisPLhpfUmLQNEgwDqwD9SBLRb5aETffV root@pardofelis";
-    sshPorts = [23930];
-  };
-
   systemd.network.enable = true;
 
   # This value determines the NixOS release from which the default
hosts/general.nix
@@ -0,0 +1,26 @@
+{myvars, ...}: {
+  modules.my-hosts = {
+    kevin.network = {
+      enable = "networkmanager";
+      iface = "wlp0s20f3";
+      useDHCP = true;
+      nameservers = myvars.defaultNameservers;
+    };
+
+    pardofelis = {
+      network = {
+        enable = "networkd";
+        iface = "eth0";
+        useDHCP = false;
+        nameservers = ["172.16.36.100"] ++ myvars.defaultNameservers;
+        search = ["local"];
+        ipv4 = {secretName = "pardofelis-ipv4";};
+        ipv6 = {secretName = "pardofelis-ipv6";};
+        defaultGateway = {secretName = "pardofelis-gateway";};
+        defaultGateway6 = {secretName = "pardofelis-gateway6";};
+      };
+      hostPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO56HKTdzGulisPLhpfUmLQNEgwDqwD9SBLRb5aETffV root@pardofelis";
+      sshPorts = [23930];
+    };
+  };
+}
modules/base/hosts.nix
@@ -1,5 +1,14 @@
 {lib, ...}:
 with lib; let
+  secretType = types.submodule {
+    options = {
+      secretName = mkOption {
+        type = types.str;
+      };
+    };
+  };
+  optSecretType = types.nullOr (types.either types.str secretType);
+
   hostModule = types.submodule {
     options = {
       network = mkOption {
@@ -41,11 +50,11 @@ with lib; let
         default = [];
       };
       ipv4 = mkOption {
-        type = types.nullOr types.str;
+        type = optSecretType;
         default = null;
       };
       ipv6 = mkOption {
-        type = types.nullOr types.str;
+        type = optSecretType;
         default = null;
       };
       prefixLength4 = mkOption {
@@ -57,11 +66,11 @@ with lib; let
         default = 64;
       };
       defaultGateway = mkOption {
-        type = types.nullOr types.str;
+        type = optSecretType;
         default = null;
       };
       defaultGateway6 = mkOption {
-        type = types.nullOr types.str;
+        type = optSecretType;
         default = null;
       };
     };
modules/base/users.nix
@@ -5,40 +5,68 @@
   ...
 }: let
   hosts = config.modules.my-hosts;
-  sshTargetHosts =
+  managedHosts =
     lib.filterAttrs (
-      n: v: !builtins.isNull v.hostPublicKey && !builtins.isNull v.network.ipv4
+      name: host:
+        !builtins.isNull host.hostPublicKey
+        && (!builtins.isNull host.network.ipv4 || !builtins.isNull host.network.ipv6)
     )
     hosts;
-in {
-  programs.ssh = {
-    extraConfig =
-      lib.attrsets.foldlAttrs
-      (acc: host: val:
-        acc
-        + ''
-          Host ${host}
-            HostName ${val.network.ipv4}
-            Port ${lib.elemAt val.sshPorts 0}
-        '')
-      ""
-      sshTargetHosts;
-    knownHosts =
-      lib.mapAttrs'
-      (
-        host: value:
-          lib.attrsets.nameValuePair
-          (value.network.ipv4)
-          {
-            inherit (value) hostPublicKey;
-            hostNames = [host];
-          }
-      )
-      sshTargetHosts;
-  };
+  secretIpHosts =
+    lib.filterAttrs (
+      name: host:
+        isSecret host.network.ipv4 || isSecret host.network.ipv6
+    )
+    managedHosts;
 
+  isSecret = v: lib.isAttrs v && v ? "secretName";
+  isPlain = v: lib.isString v;
+in {
   users.users.${myvars.username} = {
     description = myvars.userfullname;
     openssh.authorizedKeys.keys = myvars.sshAuthorizedKeys;
   };
+
+  programs.ssh.knownHosts =
+    lib.mapAttrs'
+    (name: host: lib.nameValuePair name {publicKey = host.hostPublicKey;})
+    managedHosts;
+
+  programs.ssh.extraConfig = ''
+    ${lib.concatStringsSep "\n" (
+      lib.mapAttrsToList (
+        name: host: let
+          cfg = host.network;
+        in ''
+          Host ${name}
+            ${lib.optionalString (isPlain cfg.ipv4) "HostName ${cfg.ipv4}"}
+            ${lib.optionalString (isPlain cfg.ipv6) "HostName ${cfg.ipv6}"}
+            ${
+            lib.optionalString (isSecret cfg.ipv4 || isSecret cfg.ipv6)
+            "Include ${config.sops.templates."ssh-config-${name}".path}"
+          }
+            Port ${toString (lib.elemAt host.sshPorts 0)}
+        ''
+      )
+      managedHosts
+    )}
+  '';
+
+  sops.templates =
+    lib.mapAttrs'
+    (name: host:
+      lib.nameValuePair "ssh-config-${name}" {
+        content = ''
+          ${lib.optionalString (isSecret host.network.ipv4) ''
+            HostName ${config.sops.placeholder.${host.network.ipv4.secretName}}
+          ''}
+          ${lib.optionalString (isSecret host.network.ipv6) ''
+            HostName ${config.sops.placeholder.${host.network.ipv6.secretName}}
+          ''}
+        '';
+        owner = "root";
+        group = "ssh-secrets-users";
+        mode = "0440";
+      })
+    secretIpHosts;
 }
modules/nixos/base/networking.nix
@@ -1,77 +1,148 @@
 {
   config,
   lib,
-  hostName,
   ...
-}:
-lib.mkMerge [
-  {
-    # Use an NTP server located in the mainland of China to synchronize the system time
-    networking.timeServers = [
-      "ntp.aliyun.com" # Aliyun NTP Server
-      "ntp.tencent.com" # Tencent NTP Server
-    ];
-  }
+}: let
+  hostName = config.modules.currentHost;
+in
+  lib.mkMerge [
+    {
+      # Use an NTP server located in the mainland of China to synchronize the system time
+      networking.timeServers = [
+        "ntp.aliyun.com" # Aliyun NTP Server
+        "ntp.tencent.com" # Tencent NTP Server
+      ];
+    }
 
-  (let
-    cfg = config.modules.my-hosts.${hostName}.network;
-  in
-    lib.mkIf cfg.useDHCP {
-      assertions = map (x: {
-        assertion = cfg.${x} == null;
-        message = "my-host.network.useDHCP is confilt with my-host.network.${x}";
-      }) ["ipv4" "ipv6"];
-    })
+    (let
+      cfg = config.modules.my-hosts.${hostName}.network;
+    in
+      lib.mkIf cfg.useDHCP {
+        assertions = map (x: {
+          assertion = cfg.${x} == null;
+          message = "my-host.network.useDHCP is confilt with my-host.network.${x}";
+        }) ["ipv4" "ipv6"];
+      })
 
-  (let
-    cfg = config.modules.my-hosts.${hostName}.network;
-  in
-    lib.mkIf
-    (cfg.enable == "networkmanager")
-    {
-      networking = with cfg; {
-        networkmanager.enable = true;
-        useDHCP = lib.mkDefault useDHCP;
-        inherit hostName search defaultGateway defaultGateway6 nameservers;
-        interfaces.${cfg.iface} = lib.mkIf (!builtins.isNull cfg.ipv4 && !builtins.isNull cfg.ipv6) {
-          ipv4.addresses = lib.optional (!builtins.isNull cfg.ipv4) {
-            address = cfg.ipv4;
-            prefixLength = cfg.prefixLength4;
+    (let
+      cfg = config.modules.my-hosts.${hostName}.network;
+    in
+      lib.mkIf
+      (cfg.enable == "networkmanager")
+      {
+        assertions = map (x: {
+          assertion = !(cfg.${x} ? "secretName");
+          message = "my-host.network.${x} should not be a secret when using networkmanager.";
+        }) ["ipv4" "ipv6" "defaultGateway" "defaultGateway6"];
+        networking = with cfg; {
+          networkmanager.enable = true;
+          useDHCP = lib.mkDefault useDHCP;
+          inherit hostName search defaultGateway defaultGateway6 nameservers;
+          interfaces.${cfg.iface} = lib.mkIf (!builtins.isNull cfg.ipv4 && !builtins.isNull cfg.ipv6) {
+            ipv4.addresses = lib.optional (!builtins.isNull cfg.ipv4) {
+              address = cfg.ipv4;
+              prefixLength = cfg.prefixLength4;
+            };
+            ipv6.addresses = lib.optional (!builtins.isNull cfg.ipv6) {
+              address = cfg.ipv6;
+              prefixLength = cfg.prefixLength6;
+            };
           };
-          ipv6.addresses = lib.optional (!builtins.isNull cfg.ipv6) {
-            address = cfg.ipv6;
-            prefixLength = cfg.prefixLength6;
+        };
+      })
+    (let
+      cfg = config.modules.my-hosts.${hostName}.network;
+      isSecret = v: lib.isAttrs v && v ? "secretName";
+      isInEval = x: (!builtins.isNull x && !isSecret x);
+    in
+      lib.mkIf
+      (cfg.enable == "networkd")
+      {
+        networking.useNetworkd = true;
+        networking.hostName = hostName;
+        systemd.network.networks."10-${cfg.iface}" = {
+          matchConfig.Name = [cfg.iface];
+          networkConfig = {
+            Address =
+              (lib.optionals (isInEval cfg.ipv4)
+                ["${cfg.ipv4}/${toString cfg.prefixLength4}"])
+              ++ (lib.optionals (isInEval cfg.ipv6)
+                ["${cfg.ipv6}/${toString cfg.prefixLength6}"]);
+            DNS = cfg.nameservers;
           };
+          routes =
+            (lib.optional (isInEval cfg.defaultGateway)
+              {
+                Destination = "0.0.0.0/0";
+                Gateway = cfg.defaultGateway;
+              })
+            ++ (lib.optional (isInEval cfg.defaultGateway6) {
+              Destination = "::/0";
+              Gateway = cfg.defaultGateway6;
+            });
+          linkConfig.RequiredForOnline = "routable";
         };
-      };
-    })
-  (let
-    cfg = config.modules.my-hosts.${hostName}.network;
-  in
-    lib.mkIf
-    (cfg.enable == "networkd")
-    {
-      networking.useNetworkd = true;
-      networking.hostName = hostName;
-      systemd.network.networks."10-${cfg.iface}" = {
-        matchConfig.Name = [cfg.iface];
-        networkConfig = {
-          Address =
-            (lib.optionals (!builtins.isNull cfg.ipv4) ["${cfg.ipv4}/${toString cfg.prefixLength4}"])
-            ++ (lib.optionals (!builtins.isNull cfg.ipv6) ["${cfg.ipv6}/${toString cfg.prefixLength6}"]);
-          DNS = cfg.nameservers;
+
+        environment.etc."systemd/network/10-${cfg.iface}.network.d/99-address.conf" =
+          lib.mkIf
+          (isSecret cfg.ipv4 || isSecret cfg.ipv6)
+          {
+            source = config.sops.templates.networkd-address.path;
+            user = "root";
+            group = "systemd-network";
+            mode = "0440";
+          };
+        environment.etc."systemd/network/10-${cfg.iface}.network.d/99-route.conf" =
+          lib.mkIf
+          (isSecret cfg.defaultGateway || isSecret cfg.defaultGateway6)
+          {
+            source = config.sops.templates.networkd-route.path;
+            user = "root";
+            group = "systemd-network";
+            mode = "0440";
+          };
+
+        sops.templates.networkd-address = {
+          content =
+            lib.mkIf
+            (isSecret cfg.ipv4 || isSecret cfg.ipv6)
+            ''
+              [Network]
+              ${
+                lib.optionalString (isSecret cfg.ipv4)
+                "Address=${config.sops.placeholder.${cfg.ipv4.secretName}}/${toString cfg.prefixLength4}"
+              }
+              ${
+                lib.optionalString (isSecret cfg.ipv6)
+                "Address=${config.sops.placeholder.${cfg.ipv6.secretName}}/${toString cfg.prefixLength6}"
+              }
+            '';
+          owner = "root";
+          group = "systemd-network";
+          mode = "0440";
+        };
+        sops.templates.networkd-route = {
+          content =
+            lib.mkIf
+            (isSecret cfg.defaultGateway || isSecret cfg.defaultGateway6)
+            "${
+              lib.optionalString (isSecret cfg.defaultGateway)
+              ''
+                [Route]
+                Gateway=${config.sops.placeholder.${cfg.defaultGateway.secretName}}
+                Destination=0.0.0.0/0
+              ''
+            }\n${
+              lib.optionalString (isSecret cfg.defaultGateway6)
+              ''
+                [Route]
+                Gateway=${config.sops.placeholder.${cfg.defaultGateway6.secretName}}
+                Destination=::/0
+              ''
+            }";
+          owner = "root";
+          group = "systemd-network";
+          mode = "0440";
         };
-        routes =
-          (lib.optional (!builtins.isNull cfg.defaultGateway)
-            {
-              Destination = "0.0.0.0/0";
-              Gateway = cfg.defaultGateway;
-            })
-          ++ (lib.optional (!builtins.isNull cfg.defaultGateway6) {
-            Destination = "::/0";
-            Gateway = cfg.defaultGateway6;
-          });
-        linkConfig.RequiredForOnline = "routable";
-      };
-    })
-]
+      })
+  ]
modules/nixos/base/user-group.nix
@@ -8,6 +8,7 @@
 
   users.groups = {
     "${myvars.username}" = {};
+    ssh-secrets-users = {};
   };
 
   users.users."${myvars.username}" = {
@@ -22,6 +23,7 @@
       "networkmanager"
       "wheel"
       "aria2"
+      "ssh-secrets-users"
     ];
   };
 
outputs/x86_64-linux/src/kevin.nix
@@ -17,6 +17,7 @@
       # common
       "secrets/nixos.nix"
       "modules/nixos/desktop.nix"
+      "hosts/general.nix"
       # host specific
       "hosts/chaser-${name}"
     ];
outputs/x86_64-linux/src/pardofelis.nix
@@ -20,6 +20,7 @@
       # common
       "secrets/nixos.nix"
       "modules/nixos/server/x86_64.nix"
+      "hosts/general.nix"
       # host specific
       "hosts/chaser-${name}"
     ];
secrets/hosts/pardofelis/default.nix
@@ -1,23 +1,10 @@
 {
-  config,
-  lib,
-  hostName,
-  ...
-}:
-lib.mkIf (hostName == "pardofelis") {
-  sops.secrets.pardofelis-network = {
-    format = "yaml";
-    key = "content";
-    sopsFile = ./secrets.yaml;
-    owner = "root";
-    group = "systemd-network";
-    mode = "0440";
-    path = "/etc/systemd/network/10-${config.modules.my-hosts.${hostName}.network.iface}.network.d/99-secret-ip.conf";
-  };
-  services.openssh.hostKeys = [
-    {
-      path = "/etc/ssh/ssh_host_ed25519_key";
-      type = "ed25519";
-    }
-  ];
+  sops.secrets = builtins.listToAttrs (builtins.map (x: {
+    name = "pardofelis-${x}";
+    value = {
+      format = "yaml";
+      sopsFile = ./secrets.yaml;
+      key = x;
+    };
+  }) ["ipv4" "ipv6" "gateway" "gateway6"]);
 }
secrets/hosts/pardofelis/secrets.yaml
@@ -1,4 +1,7 @@
-content: ENC[AES256_GCM,data:GWb0KiK4qOIm4ht+kRyAwSZsIysP5d/4ijvxTUUtMSxOc5Mq0j8C24x1VbFWk5n2w6Bx0PSzZwiTL0fBivZRs9bEi0lQAoLiF6szftw2hGcqwoWmNFDajr4i5MaICG1nY/zFelWd7o+45/1Z003hpx8bZEfiTAY=,iv:FXPH4KlhbVFjVvr27jJlFbehYfNeUVqXXHOY0azAJYQ=,tag:R0JPMsSxxpXDt20wlc3W6Q==,type:str]
+ipv4: ENC[AES256_GCM,data:EOyIUXJxIKZIjLjh,iv:fS6HCVpATCrOCleA+2ZqiJpQD/CqkOeFhRcgkVLx45I=,tag:7IqJE9v65SxJMcOW3juBIg==,type:str]
+ipv6: ENC[AES256_GCM,data:0tuG+y2elv10AmyLdTh6o1wggdSm9A==,iv:BzGoHn8JLlGpk/Ifn5Qtf7qHSMUzM3lXl2UOF7Eilts=,tag:mSnjqis1Z39j9+WWPQvB6g==,type:str]
+gateway: ENC[AES256_GCM,data:ScDchbNjK1DPkc4Zvw==,iv:AyMa6YkTyEJclJKOqIbWCc4bfr9IXyTrRNJ0cCv0LiE=,tag:bPwlivyWgcpKBd70Pp+z5A==,type:str]
+gateway6: ENC[AES256_GCM,data:0kNmpzpfa1Px+b8thcPU524SZWM=,iv:Rw9+fe1DvG/eE369zEiivy82aiWXRGvzTLBXEdd3BVY=,tag:nS1v2h/b169Q/7E7ywvu0A==,type:str]
 sops:
     age:
         - recipient: age1l9acz0cuy455nprryeqyv6ckfqgv3tekuk0kxvvxyunsapwmpvnsmaazhy
@@ -10,8 +13,8 @@ sops:
             WUd6SkpZSHpEYXdyZm9uV2V5VDdTNXcKO1nnWK+udwvNl6b+mxyTspgpsGxSQ9YP
             SpIglDs4+ya3n1UeoSg5JY6s7RjHQj9zFCL2b7FYUJgSrZ/XFVgS5g==
             -----END AGE ENCRYPTED FILE-----
-    lastmodified: "2025-07-15T09:27:08Z"
-    mac: ENC[AES256_GCM,data:I+y24lmIiH1kYljzLr3rWfx/9e+YNK95v6RBfqrwZifKMJbgApwnPzvAhVeYmL2Cx25TV8TLdwS1gQWvi6vje/lrM9mmRNChbXIARqQ4E58oEDpGTX1OWPNYKTb+nFwYmfO+eX1l/NR9LhM75axhLse8nVBblspXdvrcpMAHFuM=,iv:iqFyXxrAYT79l9tiXGZ57XXXmGOiOjCVEDlr4TA4jo0=,tag:23dSgbtwl2WnOYxtXRZLaw==,type:str]
+    lastmodified: "2025-07-15T14:00:16Z"
+    mac: ENC[AES256_GCM,data:1Ozw1la9CaD9b2neHnxTwZem4WsmrgUymBdUbjvDvgcH01yFVOtMs8FC2nkVus5bFuH7/jfnW30bKyIdclePYiqXXqRs3OS04Q6q9lMH71EZYdrAjlgltDFFDo67Z1WyzB02ePKVeZ7i06lDvBgLq8yq8oHUU+gSslOkP8rREUA=,iv:D5hS4VtKrWuoMx5lL0HU6lNUBTnGPGiqVFhcmCHjXYs=,tag:GcV6UnCuKTrZWd8QxT3zTA==,type:str]
     pgp:
         - created_at: "2025-07-15T09:13:38Z"
           enc: |-
secrets/hosts/README.md
@@ -0,0 +1,7 @@
+# Host secrets
+
+This folder contains host-specific settings, typically the IP address of a VPS.
+
+## IP Secret Management
+
+Define some common host configuration options in `/modules/base/hosts.nix` and declare them centrally in `/hosts/general.nix`. In `/modules/base/users.nix`, store IP-related secrets in files and import them into `knownHosts`; in `/modules/nixos/base/networking.nix`, store IP-related secrets in systemd's drop-in.
\ No newline at end of file
secrets/hosts/README.zh-CN.md
@@ -0,0 +1,7 @@
+# Host secrets
+
+此文件夹下存放主机特定的设置,一般而言是 VPS 的 IP 地址。
+
+## IP 机密管理
+
+通过 `/modules/base/hosts.nix` 定义主机的部分通用设置选项,并在 `/hosts/general.nix` 中集中声明。在 `/modules/base/users.nix` 中将 IP 相关机密存入文件并引入 `knownHosts` 中;在 `/modules/nixos/base/networking.nix` 中将 IP 相关机密存入 systemd 的 drop-in 中。
\ No newline at end of file