{ config, lib, pkgs, ... }: with lib; let mapOptional = f: val: if (val != null) then (f val) else null; masterKeyOpts = { ... }: { options = with types; { key-path = mkOption { type = str; description = "Path of the host master key file, used to decrypt secrets."; }; public-key = mkOption { type = str; description = "Public key used during deployment to decrypt secrets for the host."; }; }; }; hostOpts = { hostname, ... }: { options = with types; { master-key = mkOption { type = nullOr (submodule masterKeyOpts); description = "Public key for the host master key, used by the host to decrypt secrets."; default = null; }; domain = mkOption { type = str; description = "Primary domain to which the host belongs, in the form of a domain name."; default = "fudo.org"; }; extra-domains = mkOption { type = listOf str; description = "Extra domain in which this host is reachable."; default = [ ]; }; aliases = mkOption { type = listOf str; description = "Host aliases used by the current host. Note this will be multiplied with extra-domains."; default = [ ]; }; site = mkOption { type = str; description = "Site at which the host is located."; }; local-networks = mkOption { type = listof str; description = "A list of networks to be considered trusted by this host."; default = [ "127.0.0.0/8" ]; }; profile = mkOption { type = listOf (enumOf (attrNames config.fudo.profiles)); description = "The profile to be applied to the host, determining what software is included."; }; admin-email = mkOption { type = nullOr str; description = "Email for the administrator of this host."; default = null; }; local-users = mkOption { type = listOf str; description = "List of users who should have local (i.e. login) access to the host."; default = [ ]; }; description = mkOption { type = str; description = "Description of this host."; default = "Another Fudo Host."; }; local-admins = mkOption { type = listOf str; description = "A list of users who should have admin access to this host."; default = [ ]; }; local-groups = mkOption { type = listOf str; description = "List of groups which should exist on this host."; default = [ ]; }; ssh-fingerprints = mkOption { type = listOf str; description = '' A list of DNS SSHFP records for this host. Get with `ssh-keygen -r ` ''; default = [ ]; }; rp = mkOption { type = nullOr str; description = "Responsible person."; default = null; }; tmp-on-tmpfs = mkOption { type = bool; description = "Use tmpfs for /tmp. Great if you've got enough (>16G) RAM."; default = true; }; enable-gui = mkEnableOption "Install desktop GUI software."; docker-server = mkEnableOption "Enable Docker on the current host."; kerberos-services = mkOption { type = listOf str; description = "List of services which should exist for this host, if it belongs to a realm."; default = [ "ssh" "host" ]; }; ssh-pubkeys = mkOption { type = listOf path; description = "SSH key files of the host."; default = []; }; build-pubkeys = mkOption { type = listOf str; description = "SSH public keys used to access the build server."; default = [ ]; }; external-interfaces = mkOption { type = listOf str; description = "A list of interfaces on which to enable the firewall."; default = [ ]; }; keytab-secret-file = mkOption { type = nullOr str; description = "Keytab from which to create a keytab secret."; default = null; }; keep-cool = mkOption { type = bool; description = "A host that tends to overheat. Try to keep it cooler."; default = false; }; nixos-system = mkOption { type = bool; description = "Whether the host is a NixOS system."; default = true; }; arch = mkOption { type = str; description = "System architecture of the system."; }; machine-id = mkOption { type = nullOr str; description = "Machine id of the system. See: man machine-id."; default = null; }; android-dev = mkEnableOption "Enable ADB on the host."; }; }; in { options.fudo.hosts = with types; mkOption { type = attrsOf (submodule hostOpts); description = "Host configurations for all hosts known to the system."; default = { }; }; config = let hostname = config.instance.hostname; host-cfg = config.fudo.hosts.${hostname}; site-name = host-cfg.site; site = config.fudo.sites.${site-name}; domain-name = host-cfg.domain; domain = config.fudo.domains.${domain-name}; has-build-servers = (length (attrNames site.build-servers)) > 0; has-build-keys = (length host-cfg.build-pubkeys) > 0; in { networking = { hostName = config.instance.hostname; domain = domain-name; nameservers = site.nameservers; # This will cause a loop on the gateway itself #defaultGateway = site.gateway-v4; #defaultGateway6 = site.gateway-v6; firewall = { enable = (length host-cfg.external-interfaces) > 0; allowedTCPPorts = [ 22 ]; }; hostId = mkIf (host-cfg.machine-id != null) (substring 0 8 host-cfg.machine-id); }; # NixOS generates a stupid hosts file, just force it environment.etc = { hosts = let host-entries = mapAttrsToList (ip: hostnames: "${ip} ${concatStringsSep " " hostnames}") config.fudo.system.hostfile-entries; in mkForce { text = '' 127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost 127.0.0.2 ${hostname} localhost ::1 ${hostname}.${domain-name} ${hostname} localhost ${concatStringsSep "\n" host-entries} ''; user = "root"; group = "root"; mode = "0444"; }; machine-id = mkIf (host-cfg.machine-id != null) { text = host-cfg.machine-id; user = "root"; group = "root"; mode = "0444"; }; }; # fudo.hosts.${hostname}.build-pubkeys = # map builtins.readFile # (map (build-key-path: "${build-key-path}/${hostname}.key.pub") # (optional (site.build-key-path != null) site.build-key-path)); # nix = mkIf # (has-build-servers && has-build-keys && site.enable-distributed-builds) { # buildMachines = mapAttrsToList (hostname: buildOpts: { # hostName = "${hostname}.${domain-name}"; # maxJobs = buildOpts.max-jobs; # speedFactor = buildOpts.speed-factor; # supportedFeatures = buildOpts.supported-features; # sshKey = config.fudo.secrets.host-secrets.${hostname}.build-private-key.target-file; # }) site.build-servers; # distributedBuilds = true; # }; time.timeZone = site.timezone; krb5.libdefaults.default_realm = domain.gssapi-realm; services.cron.mailto = domain.admin-email; environment.systemPackages = with pkgs; mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ]; virtualisation.docker = mkIf (host-cfg.docker-server) { enable = true; enableOnBoot = true; autoPrune.enable = true; }; boot.tmpOnTmpfs = host-cfg.tmp-on-tmpfs; fudo.secrets.host-secrets.${hostname} = let keytab-file = if (hasAttr hostname config.fudo.secrets.files.host-keytabs) then config.fudo.secrets.files.host-keytabs.${hostname} else null; build-private-key-file = if (hasAttr hostname config.fudo.secrets.files.build-keypairs) then config.fudo.secrets.files.build-keypairs.${hostname}.private-key else null; in { host-keytab = mkIf (keytab-file != null) { source-file = keytab-file; target-file = "/etc/krb5.keytab"; user = "root"; }; build-private-key = mkIf (build-private-key-file != null) { source-file = build-private-key-file; target-file = "/var/run/nix-build/host.key"; user = "root"; }; }; programs.adb.enable = host-cfg.android-dev; users.groups.adbusers = mkIf host-cfg.android-dev { members = config.instance.local-admins; }; # programs.ssh.knownHosts = let # keyed-hosts = # filterAttrs (host: opts: opts.ssh-pubkeys != []) config.fudo.hosts; # crossProduct = f: list0: list1: # concatMap (el0: map (el1: f el0 el1) list1) list0; # getHostnames = hostOpts: # [ hostOpts.hostname ] # ++ (crossProduct (host: domain: "${host}.${domain}") # ([ hostOpts.hostname ] ++ hostOpts.aliases) # ([ hostOpts.domain ] ++ hostOpts.extra-domains)); # getHostEntryPairs = host: # map (hostname: nameValuePair hostname { publicKey = host.ssh-pubkey; }) # (getHostnames host); # hostAttrsToList = hostAttrs: # mapAttrsToList (hostname: opts: { hostname = hostname; } // opts) # hostAttrs; # getKnownHosts = hosts: # concatMap getHostEntryPairs (hostAttrsToList hosts); # in listToAttrs (getKnownHosts keyed-hosts); }; }