From 09bb7e1ed9ea246929d819deaaf50d208f8e7622 Mon Sep 17 00:00:00 2001 From: cinereal Date: Sun, 24 Aug 2025 11:11:10 +0200 Subject: [PATCH] adapt generator modules from clan-core modules see https://git.clan.lol/clan/clan-core/src/branch/main/clanServices --- flake.nix | 1 + generators/default.nix | 11 +++ generators/disk-id/default.nix | 30 ++++++++ generators/disk-id/uuid4.sh | 20 ++++++ generators/garage/default.nix | 59 +++++++++++++++ generators/machine-id/default.nix | 46 ++++++++++++ generators/machine-id/uuid4.sh | 20 ++++++ generators/options.nix | 9 +++ generators/root-password/default.nix | 60 ++++++++++++++++ generators/sshd/client.nix | 6 ++ generators/sshd/default.nix | 6 ++ generators/sshd/server.nix | 103 +++++++++++++++++++++++++++ generators/sshd/shared.nix | 44 ++++++++++++ generators/state-version/default.nix | 22 ++++++ generators/user-password/default.nix | 74 +++++++++++++++++++ 15 files changed, 511 insertions(+) create mode 100644 generators/default.nix create mode 100644 generators/disk-id/default.nix create mode 100644 generators/disk-id/uuid4.sh create mode 100644 generators/garage/default.nix create mode 100644 generators/machine-id/default.nix create mode 100644 generators/machine-id/uuid4.sh create mode 100644 generators/options.nix create mode 100644 generators/root-password/default.nix create mode 100644 generators/sshd/client.nix create mode 100644 generators/sshd/default.nix create mode 100644 generators/sshd/server.nix create mode 100644 generators/sshd/shared.nix create mode 100644 generators/state-version/default.nix create mode 100644 generators/user-password/default.nix diff --git a/flake.nix b/flake.nix index 22e5860..c19c461 100644 --- a/flake.nix +++ b/flake.nix @@ -16,6 +16,7 @@ in { nixosModules.default = { imports = [ ./options.nix ]; }; nixosModules.backend-on-machine = { imports = [ ./backends/on-machine.nix ]; }; + nixosModules.generators = { imports = [ ./generators ]; }; # TODO fix tests checks = forAllSystems (system: let tests = { diff --git a/generators/default.nix b/generators/default.nix new file mode 100644 index 0000000..026b281 --- /dev/null +++ b/generators/default.nix @@ -0,0 +1,11 @@ +{ + imports = [ + ./disk-id + ./garage + ./machine-id + ./root-password + ./sshd + ./state-version + ./user-password + ]; +} diff --git a/generators/disk-id/default.nix b/generators/disk-id/default.nix new file mode 100644 index 0000000..4984660 --- /dev/null +++ b/generators/disk-id/default.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.vars.disk-id; +in +{ + options.vars.disk-id.enable = lib.mkEnableOption "Generates a uuid for use in disk device naming"; + config = lib.mkIf cfg.enable { + vars.generators.disk-id = { + files.diskId.secret = false; + runtimeInputs = [ + pkgs.coreutils + pkgs.bash + ]; + script = '' + uuid=$(bash ${./uuid4.sh}) + + # Remove the hyphens from the UUID + uuid_no_hyphens=$(echo -n "$uuid" | tr -d '-') + + echo -n "$uuid_no_hyphens" > "$out/diskId" + ''; + }; + disko.devices.disk."main".name = "main-" + config.vars.generators.disk-id.files.diskId.value; + }; +} diff --git a/generators/disk-id/uuid4.sh b/generators/disk-id/uuid4.sh new file mode 100644 index 0000000..ca3a7c4 --- /dev/null +++ b/generators/disk-id/uuid4.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Read 16 bytes from /dev/urandom +uuid=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n') + +# Break the UUID into pieces and apply the required modifications +byte6=${uuid:12:2} +byte8=${uuid:16:2} + +# Construct the correct version and variant +hex_byte6=$(printf "%x" $((0x$byte6 & 0x0F | 0x40))) +hex_byte8=$(printf "%x" $((0x$byte8 & 0x3F | 0x80))) + +# Rebuild the UUID with the correct fields +uuid_v4="${uuid:0:12}${hex_byte6}${uuid:14:2}${hex_byte8}${uuid:18:14}" + +# Format the UUID correctly 8-4-4-4-12 +uuid_formatted="${uuid_v4:0:8}-${uuid_v4:8:4}-${uuid_v4:12:4}-${uuid_v4:16:4}-${uuid_v4:20:12}" + +echo -n "$uuid_formatted" \ No newline at end of file diff --git a/generators/garage/default.nix b/generators/garage/default.nix new file mode 100644 index 0000000..2782514 --- /dev/null +++ b/generators/garage/default.nix @@ -0,0 +1,59 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.vars.garage; +in +{ + options.vars.garage.enable = lib.mkEnableOption '' + S3-compatible object store for small self-hosted geo-distributed deployments. + + This module generates garage-specific keys automatically. + + Options: [NixosModuleOptions](https://search.nixos.org/options?channel=unstable&size=50&sort=relevance&type=packages&query=garage) + Documentation: https://garagehq.deuxfleurs.fr/ + ''; + config = lib.mkIf cfg.enable { + systemd.services.garage.serviceConfig = { + LoadCredential = [ + "rpc_secret_path:${config.vars.generators.garage-shared.files.rpc_secret.path}" + "admin_token_path:${config.vars.generators.garage.files.admin_token.path}" + "metrics_token_path:${config.vars.generators.garage.files.metrics_token.path}" + ]; + Environment = [ + "GARAGE_ALLOW_WORLD_READABLE_SECRETS=true" + "GARAGE_RPC_SECRET_FILE=%d/rpc_secret_path" + "GARAGE_ADMIN_TOKEN_FILE=%d/admin_token_path" + "GARAGE_METRICS_TOKEN_FILE=%d/metrics_token_path" + ]; + }; + + vars.generators.garage = { + files.admin_token = { }; + files.metrics_token = { }; + runtimeInputs = [ + pkgs.coreutils + pkgs.openssl + ]; + script = '' + openssl rand -base64 -out "$out"/admin_token 32 + openssl rand -base64 -out "$out"/metrics_token 32 + ''; + }; + + vars.generators.garage-shared = { + share = true; + files.rpc_secret = { }; + runtimeInputs = [ + pkgs.coreutils + pkgs.openssl + ]; + script = '' + openssl rand -hex -out "$out"/rpc_secret 32 + ''; + }; + }; +} diff --git a/generators/machine-id/default.nix b/generators/machine-id/default.nix new file mode 100644 index 0000000..e6d9604 --- /dev/null +++ b/generators/machine-id/default.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + pkgs, + ... +}: +let + var = config.vars.generators.machine-id.files.machineId or { }; + cfg = config.vars.machine-id; +in +{ + options.vars.machine-id.enable = lib.mkEnableOption "Sets the /etc/machine-id and exposes it as a nix option"; + config = lib.mkIf cfg.enable (lib.mkMerge [ + (lib.mkIf ((var.value or null) != null) { + assertions = [ + { + assertion = lib.stringLength var.value == 32; + message = "machineId must be exactly 32 characters long."; + } + ]; + boot.kernelParams = [ + ''systemd.machine_id=${var.value}'' + ]; + environment.etc."machine-id" = { + text = var.value; + }; + }) + { + vars.generators.machine-id = { + files.machineId.secret = false; + runtimeInputs = [ + pkgs.coreutils + pkgs.bash + ]; + script = '' + uuid=$(bash ${./uuid4.sh}) + + # Remove the hyphens from the UUID + uuid_no_hyphens=$(echo -n "$uuid" | tr -d '-') + + echo -n "$uuid_no_hyphens" > "$out/machineId" + ''; + }; + } + ]); +} diff --git a/generators/machine-id/uuid4.sh b/generators/machine-id/uuid4.sh new file mode 100644 index 0000000..ca3a7c4 --- /dev/null +++ b/generators/machine-id/uuid4.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Read 16 bytes from /dev/urandom +uuid=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n') + +# Break the UUID into pieces and apply the required modifications +byte6=${uuid:12:2} +byte8=${uuid:16:2} + +# Construct the correct version and variant +hex_byte6=$(printf "%x" $((0x$byte6 & 0x0F | 0x40))) +hex_byte8=$(printf "%x" $((0x$byte8 & 0x3F | 0x80))) + +# Rebuild the UUID with the correct fields +uuid_v4="${uuid:0:12}${hex_byte6}${uuid:14:2}${hex_byte8}${uuid:18:14}" + +# Format the UUID correctly 8-4-4-4-12 +uuid_formatted="${uuid_v4:0:8}-${uuid_v4:8:4}-${uuid_v4:12:4}-${uuid_v4:16:4}-${uuid_v4:20:12}" + +echo -n "$uuid_formatted" \ No newline at end of file diff --git a/generators/options.nix b/generators/options.nix new file mode 100644 index 0000000..4d0ccb1 --- /dev/null +++ b/generators/options.nix @@ -0,0 +1,9 @@ +{ + lib, + ... +}: +{ + options.vars = { + unattended = lib.mkEnableOption "Whether to default to generating values unattended, rather than prompting for desired values."; + }; +} diff --git a/generators/root-password/default.nix b/generators/root-password/default.nix new file mode 100644 index 0000000..d3abdbf --- /dev/null +++ b/generators/root-password/default.nix @@ -0,0 +1,60 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.vars.root-password; +in +{ + imports = [ + ../options.nix + ]; + options.vars.root-password = { + enable = lib.mkEnableOption "Automatically generates and configures a password for the root user."; + prompt = lib.mkOption { + type = lib.types.bool; + default = !config.vars.unattended; + example = true; + description = "Whether the user should be prompted."; + }; + }; + config = lib.mkIf cfg.enable { + users.mutableUsers = false; + users.users.root.hashedPasswordFile = + config.vars.generators.root-password.files.password-hash.path; + + vars.generators.root-password = { + files.password-hash = { + neededFor = "users"; + }; + files.password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service"; + files.password = { + deploy = false; + }; + runtimeInputs = [ + pkgs.coreutils + pkgs.mkpasswd + pkgs.xkcdpass + ]; + prompts = lib.mkIf cfg.prompt { + password = { + type = "hidden"; + persist = true; + description = "You can autogenerate a password, if you leave this prompt blank."; + }; + }; + + script = '' + prompt_value=${if cfg.prompt then ''$(cat "$prompts"/password)'' else ""} + if [[ -n "''${prompt_value-}" ]]; then + echo "$prompt_value" | tr -d "\n" > "$out"/password + else + xkcdpass --numwords 3 --delimiter - --count 1 | tr -d "\n" > "$out"/password + fi + mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash + ''; + }; + }; +} diff --git a/generators/sshd/client.nix b/generators/sshd/client.nix new file mode 100644 index 0000000..221bf29 --- /dev/null +++ b/generators/sshd/client.nix @@ -0,0 +1,6 @@ +{ ... }: +{ + imports = [ + ./shared.nix + ]; +} diff --git a/generators/sshd/default.nix b/generators/sshd/default.nix new file mode 100644 index 0000000..f7ecff0 --- /dev/null +++ b/generators/sshd/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./client.nix + ./server.nix + ]; +} diff --git a/generators/sshd/server.nix b/generators/sshd/server.nix new file mode 100644 index 0000000..16af8d2 --- /dev/null +++ b/generators/sshd/server.nix @@ -0,0 +1,103 @@ +{ + config, + pkgs, + lib, + ... +}: +let + stringSet = list: builtins.attrNames (builtins.groupBy lib.id list); + + domains = stringSet config.vars.sshd.certificate.searchDomains; + + cfg = config.vars.sshd; + + name = config.networking.hostName; +in +{ + imports = [ ../shared.nix ]; + options = { + vars.sshd = { + enable = lib.mkEnableOption "Set up the opensshd service, generating a host key for the machine."; + hostKeys.rsa.enable = lib.mkEnableOption "Generate RSA host key"; + }; + }; + config = lib.mkIf cfg.enable { + services.openssh = { + enable = true; + settings.PasswordAuthentication = false; + + settings.HostCertificate = lib.mkIf ( + cfg.certificate.searchDomains != [ ] + ) config.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path; + + hostKeys = + [ + { + path = config.vars.generators.openssh.files."ssh.id_ed25519".path; + type = "ed25519"; + } + ] + ++ lib.optional cfg.hostKeys.rsa.enable { + path = config.vars.generators.openssh-rsa.files."ssh.id_rsa".path; + type = "rsa"; + }; + }; + + vars.generators.openssh = { + files."ssh.id_ed25519" = { }; + files."ssh.id_ed25519.pub".secret = false; + runtimeInputs = [ + pkgs.coreutils + pkgs.openssh + ]; + script = '' + ssh-keygen -t ed25519 -N "" -f "$out"/ssh.id_ed25519 + ''; + }; + + programs.ssh.knownHosts.sshd-self-ed25519 = { + hostNames = [ + "localhost" + config.networking.hostName + ] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn); + publicKey = config.vars.generators.openssh.files."ssh.id_ed25519.pub".value; + }; + + vars.generators.openssh-rsa = lib.mkIf config.vars.sshd.hostKeys.rsa.enable { + files."ssh.id_rsa" = { }; + files."ssh.id_rsa.pub".secret = false; + runtimeInputs = [ + pkgs.coreutils + pkgs.openssh + ]; + script = '' + ssh-keygen -t rsa -b 4096 -N "" -f "$out"/ssh.id_rsa + ''; + }; + + vars.generators.openssh-cert = lib.mkIf (cfg.certificate.searchDomains != [ ]) { + files."ssh.id_ed25519-cert.pub".secret = false; + dependencies = [ + "openssh" + "openssh-ca" + ]; + validation = { + inherit name; + domains = lib.genAttrs config.vars.sshd.certificate.searchDomains lib.id; + }; + runtimeInputs = [ + pkgs.openssh + pkgs.jq + ]; + script = '' + ssh-keygen \ + -s $in/openssh-ca/id_ed25519 \ + -I ${name} \ + -h \ + -n ${lib.concatMapStringsSep "," (d: "${name}.${d}") domains} \ + $in/openssh/ssh.id_ed25519.pub + mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub + ''; + }; + }; +} diff --git a/generators/sshd/shared.nix b/generators/sshd/shared.nix new file mode 100644 index 0000000..f9dcc88 --- /dev/null +++ b/generators/sshd/shared.nix @@ -0,0 +1,44 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.vars.sshd.certificate; +in +{ + options.vars.sshd.certificate = { + enable = lib.mkEnableOption "Add machines to the known hosts, enabling secure remote access to them over ssh."; + searchDomains = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "mydomain.com" ]; + description = "List of domains to include in the certificate. This option will prepend the machine name in front of each domain before adding it to the certificate."; + }; + }; + config = lib.mkIf cfg.enable { + vars.generators.openssh-ca = + lib.mkIf (config.vars.sshd.certificate.searchDomains != [ ]) + { + share = true; + files.id_ed25519.deploy = false; + files."id_ed25519.pub" = { + deploy = false; + secret = false; + }; + runtimeInputs = [ + pkgs.openssh + ]; + script = '' + ssh-keygen -t ed25519 -N "" -f "$out"/id_ed25519 + ''; + }; + + programs.ssh.knownHosts.ssh-ca = lib.mkIf (config.vars.sshd.certificate.searchDomains != [ ]) { + certAuthority = true; + extraHostNames = builtins.map (domain: "*.${domain}") config.vars.sshd.certificate.searchDomains; + publicKey = config.vars.generators.openssh-ca.files."id_ed25519.pub".value; + }; + }; +} diff --git a/generators/state-version/default.nix b/generators/state-version/default.nix new file mode 100644 index 0000000..77ae559 --- /dev/null +++ b/generators/state-version/default.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: +let + var = config.vars.generators.state-version.files.version or { }; + cfg = config.vars.state-version; +in +{ + options.vars.state-version.enable = lib.mkEnableOption "Automatically generate the state version of the nixos installation."; + config = lib.mkIf cfg.enable { + system.stateVersion = lib.mkDefault (lib.removeSuffix "\n" var.value); + + vars.generators.state-version = { + files.version = { + secret = false; + value = lib.mkDefault config.system.nixos.release; + }; + runtimeInputs = [ ]; + script = '' + echo -n ${config.system.stateVersion} > "$out"/version + ''; + }; + }; +} diff --git a/generators/user-password/default.nix b/generators/user-password/default.nix new file mode 100644 index 0000000..8cac2ad --- /dev/null +++ b/generators/user-password/default.nix @@ -0,0 +1,74 @@ +{ + pkgs, + config, + lib, + ... +}: +let + cfg = config.vars.user-password; +in +{ + imports = [ + ../options.nix + ]; + options.vars.user-password = { + enable = lib.mkEnableOption '' + Automatically generates and configures a password for a user. + + This will set `mutableUsers` to `false`, meaning you can not manage user passwords through `passwd` anymore. + ''; + users = lib.mkOption { + type = lib.types.attrsOf (lib.submodule { + options = { + prompt = lib.mkOption { + type = lib.types.bool; + default = !config.vars.unattended; + example = true; + description = "Whether the user should be prompted."; + }; + }; + }); + description = "The users for which to generate passwords."; + example = "{ alice = { }; }"; + }; + }; + + config = lib.mkIf cfg.enable { + users.mutableUsers = false; + users.users = lib.mapAttrs (userName: _: { + hashedPasswordFile = config.vars.generators."user-password-${userName}".files.user-password-hash.path; + isNormalUser = lib.mkDefault true; + }) cfg.users; + + vars.generators = lib.mapAttrs (userName: user: { + "user-password-${userName}" = { + files.user-password-hash.neededFor = "users"; + files.user-password-hash.restartUnits = lib.optional (config.services.userborn.enable) "userborn.service"; + + prompts = lib.mkIf user.prompt { + "user-password-${userName}" = { + type = "hidden"; + persist = true; + description = "You can autogenerate a password, if you leave this prompt blank."; + files.user-password.deploy = false; + }; + }; + + runtimeInputs = [ + pkgs.coreutils + pkgs.xkcdpass + pkgs.mkpasswd + ]; + script = '' + prompt_value=${if user.prompt then ''$(cat "$prompts"/user-password)'' else ""} + if [[ -n "''${prompt_value-}" ]]; then + echo "$prompt_value" | tr -d "\n" > "$out"/user-password + else + xkcdpass --numwords 3 --delimiter - --count 1 | tr -d "\n" > "$out"/user-password + fi + mkpasswd -s -m sha-512 < "$out"/user-password | tr -d "\n" > "$out"/user-password-hash + ''; + }; + }) cfg.users; + }; +}