From 9edae4a39c2091a48d5a654c598008a73cea5786 Mon Sep 17 00:00:00 2001 From: niten Date: Tue, 26 Apr 2022 18:39:53 -0700 Subject: [PATCH] Lots of changes. --- lib/fudo/git.nix | 15 ++- lib/fudo/postgres.nix | 56 +++++----- lib/fudo/powerdns.nix | 236 +++++++++++++++++++++------------------- lib/fudo/prometheus.nix | 70 +++++++----- lib/fudo/secrets.nix | 97 +++++++++-------- lib/lib/filesystem.nix | 35 +++--- 6 files changed, 269 insertions(+), 240 deletions(-) diff --git a/lib/fudo/git.nix b/lib/fudo/git.nix index 848977d..1104446 100644 --- a/lib/fudo/git.nix +++ b/lib/fudo/git.nix @@ -103,14 +103,13 @@ in { networking.firewall.allowedTCPPorts = mkIf (cfg.ssh != null) [ cfg.ssh.listen-port ]; - environment.systemPackages = with pkgs; let - gitea-admin = writeShellScriptBin "gitea-admin" '' - TMP=$(mktemp -d /tmp/gitea-XXXXXXXX) - ${gitea}/bin/gitea --custom-path ${cfg.state-dir}/custom --config ${cfg.state-dir}/custom/conf/app.ini --work-path $TMP $@ - ''; - in [ - gitea-admin - ]; + environment.systemPackages = with pkgs; + let + gitea-admin = writeShellScriptBin "gitea-admin" '' + TMP=$(mktemp -d /tmp/gitea-XXXXXXXX) + ${gitea}/bin/gitea --custom-path ${cfg.state-dir}/custom --config ${cfg.state-dir}/custom/conf/app.ini --work-path $TMP $@ + ''; + in [ gitea-admin ]; services = { gitea = { diff --git a/lib/fudo/postgres.nix b/lib/fudo/postgres.nix index 5a4c612..cabc0bc 100644 --- a/lib/fudo/postgres.nix +++ b/lib/fudo/postgres.nix @@ -11,8 +11,7 @@ let join-lines = lib.concatStringsSep "\n"; - strip-ext = filename: - head (builtins.match "^(.+)[.][^.]+$" filename); + strip-ext = filename: head (builtins.match "^(.+)[.][^.]+$" filename); userDatabaseOpts = { database, ... }: { options = { @@ -109,25 +108,25 @@ let makeNetworksEntry = networks: join-lines (map makeEntry networks); - makeLocalUserPasswordEntries = users: networks: let - network-entries = user: db: - join-lines - (map (network: "hostssl ${db} ${user} ${network} md5") - networks); - in join-lines (mapAttrsToList (user: opts: - join-lines (map (db: '' - local ${db} ${user} md5 - host ${db} ${user} 127.0.0.1/16 md5 - host ${db} ${user} ::1/128 md5 - ${network-entries user db} - '') (attrNames opts.databases))) (filterPasswordedUsers users)); + makeLocalUserPasswordEntries = users: networks: + let + network-entries = user: db: + join-lines + (map (network: "hostssl ${db} ${user} ${network} md5") networks); + in join-lines (mapAttrsToList (user: opts: + join-lines (map (db: '' + local ${db} ${user} md5 + host ${db} ${user} 127.0.0.1/16 md5 + host ${db} ${user} ::1/128 md5 + ${network-entries user db} + '') (attrNames opts.databases))) (filterPasswordedUsers users)); userTableAccessSql = user: entity: access: "GRANT ${access} ON ${entity} TO ${user};"; userDatabaseAccessSql = user: database: dbOpts: '' \c ${database} ${join-lines - (mapAttrsToList (userTableAccessSql user) dbOpts.entity-access)} + (mapAttrsToList (userTableAccessSql user) dbOpts.entity-access)} ''; userAccessSql = user: userOpts: join-lines (mapAttrsToList (userDatabaseAccessSql user) userOpts.databases); @@ -220,7 +219,7 @@ in { cleanup-tasks = mkOption { type = listOf str; description = "List of actions to take during shutdown of the service."; - default = []; + default = [ ]; }; systemd-target = mkOption { @@ -254,8 +253,7 @@ in { ensurePermissions = { "DATABASE ${database}" = "ALL PRIVILEGES"; }; }) opts.users)) cfg.databases))); - settings = let - ssl-enabled = cfg.ssl-certificate != null; + settings = let ssl-enabled = cfg.ssl-certificate != null; in { krb_server_keyfile = mkIf (cfg.keytab != null) cfg.keytab; @@ -286,9 +284,9 @@ in { systemd = { - tmpfiles.rules = optional (cfg.state-directory != null) (let - user = config.systemd.services.postgresql.serviceConfig.User; - in "d ${cfg.state-directory} 0700 ${user} - - -"); + tmpfiles.rules = optional (cfg.state-directory != null) + (let user = config.systemd.services.postgresql.serviceConfig.User; + in "d ${cfg.state-directory} 0700 ${user} - - -"); targets.${strip-ext cfg.systemd-target} = { description = "Postgresql and associated systemd services."; @@ -296,14 +294,12 @@ in { }; paths = let - user-password-files = mapAttrsToList - (user: userOpts: userOpts.password-file) - cfg.users; + user-password-files = + mapAttrsToList (user: userOpts: userOpts.password-file) cfg.users; in { postgresql-password-watcher = mkIf (length user-password-files > 0) { wantedBy = [ "default.target" ]; - description = - "Reset all user passwords if any changes occur."; + description = "Reset all user passwords if any changes occur."; pathConfig = { PathChanged = user-password-files; Unit = "postgresql-password-setter.service"; @@ -368,6 +364,10 @@ in { # ${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL* # ''; + # Wait a bit before starting dependent services, to let postgres finish initializing + serviceConfig.ExecStartPost = + mkAfter [ "${pkgs.coreutils}/bin/sleep 10" ]; + postStop = concatStringsSep "\n" cfg.cleanup-tasks; }; @@ -382,8 +382,8 @@ in { allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;"; extra-settings-sql = pkgs.writeText "settings.sql" '' - ${concatStringsSep "\n" - (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))} + ${concatStringsSep "\n" (map allow-user-login + (mapAttrsToList (key: val: key) cfg.users))} ${usersAccessSql cfg.users} ''; in pkgs.writeShellScript "postgresql-finalizer.sh" '' diff --git a/lib/fudo/powerdns.nix b/lib/fudo/powerdns.nix index 61e878f..8f5d0e3 100644 --- a/lib/fudo/powerdns.nix +++ b/lib/fudo/powerdns.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... } @ toplevel: +{ config, lib, pkgs, ... }@toplevel: # NOTE: To get DNS records: # pdnsutil --config-dir=... show-zone @@ -38,61 +38,67 @@ let include-dir=${runtime-dir} ''; - make-pgpass-file = user: target-file: let - db = cfg.database; - in pkgs.writeShellScript "genenrate-pgpass-file.sh" '' - touch ${target-file} - chown ${user} ${target-file} - chmod 700 ${target-file} - PASSWORD=$(cat ${db.password-file}) - echo "${db.host}:${toString db.port}:${db.database}:${db.user}:__PASSWORD__" | sed "s/__PASSWORD__/$PASSWORD/" > ${target-file} - ''; + make-pgpass-file = user: target-file: + let db = cfg.database; + in pkgs.writeShellScript "genenrate-pgpass-file.sh" '' + touch ${target-file} + chown ${user} ${target-file} + chmod 700 ${target-file} + PASSWORD=$(cat ${db.password-file}) + echo "${db.host}:${ + toString db.port + }:${db.database}:${db.user}:__PASSWORD__" | sed "s/__PASSWORD__/$PASSWORD/" > ${target-file} + ''; - mkRecord = name: type: content: { - inherit name type content; - }; + mkRecord = name: type: content: { inherit name type content; }; - initialize-domain-sql = domain: let - domain-name = domain.domain; - host-ip = pkgs.lib.network.host-ipv4 config hostname; - ipv6-net = net: (builtins.match ":" net) != null; - ipv4-net = net: !(ipv6-net net); - domain-records = [ - (mkRecord domain-name "SOA" "ns1.${domain-name} hostmaster.${domain-name} ${toString config.instance.build-timestamp} 10800 3600 1209600 3600") - (mkRecord "_dmark.${domain-name}" "TXT" ''"v=DMARC1; p=reject; rua=mailto:${domain.admin}; ruf=mailto:${domain.admin}; fo=1;"'') - (mkRecord domain-name "NS" "ns1.${domain-name}") - (mkRecord domain-name "TXT" (let - networks = config.instance.local-networks; - v4-nets = map (net: "ip4:${net}") (filter ipv4-net networks); - v6-nets = map (net: "ip6:${net}") (filter ipv6-net networks); - networks-string = concatStringsSep " " (v4-nets ++ v6-nets); - in ''"v=spf1 mx ${networks-string} -all"'')) - (mkRecord "ns1.${domain-name}" "A" host-ip) - (mkRecord domain-name "A" host-ip) - ] ++ - (optional (domain.gssapi-realm != null) - (mkRecord "_kerberos.${domain-name}" "TXT" ''"domain.gssapi-realm"'')) ++ - (mapAttrsToList (alias: target: mkRecord alias "CNAME" target) - domain.aliases); - records-strings = map (record: - "INSERT INTO records (domain_id, name, type, content) SELECT id, '${record.name}', '${record.type}', '${record.content}' FROM domains WHERE name='${domain-name}';") - domain-records; - in pkgs.writeText "initialize-${domain-name}.sql" '' - INSERT INTO domains (name, master, type, notified_serial) VALUES ('${domain-name}', '${host-ip}', 'MASTER', '${toString config.instance.build-timestamp}'); - ${concatStringsSep "\n" records-strings} - ''; + initialize-domain-sql = domain: + let + domain-name = domain.domain; + host-ip = pkgs.lib.network.host-ipv4 config hostname; + ipv6-net = net: (builtins.match ":" net) != null; + ipv4-net = net: !(ipv6-net net); + domain-records = [ + (mkRecord domain-name "SOA" + "ns1.${domain-name} hostmaster.${domain-name} ${ + toString config.instance.build-timestamp + } 10800 3600 1209600 3600") + (mkRecord "_dmark.${domain-name}" "TXT" '' + "v=DMARC1; p=reject; rua=mailto:${domain.admin}; ruf=mailto:${domain.admin}; fo=1;"'') + (mkRecord domain-name "NS" "ns1.${domain-name}") + (mkRecord domain-name "TXT" (let + networks = config.instance.local-networks; + v4-nets = map (net: "ip4:${net}") (filter ipv4-net networks); + v6-nets = map (net: "ip6:${net}") (filter ipv6-net networks); + networks-string = concatStringsSep " " (v4-nets ++ v6-nets); + in ''"v=spf1 mx ${networks-string} -all"'')) + (mkRecord "ns1.${domain-name}" "A" host-ip) + (mkRecord domain-name "A" host-ip) + ] ++ (optional (domain.gssapi-realm != null) + (mkRecord "_kerberos.${domain-name}" "TXT" ''"domain.gssapi-realm"'')) + ++ (mapAttrsToList (alias: target: mkRecord alias "CNAME" target) + domain.aliases); + records-strings = map (record: + "INSERT INTO records (domain_id, name, type, content) SELECT id, '${record.name}', '${record.type}', '${record.content}' FROM domains WHERE name='${domain-name}';") + domain-records; + in pkgs.writeText "initialize-${domain-name}.sql" '' + INSERT INTO domains (name, master, type, notified_serial) VALUES ('${domain-name}', '${host-ip}', 'MASTER', '${ + toString config.instance.build-timestamp + }'); + ${concatStringsSep "\n" records-strings} + ''; - initialize-domain-script = domain: let - domain-name = domain.domain; - in pkgs.writeShellScript "initialize-${domain-name}.sh" '' - if [ "$( psql -tAc "SELECT id FROM domains WHERE name='${domain-name}'" )" ]; then - logger "${domain-name} already initialized, skipping" - exit 0 - else - logger "initializing ${domain-name} in powerdns database" - psql -f ${initialize-domain-sql domain} - fi - ''; + initialize-domain-script = domain: + let domain-name = domain.domain; + in pkgs.writeShellScript "initialize-${domain-name}.sh" '' + if [ "$( psql -tAc "SELECT id FROM domains WHERE name='${domain-name}'" )" ]; then + logger "${domain-name} already initialized, skipping" + exit 0 + else + logger "initializing ${domain-name} in powerdns database" + psql -f ${initialize-domain-sql domain} + fi + ''; domainOpts = { name, ... }: { options = with types; { @@ -111,7 +117,7 @@ let aliases = mkOption { type = attrsOf str; description = "Map of alias to authoritative hostname for this domain."; - default = {}; + default = { }; }; gssapi-realm = mkOption { @@ -152,7 +158,7 @@ in { listen-v6-addresses = mkOption { type = listOf str; description = "List of IPv6 addresses on which to listen."; - default = []; + default = [ ]; }; domains = mkOption { @@ -238,32 +244,34 @@ in { systemd = let pgpass-file = "${runtime-dir}/pgpass"; - initialize-jobs = mapAttrs' (_: domain: let - domain-name = domain.domain; - in nameValuePair "powerdns-initialize-${domain-name}" { - description = "Initialize the ${domain-name} domain"; - requires = [ "powerdns-initialize-db.service" "powerdns-generate-pgpass.service" ]; - after = [ "powerdns-initialize-db.service" "powerdns-generate-pgpass.service" ]; - requiredBy = [ "powerdns.service" ]; - wantedBy = [ "powerdns.service" ]; - before = [ "powerdns.service" ]; - environment = { - PGHOST = cfg.database.host; - PGUSER = cfg.database.user; - PGDATABASE = cfg.database.database; - PGPORT = toString cfg.database.port; - PGSSLMODE= "require"; - PGPASSFILE = pgpass-file; - }; - path = with pkgs; [ postgresql util-linux ]; - serviceConfig = { - ExecStart = initialize-domain-script domain; - }; - }) cfg.domains; + initialize-jobs = mapAttrs' (_: domain: + let domain-name = domain.domain; + in nameValuePair "powerdns-initialize-${domain-name}" { + description = "Initialize the ${domain-name} domain"; + requires = [ + "powerdns-initialize-db.service" + "powerdns-generate-pgpass.service" + ]; + after = [ + "powerdns-initialize-db.service" + "powerdns-generate-pgpass.service" + ]; + requiredBy = [ "powerdns.service" ]; + wantedBy = [ "powerdns.service" ]; + before = [ "powerdns.service" ]; + environment = { + PGHOST = cfg.database.host; + PGUSER = cfg.database.user; + PGDATABASE = cfg.database.database; + PGPORT = toString cfg.database.port; + PGSSLMODE = "require"; + PGPASSFILE = pgpass-file; + }; + path = with pkgs; [ postgresql util-linux ]; + serviceConfig = { ExecStart = initialize-domain-script domain; }; + }) cfg.domains; in { - tmpfiles.rules = [ - "d ${runtime-dir} 0750 ${cfg.user} ${cfg.group} - -" - ]; + tmpfiles.rules = [ "d ${runtime-dir} 0750 ${cfg.user} ${cfg.group} - -" ]; services = initialize-jobs // { powerdns-generate-pgpass = { @@ -285,7 +293,7 @@ in { PGUSER = cfg.database.user; PGDATABASE = cfg.database.database; PGPORT = toString cfg.database.port; - PGSSLMODE= "require"; + PGSSLMODE = "require"; PGPASSFILE = pgpass-file; }; serviceConfig = { @@ -297,16 +305,20 @@ in { psql -f ${pkgs.powerdns}/share/doc/pdns/schema.pgsql.sql fi ''; + # Wait until posgresql is available before starting + ExecStartPre = + pkgs.writeShellScript "ensure-postgresql-running.sh" '' + while [ ! "$( psql -tAc "SELECT 1" )" ]; do + ${pkgs.coreutils}/bin/sleep 3 + done + ''; }; }; powerdns = { description = "PowerDNS nameserver."; requires = [ "powerdns-config-generator.service" ]; - after = [ - "network.target" - "powerdns-config-generator.service" - ]; + after = [ "network.target" "powerdns-config-generator.service" ]; wantedBy = [ "multi-user.target" ]; path = with pkgs; [ powerdns postgresql util-linux ]; serviceConfig = { @@ -318,35 +330,33 @@ in { PASSWORD=$( cat ${cfg.database.password-file} | tr -d '\n') sed -e "s/__PASSWORD__/$PASSWORD/" ${gpgsql-template} > $TARGET ''; - ExecStart = pkgs.writeShellScript "launch-powerdns.sh" (concatStringsSep " " [ - "${pkgs.powerdns}/bin/pdns_server" - "--setuid=${cfg.user}" - "--setgid=${cfg.group}" - "--chroot=${runtime-dir}" - "--daemon=no" - "--guardian=no" - "--write-pid=no" - "--config-dir=${pdns-config-dir}" - ]); + ExecStart = pkgs.writeShellScript "launch-powerdns.sh" + (concatStringsSep " " [ + "${pkgs.powerdns}/bin/pdns_server" + "--setuid=${cfg.user}" + "--setgid=${cfg.group}" + "--chroot=${runtime-dir}" + "--daemon=no" + "--guardian=no" + "--write-pid=no" + "--config-dir=${pdns-config-dir}" + ]); ExecStartPost = pkgs.writeShellScript "powerdns-secure-zones.sh" - (concatStringsSep "\n" - (mapAttrsToList - (_: domain: '' - DNSINFO=$(${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} show-zone ${domain.domain}) - if [[ "x$DNSINFO" =~ "xNo such zone in the database" ]]; then - logger "zone ${domain.domain} does not exist in powerdns database" - elif [[ "x$DNSINFO" =~ "xZone is not actively secured" ]]; then - logger "securing zone ${domain.domain} in powerdns database" - ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} - elif [[ "x$DNSINFO" =~ "xNo keys for zone" ]]; then - logger "securing zone ${domain.domain} in powerdns database" - ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} - else - logger "not securing zone ${domain.domain} in powerdns database" - fi - ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} rectify-zone ${domain.domain} - '') - cfg.domains)); + (concatStringsSep "\n" (mapAttrsToList (_: domain: '' + DNSINFO=$(${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} show-zone ${domain.domain}) + if [[ "x$DNSINFO" =~ "xNo such zone in the database" ]]; then + logger "zone ${domain.domain} does not exist in powerdns database" + elif [[ "x$DNSINFO" =~ "xZone is not actively secured" ]]; then + logger "securing zone ${domain.domain} in powerdns database" + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} + elif [[ "x$DNSINFO" =~ "xNo keys for zone" ]]; then + logger "securing zone ${domain.domain} in powerdns database" + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} secure-zone ${domain.domain} + else + logger "not securing zone ${domain.domain} in powerdns database" + fi + ${pkgs.powerdns}/bin/pdnsutil --config-dir=${pdns-config-dir} rectify-zone ${domain.domain} + '') cfg.domains)); }; }; }; diff --git a/lib/fudo/prometheus.nix b/lib/fudo/prometheus.nix index c604cd4..87aabd8 100644 --- a/lib/fudo/prometheus.nix +++ b/lib/fudo/prometheus.nix @@ -1,8 +1,7 @@ { config, lib, pkgs, ... }: with lib; -let - cfg = config.fudo.metrics.prometheus; +let cfg = config.fudo.metrics.prometheus; in { @@ -19,26 +18,24 @@ in { postfix = [ "postfix._metrics._tcp.my-domain.com" ]; }; default = { - dovecot = []; - node = []; - postfix = []; - rspamd = []; + dovecot = [ ]; + node = [ ]; + postfix = [ ]; + rspamd = [ ]; }; }; static-targets = mkOption { type = attrsOf (listOf str); description = '' - A map of exporter type to a list of host:ports from which to collect metrics. - ''; - example = { - node = [ "my-host.my-domain:1111" ]; - }; + A map of exporter type to a list of host:ports from which to collect metrics. + ''; + example = { node = [ "my-host.my-domain:1111" ]; }; default = { - dovecot = []; - node = []; - postfix = []; - rspamd = []; + dovecot = [ ]; + node = [ ]; + postfix = [ ]; + rspamd = [ ]; }; }; @@ -47,7 +44,7 @@ in { description = '' A list of explicit docker targets from which to gather node data. ''; - default = []; + default = [ ]; }; push-url = mkOption { @@ -86,6 +83,15 @@ in { "d ${cfg.state-directory} 0700 ${config.systemd.services.prometheus.serviceConfig.User} - - -" ]; + fileSystems = + let state-dir = "/var/lib/${config.services.prometheus.stateDir}"; + in mkIf (cfg.state-directory != state-dir) { + ${state-dir} = { + device = cfg.state-directory; + options = [ "bind" ]; + }; + }; + services.nginx = { enable = true; recommendedOptimisation = true; @@ -93,17 +99,18 @@ in { virtualHosts = { "${cfg.hostname}" = { - enableACME = ! cfg.private-network; - forceSSL = ! cfg.private-network; + enableACME = !cfg.private-network; + forceSSL = !cfg.private-network; locations."/" = { proxyPass = "http://127.0.0.1:9090"; - extraConfig = let - local-networks = config.instance.local-networks; + extraConfig = let local-networks = config.instance.local-networks; in "${optionalString ((length local-networks) > 0) - (concatStringsSep "\n" - (map (network: "allow ${network};") local-networks)) + "\ndeny all;"}"; + (concatStringsSep "\n" + (map (network: "allow ${network};") local-networks)) + '' + + deny all;''}"; }; }; }; @@ -124,19 +131,22 @@ in { honor_labels = false; scheme = if cfg.private-network then "http" else "https"; metrics_path = "/metrics/${type}"; - dns_sd_configs = if (hasAttr type cfg.service-discovery-dns) then - [ { names = cfg.service-discovery-dns.${type}; } ] else []; - static_configs = if (hasAttr type cfg.static-targets) then - [ { targets = cfg.static-targets.${type}; } ] else []; + dns_sd_configs = if (hasAttr type cfg.service-discovery-dns) then [{ + names = cfg.service-discovery-dns.${type}; + }] else + [ ]; + static_configs = if (hasAttr type cfg.static-targets) then [{ + targets = cfg.static-targets.${type}; + }] else + [ ]; }; - in map make-job ["docker" "node" "dovecot" "postfix" "rspamd"]; + in map make-job [ "docker" "node" "dovecot" "postfix" "rspamd" ]; pushgateway = { enable = if (cfg.push-url != null) then true else false; web = { - external-url = if cfg.push-url == null then - cfg.push-address else - cfg.push-url; + external-url = + if cfg.push-url == null then cfg.push-address else cfg.push-url; listen-address = cfg.push-address; }; }; diff --git a/lib/fudo/secrets.nix b/lib/fudo/secrets.nix index 2382693..cbb1737 100644 --- a/lib/fudo/secrets.nix +++ b/lib/fudo/secrets.nix @@ -16,7 +16,8 @@ let decrypt-script = { secret-name, source-file, target-host, target-file , host-master-key, user, group, permissions }: - pkgs.writeShellScript "decrypt-fudo-secret-${target-host}-${secret-name}.sh" '' + pkgs.writeShellScript + "decrypt-fudo-secret-${target-host}-${secret-name}.sh" '' rm -f ${target-file} touch ${target-file} chown ${user}:${group} ${target-file} @@ -24,27 +25,33 @@ let # NOTE: silly hack because sometimes age leaves a blank line # Only include lines with at least one non-space character SRC=$(mktemp fudo-secret-${target-host}-${secret-name}.XXXXXXXX) - cat ${encrypt-on-disk { + cat ${ + encrypt-on-disk { inherit secret-name source-file target-host; target-pubkey = host-master-key.public-key; - }} | grep "[^ ]" > $SRC + } + } | grep "[^ ]" > $SRC age -d -i ${host-master-key.key-path} -o ${target-file} $SRC rm -f $SRC ''; secret-service = target-host: secret-name: { source-file, target-file, user, group, permissions, ... }: { - description = "decrypt secret ${secret-name} for ${target-host}."; + description = + "decrypt secret ${secret-name} at ${target-host}:${target-file}."; wantedBy = [ cfg.secret-target ]; before = [ cfg.secret-target ]; serviceConfig = { - Type = "oneshot"; - ExecStart = let - host-master-key = config.fudo.hosts.${target-host}.master-key; - in decrypt-script { - inherit secret-name source-file target-host target-file host-master-key - user group permissions; - }; + Type = "simple"; + RemainAfterExit = true; + ExecStart = + let host-master-key = config.fudo.hosts.${target-host}.master-key; + in decrypt-script { + inherit secret-name source-file target-host target-file + host-master-key user group permissions; + }; + ExecStop = pkgs.writeShellScript "fudo-remove-${secret-name}-secret.sh" + "rm -f ${target-file}"; }; path = [ pkgs.age ]; }; @@ -52,8 +59,10 @@ let secretOpts = { name, ... }: { options = with types; { source-file = mkOption { - type = path; # CAREFUL: this will copy the file to nixstore...keep on deploy host - description = "File from which to load the secret. If unspecified, a random new password will be generated."; + type = + path; # CAREFUL: this will copy the file to nixstore...keep on deploy host + description = + "File from which to load the secret. If unspecified, a random new password will be generated."; default = "${generate-secret name}/passwd"; }; @@ -72,7 +81,7 @@ let group = mkOption { type = str; description = "Group (on target host) to which the file will belong."; - default = "nogroup"; + default = "root"; }; permissions = mkOption { @@ -84,7 +93,7 @@ let metadata = mkOption { type = attrsOf anything; description = "Arbitrary metadata associated with this secret."; - default = {}; + default = { }; }; service = mkOption { @@ -97,32 +106,34 @@ let nix-build-users = let usernames = attrNames config.users.users; in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null) - usernames; + usernames; - generate-secret = name: pkgs.stdenv.mkDerivation { - name = "${name}-generated-passwd"; + generate-secret = name: + pkgs.stdenv.mkDerivation { + name = "${name}-generated-passwd"; - phases = [ "installPhase" ]; + phases = [ "installPhase" ]; - buildInputs = with pkgs; [ pwgen ]; + buildInputs = with pkgs; [ pwgen ]; - buildPhase = '' - echo "${name}-${config.instance.build-timestamp}" >> file.txt - pwgen --secure --symbols --num-passwords=1 --sha1=file.txt 40 > passwd - rm -f file.txt - ''; + buildPhase = '' + echo "${name}-${config.instance.build-timestamp}" >> file.txt + pwgen --secure --symbols --num-passwords=1 --sha1=file.txt 40 > passwd + rm -f file.txt + ''; - installPhase = '' - mkdir $out - mv passwd $out/passwd - ''; - }; + installPhase = '' + mkdir $out + mv passwd $out/passwd + ''; + }; in { options.fudo.secrets = with types; { enable = mkOption { type = bool; - description = "Include secrets in the build (disable when secrets are unavailable)"; + description = + "Include secrets in the build (disable when secrets are unavailable)"; default = true; }; @@ -175,9 +186,7 @@ in { config = mkIf cfg.enable { users.groups = { - ${cfg.secret-group} = { - members = cfg.secret-users ++ nix-build-users; - }; + ${cfg.secret-group} = { members = cfg.secret-users ++ nix-build-users; }; }; systemd = let @@ -189,25 +198,24 @@ in { { }; host-secret-services = let - head-or-null = lst: if (lst == []) then null else head lst; + head-or-null = lst: if (lst == [ ]) then null else head lst; strip-service = service-name: - head-or-null - (builtins.match "^(.+)[.]service$" service-name); + head-or-null (builtins.match "^(.+)[.]service$" service-name); in mapAttrs' (secret: secretOpts: (nameValuePair (strip-service secretOpts.service) (secret-service hostname secret secretOpts))) host-secrets; trace-all = obj: builtins.trace obj obj; - host-secret-paths = mapAttrsToList - (secret: secretOpts: - let perms = if secretOpts.group != "nobody" then "550" else "500"; - in "d ${dirOf secretOpts.target-file} ${perms} ${secretOpts.user} ${secretOpts.group} - -") - host-secrets; + host-secret-paths = mapAttrsToList (secret: secretOpts: + let perms = if secretOpts.group != "nobody" then "550" else "500"; + in "d ${ + dirOf secretOpts.target-file + } ${perms} ${secretOpts.user} ${secretOpts.group} - -") host-secrets; build-secret-paths = map (path: "d '${path}' - root ${cfg.secret-group} - -") - cfg.secret-paths; + cfg.secret-paths; in { tmpfiles.rules = host-secret-paths ++ build-secret-paths; @@ -231,7 +239,8 @@ in { strip-ext = filename: head (builtins.match "^(.+)[.]target$" filename); in { ${strip-ext cfg.secret-target} = { - description = "Target indicating that all Fudo secrets are available."; + description = + "Target indicating that all Fudo secrets are available."; wantedBy = [ "default.target" ]; }; }; diff --git a/lib/lib/filesystem.nix b/lib/lib/filesystem.nix index ab7b20a..02ce3cf 100644 --- a/lib/lib/filesystem.nix +++ b/lib/lib/filesystem.nix @@ -2,33 +2,34 @@ with pkgs.lib; let - head-or-null = lst: if (lst == []) then null else head lst; + head-or-null = lst: if (lst == [ ]) then null else head lst; is-regular-file = filename: type: type == "regular" || type == "link"; regular-files = path: filterAttrs is-regular-file (builtins.readDir path); - matches-ext = ext: filename: type: (builtins.match ".+[.]${ext}$" filename) != null; + matches-ext = ext: filename: type: + (builtins.match ".+[.]${ext}$" filename) != null; is-nix-file = matches-ext "nix"; - strip-ext = ext: filename: head-or-null (builtins.match "(.+)[.]${ext}$" filename); + strip-ext = ext: filename: + head-or-null (builtins.match "(.+)[.]${ext}$" filename); get-ext = filename: head-or-null (builtins.match "^.+[.](.+)$" filename); hostname-from-file = filename: strip-ext "nix"; - nix-files = path: - attrNames - (filterAttrs is-nix-file - (filterAttrs is-regular-file - (builtins.readDir path))); - basename-to-file-map = path: let - files = nix-files path; - in listToAttrs - (map (file: - nameValuePair (strip-ext "nix" file) - (path + "/${file}")) + ext-files = ext: path: + attrNames (filterAttrs (matches-ext ext) + (filterAttrs is-regular-file (builtins.readDir path))); + + nix-files = ext-files "nix"; + + basename-to-file-map = path: + let files = nix-files path; + in listToAttrs + (map (file: nameValuePair (strip-ext "nix" file) (path + "/${file}")) files); import-by-basename = path: - mapAttrs (attr: attr-file: import attr-file) - (basename-to-file-map path); + mapAttrs (attr: attr-file: import attr-file) (basename-to-file-map path); list-nix-files = nix-files; in { - inherit basename-to-file-map import-by-basename list-nix-files; + inherit strip-ext basename-to-file-map import-by-basename list-nix-files + ext-files; }