{ zentralwerk, hostRegistry, config, options, lib, pkgs, ... }: let cfg = config.c3d2; hqPrefix64 = lib.removeSuffix "::" (builtins.head ( builtins.split "/" zentralwerk.lib.config.site.net.c3d2.subnets6.dn42 )); neighMod = with lib; types.submodule { options = { addrs = mkOption { type = with types; attrsOf str; default = { }; }; via = mkOption { type = with types; listOf str; default = [ ]; }; } // (with builtins; let value = mkOption { type = types.str; }; in listToAttrs (map (name: { inherit name value; }) [ "exchpub" "id" "noisepub" "signpub" ])); }; # Generate a deterministic IPv6 address for a 64 bit prefix # and seed string. Prefix must not contain trailing ':'. toIpv6Address = prefix64: seed: with builtins; let digest = builtins.hashString "sha256" seed; hextets = map (i: substring (4 * i) 4 digest) [ 0 1 2 3 ]; in concatStringsSep ":" ([ prefix64 ] ++ hextets); # Generate a deterministic public IPv6 addresses # for the HQ networking using a seed string. toHqPrivateAddress = toIpv6Address hqPrefix64; in { options.c3d2 = with lib; { acmeEmail = mkOption { type = types.str; default = "mail@c3d2.de"; description = '' Admin email address to use for Letsencrypt ''; }; allUsersCanSshRoot = lib.mkOption { type = lib.types.bool; default = false; description = '' Let all people in c3d2.users login as root for deployment via SSH. ''; }; isInHq = mkEnableOption "HQ presence (TODO: what is this? association to VLAN 5?)"; enableMotd = mkOption { type = types.bool; default = cfg.isInHq; defaultText = literalExample "config.c3d2.isInHq"; }; mergeHostsFile = mkOption { type = types.bool; default = cfg.isInHq; description = '' Whether to add c3d2.hosts to /etc/hosts. ''; }; mergeNncpSettings = mkEnableOption '' Whether to merge c3d2.nncp.<…>.nncp into programs.nncp.settings. ''; k-ot.enable = mkEnableOption '' Add k-ot user to this machine. Anyone with an SSH key listed in c3d2.users can log in as this user. ''; hq = { interface = mkOption { type = with types; nullOr str; default = null; example = "eth0"; description = '' Configure the given interface name with an internal IP address. ''; }; enableBinaryCache = mkOption { type = types.bool; default = cfg.isInHq; defaultText = literalExample "config.c3d2.isInHq"; description = "Whether to enable the local Nix binary cache"; }; enableMpdProxy = mkOption { type = types.bool; default = false; description = "Whether to proxy the local MPD database"; }; journalToMqtt = mkOption { type = types.bool; # broken :( default = false; }; }; hosts = mkOption { type = types.attrsOf (types.submodule { options = { ether = mkOption { type = with types; nullOr str; default = null; }; ip4 = mkOption { type = with types; nullOr str; default = null; }; ip6 = mkOption { type = with types; nullOr str; default = null; }; publicKey = mkOption { type = with types; nullOr str; default = null; }; wol = mkOption { type = types.bool; default = false; }; serial = mkOption { type = with types; nullOr str; default = null; description = '' Hardware serial number to help identification when netbooting. ''; }; }; }); }; nncp = { neigh = mkOption { type = with types; attrsOf neighMod; default = { }; description = '' Attrset of NNCP neighbours for relaying packets. User endpoints go in c3d2.users. ''; }; }; simd = { enable = lib.mkEnableOption "optimized builds with simd instructions"; arch = lib.mkOption { type = with lib.types; nullOr str; default = null; description = '' Microarchitecture string for nixpkgs.hostPlatform.gcc.march and to generate system-features. Can be determined with: gcc -march=native -Q --help=target | grep march ''; }; }; users = mkOption { type = types.attrsOf (types.submodule { options.sshKeys = mkOption { type = with types; listOf str; default = [ ]; }; }); }; }; config = let adminKeys = with builtins; lib.lists.flatten ( map (getAttr "sshKeys") (attrValues cfg.users) ); mkIfIsInHq = x: lib.mkIf cfg.isInHq (lib.mkDefault x); in { networking.hosts = lib.mkIf cfg.mergeHostsFile (( lib.attrsets.mapAttrs' (n: v: { name = v.ip4; value = [ "${n}.c3d2" ]; }) (lib.attrsets.filterAttrs (_: v: v.ip4 != null) cfg.hosts) ) // ( lib.attrsets.mapAttrs' (n: v: { name = v.ip6; value = [ "${n}.c3d2" ]; }) (lib.attrsets.filterAttrs (_: v: v.ip6 != null) cfg.hosts) )); programs.nncp.settings = lib.optionalAttrs cfg.mergeNncpSettings cfg.nncp; users.motd = lib.mkIf cfg.enableMotd (builtins.readFile ./motd); users = { users = { k-ot = lib.mkIf cfg.k-ot.enable { createHome = true; isNormalUser = true; uid = 1000; extraGroups = [ "audio" "video" "wheel" ]; password = "k-otk-ot"; openssh.authorizedKeys.keys = adminKeys; }; root.openssh.authorizedKeys.keys = lib.mkIf cfg.allUsersCanSshRoot adminKeys; }; }; services.vector = lib.mkIf config.c3d2.hq.journalToMqtt { enable = true; journaldAccess = true; settings = { sources.journal = { type = "journald"; current_boot_only = true; }; sinks.mqtt = { inputs = [ "journal" ]; type = "mqtt"; host = "broker.serv.zentralwerk.org"; # port = 8883; user = "SECRET[mqtt.user]"; password = "SECRET[mqtt.password]"; client_id = "vector-${config.networking.hostName}"; encoding.codec = "json"; topic = "journal/{{ host }}/{{ _SYSTEMD_UNIT }}/{{ PRIORITY }}"; # tls.enabled = true; # tls.ca_file = "/etc/ssl/certs/ca-certificates.crt"; }; secret.mqtt = let catSecrets = with pkgs; writeScript "cat-vector-secrets" '' #!${runtimeShell} -e echo '{' COMMA=n for F in $@; do if [ $COMMA = y ]; then echo ' ,' else COMMA=y fi echo ' "'$(basename $F)'": {"value": "'$(cat $F)'", "error": null }' done echo '}' ''; in { type = "exec"; command = [ catSecrets config.sops.secrets."mqtt/user".path config.sops.secrets."mqtt/password".path ]; }; }; }; sops.secrets = lib.mkIf config.c3d2.hq.journalToMqtt { "mqtt/user" = { sopsFile = ../modules/mqtt.yaml; owner = config.systemd.services.vector.serviceConfig.User; }; "mqtt/password" = { sopsFile = ../modules/mqtt.yaml; owner = config.systemd.services.vector.serviceConfig.User; }; }; assertions = [ { assertion = cfg.isInHq -> (config.users.users.root.password == null); message = "Root passwords not allowed in HQ"; } { assertion = cfg.hq.enableBinaryCache -> cfg.mergeHostsFile; message = "mergeHostsFile must be enabled for enableBinaryCache"; } { assertion = cfg.hq.enableMpdProxy -> cfg.mergeHostsFile; message = "mergeHostsFile must be enabled for enableMpdProxy"; } { assertion = cfg.isInHq -> builtins.hasAttr config.networking.hostName cfg.hosts; message = "${config.networking.hostName} is not registered in ${toString ../host-registry.nix}"; } ( # Check for host registry address collisions let getAddrHosts = key: builtins.foldl' (result: host: if cfg.hosts.${host}.${key} != null then let addr = cfg.hosts."${host}"."${key}"; in if result ? "${addr}" then result // { "${addr}" = result."${addr}" ++ [ host ]; } else result // { "${addr}" = [ host ]; } else result ) { } (builtins.attrNames cfg.hosts); dupHosts = builtins.concatMap (hosts: if builtins.length hosts == 1 then [ ] else hosts ) ( builtins.attrValues ( getAddrHosts "ip4" // getAddrHosts "ip6" ) ); in { assertion = dupHosts == [ ]; message = "Hosts have duplicate addresses: ${lib.concatStringsSep " " dupHosts}"; } ) ]; boot.cleanTmpDir = true; documentation.nixos.enable = false; c3d2.allUsersCanSshRoot = lib.mkDefault true; i18n = { defaultLocale = "en_US.UTF-8"; supportedLocales = [ "en_US.UTF-8/UTF-8" "de_DE.UTF-8/UTF-8" ]; }; systemd.network.networks = lib.mkIf (cfg.hq.interface != null && config.networking.useNetworkd) { "40-eth0".routes = [{ routeConfig = { Gateway = "172.22.99.4"; GatewayOnLink = true; }; }]; }; networking = { defaultGateway = lib.mkIf (!config.networking.useNetworkd) ( mkIfIsInHq "172.22.99.4" ); domain = mkIfIsInHq "hq.c3d2.de"; interfaces = lib.mkIf (cfg.hq.interface != null) { "${cfg.hq.interface}".ipv6.addresses = [{ address = toHqPrivateAddress config.networking.hostName; prefixLength = 64; }]; }; nameservers = with hostRegistry.hosts.dnscache; [ ip4 ip6 "9.9.9.9" ]; useHostResolvConf = lib.mkIf (!config.services.resolved.enable) true; }; environment.etc."resolv.conf" = lib.mkIf (!config.services.resolved.enable) { text = lib.concatMapStrings (ns: '' nameserver ${ns} '') config.networking.nameservers; }; nix = { settings = { trusted-public-keys = lib.mkIf (config.networking.hostName != "hydra") [ (builtins.readFile ../hosts/hydra/cache-pub.key) ]; substituters = lib.mkIf (config.networking.hostName != "hydra") ( lib.mkBefore [ "https://nix-serve.hq.c3d2.de" ] ); }; gc = { automatic = lib.mkDefault true; dates = "06:00"; options = "--delete-older-than 21d"; randomizedDelaySec = "6h"; }; registry.c3d2 = { from = { id = "c3d2"; type = "indirect"; }; to = { type = "git"; url = "https://gitea.c3d2.de/C3D2/nix-config.git"; }; }; extraOptions = '' experimental-features = nix-command flakes builders-use-substitutes = true ''; }; services.openssh = { # Required for deployment enable = true; permitRootLogin = "prohibit-password"; }; sops.age.sshKeyPaths = lib.mkDefault [ "/etc/ssh/ssh_host_ed25519_key" ]; environment = { systemPackages = with pkgs; [ # Network fetchers curl wget git # System monitors htop iotop bmon ripgrep # Terminal managers tmux screen # Editors vim # Pipeview pv # Network debugging tcpdump ethtool mtr ]; variables = { # TERM = "xterm-256color"; }; # breaks various package builds noXlibs = lib.mkForce false; }; programs = { ssh.knownHosts = with builtins; let intersectKeys = intersectAttrs { publicKey = null; publicKeyFile = null; }; list = map (name: let host = getAttr name cfg.hosts; sshAttrs = intersectKeys host; in if sshAttrs == { } then null else { inherit name; value = let ip6 = if host.ip6 != null then host.ip6 else toHqPrivateAddress name; in { publicKey = null; publicKeyFile = null; hostNames = [ ip6 "${name}.hq.c3d2.de" "${name}.hq" name ]; } // sshAttrs; }) (builtins.attrNames cfg.hosts); keyedHosts = filter (x: x.value.publicKey != null || x.value.publicKeyFile != null) list; in listToAttrs keyedHosts; vim.defaultEditor = true; }; services.nginx = lib.mkIf config.services.nginx.enable { recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; }; time.timeZone = lib.mkDefault "Europe/Berlin"; # Reboot on hang systemd.watchdog = lib.mkIf (!config.boot.isContainer) { runtimeTime = "15s"; rebootTime = "15s"; }; # Defaults for LetsEncrypt security.acme = if options.security.acme ? defaults then { acceptTerms = true; # NixOS>=22.05 defaults = { email = cfg.acmeEmail; # letsencrypt staging server with way higher rate limits # server = "https://acme-staging-v02.api.letsencrypt.org/directory"; }; } else { acceptTerms = true; # TODO: NixOS<=21.05 email = cfg.acmeEmail; }; zramSwap.enable = true; }; }