{ config, lib, pkgs, ... }: with lib; let hostOpts = { hostname, ... }: { options = with types; { 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 { # FIXME: get this list from profiles directly type = listof (enum "desktop" "laptop" "server"); 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-pubkey = mkOption { type = nullOr str; description = "SSH key of the host. Find with `ssh-keyscan`. Skip the hostname, just type and key."; default = null; }; 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; }; }; }; 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; # Necessary to ensure that Kerberos and Avahi both work. Kerberos needs # the fqdn of the host, whereas Avahi wants just the simple hostname.` hosts = { "127.0.0.2" = [ "${hostname}.${domain-name}" "${hostname}" ]; "127.0.0.1" = [ "${hostname}.${domain-name}" "${hostname}" ]; "::1" = [ "${hostname}.${domain-name}" "${hostname}" ]; }; firewall = { enable = (length host-cfg.external-interfaces) > 0; allowedTCPPorts = [ 22 ]; }; }; environment.etc.hosts = mkForce { text = '' 127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost 127.0.0.2 ${hostname} localhost ::1 ${hostname}.${domain-name} ${hostname} localhost ''; user = "root"; group = "root"; mode = "0444"; }; 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; }) 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}.host-keytab = let mapOptional = f: val: if (val != null) then (f val) else null; keytab-file = mapOptional (keytab-path: if (pathExists keytab-path) then /. + builtins.toPath keytab-path else null) (mapOptional (keytab-dir: "${keytab-dir}/${hostname}.keytab") site.keytab-directory); in mkIf (keytab-file != null) { source-file = /. + builtins.toPath keytab-file; target-file = "/etc/krb5.keytab"; user = "root"; }; programs.ssh.knownHosts = let keyed-hosts = filterAttrs (host: opts: opts.ssh-pubkey != null) config.fudo.hosts; traceOut = obj: builtins.trace obj obj; 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); }; }