diff --git a/config/host-config/limina.nix b/config/host-config/limina.nix index 5fec051..6d6f80a 100644 --- a/config/host-config/limina.nix +++ b/config/host-config/limina.nix @@ -64,13 +64,23 @@ in { network-definition = config.fudo.networks.${domain-name}; }; + secrets = { + backplane-client-limina-passwd = { + source-file = /srv/secrets/backplane-client/limina.passwd; + target-file = "/srv/backplane/dns/client.passwd"; + target-host = "limina"; + user = config.fudo.client.dns.user; + }; + }; + client.dns = { enable = true; ipv4 = true; ipv6 = true; user = "fudo-client"; external-interface = "enp1s0"; - password-file = "/srv/client/secure/client.passwd"; + password-file = + config.fudo.secrets.backplane-client-limina-passwd.target-file; }; garbage-collector = { @@ -118,28 +128,6 @@ in { }; }; - services.nginx = { - enable = true; - - recommendedOptimisation = true; - recommendedGzipSettings = true; - recommendedProxySettings = true; - - virtualHosts = { - "dns-hole.${domain-name}" = { - serverAliases = [ - "pihole.${domain-name}" - "hole.${domain-name}" - "pihole" - "dns-hole" - "hole" - ]; - - locations."/" = { proxyPass = "http://127.0.0.1:3080"; }; - }; - }; - }; - # Support for statelessness environment.etc = { nixos.source = "/state/nixos"; @@ -173,18 +161,87 @@ in { "L /etc/ssh/ssh_host_rsa_key - - - - /state/ssh/ssh_host_rsa_key" ]; - services.openssh = { - hostKeys = [ - { - path = "/state/ssh/ssh_host_ed25519_key"; - type = "ed25519"; - } - { - path = "/state/ssh/ssh_host_rsa_key"; - type = "rsa"; - bits = 4096; - } - ]; + security.acme.certs."sea-camera.fudo.link".email = "niten@fudo.org"; + + services = { + nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + + virtualHosts = { + "dns-hole.${domain-name}" = { + serverAliases = [ + "pi-hole.${domain-name}" + "pihole.${domain-name}" + "hole.${domain-name}" + "pi-hole" + "pihole" + "dns-hole" + "hole" + ]; + + locations."/" = { proxyPass = "http://127.0.0.1:3080"; }; + }; + + "sea-camera.fudo.link" = { + enableACME = true; + forceSSL = true; + + locations."/" = { + proxyPass = "http://panopticon.sea.fudo.org"; + + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-By $server_addr:$server_port; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + }; + }; + + # "sea-camera-od.fudo.link" = { + # enableACME = true; + # forceSSL = true; + + # locations."/" = { + # proxyPass = "http://panopticon-od.sea.fudo.org"; + + # extraConfig = '' + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "Upgrade"; + + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-By $server_addr:$server_port; + # proxy_set_header X-Forwarded-For $remote_addr; + # proxy_set_header X-Forwarded-Proto $scheme; + # ''; + # }; + # }; + }; + }; + + openssh = { + hostKeys = [ + { + path = "/state/ssh/ssh_host_ed25519_key"; + type = "ed25519"; + } + { + path = "/state/ssh/ssh_host_rsa_key"; + type = "rsa"; + bits = 4096; + } + ]; + }; }; }; } diff --git a/lib/default.nix b/lib/default.nix index d20bff6..e8f0f03 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -32,6 +32,7 @@ with lib; { ./fudo/password.nix ./fudo/postgres.nix ./fudo/prometheus.nix + ./fudo/secrets.nix ./fudo/secure-dns-proxy.nix ./fudo/sites.nix ./fudo/slynk.nix diff --git a/lib/fudo/client/dns.nix b/lib/fudo/client/dns.nix index a1db07e..ff62fc1 100644 --- a/lib/fudo/client/dns.nix +++ b/lib/fudo/client/dns.nix @@ -4,6 +4,11 @@ with lib; let cfg = config.fudo.client.dns; + ssh-key-files = + map (host-key: host-key.path) config.services.openssh.hostKeys; + + ssh-key-args = concatStringsSep " " (map (file: "-f ${file}") ssh-key-files); + in { options.fudo.client.dns = { enable = mkEnableOption "Enable Fudo DynDNS Client."; @@ -46,23 +51,24 @@ in { frequency = mkOption { type = types.str; - description = "Frequency at which to report the local IP(s) to backplane."; + description = + "Frequency at which to report the local IP(s) to backplane."; default = "*:0/15"; }; user = mkOption { type = types.str; - description = "User as which to run the client script (must have access to password file)."; + description = + "User as which to run the client script (must have access to password file)."; default = "backplane-dns-client"; }; external-interface = mkOption { type = with types; nullOr str; - description = "Interface with which this host communicates with the larger internet."; + description = + "Interface with which this host communicates with the larger internet."; default = null; }; - - # FIXME: take the relevant SSH package }; config = mkIf cfg.enable { @@ -81,20 +87,16 @@ in { partOf = [ "backplane-dns-client.service" ]; wantedBy = [ "timers.target" ]; requires = [ "network-online.target" ]; - timerConfig = { - OnCalendar = cfg.frequency; - }; + timerConfig = { OnCalendar = cfg.frequency; }; }; services.backplane-dns-client-pw-file = { enable = true; requiredBy = [ "backplane-dns-client.services" ]; reloadIfChanged = true; - serviceConfig = { - Type = "oneshot"; - }; + serviceConfig = { Type = "oneshot"; }; script = '' - chmod 600 ${cfg.password-file} + chmod 400 ${cfg.password-file} chown ${cfg.user} ${cfg.password-file} ''; }; @@ -105,12 +107,20 @@ in { Type = "oneshot"; StandardOutput = "journal"; User = cfg.user; + ExecStart = pkgs.writeShellScript "start-backplane-dns-client.sh" '' + ${pkgs.backplane-dns-client}/bin/backplane-dns-client ${ + optionalString cfg.ipv4 "-4" + } ${optionalString cfg.ipv6 "-6"} ${ + optionalString cfg.sshfp ssh-key-args + } ${ + optionalString (cfg.external-interface != null) + "--interface=${cfg.external-interface}" + } --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file} + ''; }; + # Needed to generate SSH fingerprinst path = [ pkgs.openssh ]; reloadIfChanged = true; - script = '' - ${pkgs.backplane-dns-client}/bin/backplane-dns-client ${optionalString cfg.ipv4 "-4"} ${optionalString cfg.ipv6 "-6"} ${optionalString cfg.sshfp "-f"} ${optionalString (cfg.external-interface != null) "--interface=${cfg.external-interface}"} --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file} - ''; }; }; }; diff --git a/lib/fudo/secrets.nix b/lib/fudo/secrets.nix new file mode 100644 index 0000000..b26f091 --- /dev/null +++ b/lib/fudo/secrets.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + all-secrets = config.fudo.secrets; + + encrypt-on-disk = name: + { target-host, source-file }: + pkgs.stdenv.mkDerivation { + name = "${name}-secret"; + phases = "installPhase"; + buildInputs = [ pkgs.age ]; + installPhase = let key = config.fudo.hosts.${target-host}.ssh-pubkey; + in '' + age -a -r "${key}" -o $out ${source-file} + ''; + }; + + decrypt-script = name: + { source-file, target-host, target-file, decrypt-key, user, group + , permissions }: + pkgs.writeShellScript "decrypt-fudo-secret-${name}.sh" '' + rm -rf ${target-file} + age -d -i ${decrypt-key} -o ${target-file} ${ + encrypt-on-disk name { inherit source-file target-host; } + } + chown ${user}:${group} ${target-file} + chmod ${permissions} ${target-file} + ''; + + secret-service = name: + { source-file, target-host, target-file, user, group, permissions + , key-type ? "ed25519" }: { + description = "decrypt secret ${name} for ${target-host}."; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStartPre = pkgs.writeShellScript "prepare-secrets-dir.sh" '' + TARGET_DIR=$(dirname ${target-file}) + if [[ ! -d "$TARGET_DIR" ]]; then + mkdir -p "$TARGET_DIR" + fi + ''; + ExecStart = let + decrypt-keys = + filter (key: key.type == key-type) config.services.openssh.hostKeys; + decrypt-key = head (map (key: key.path) decrypt-keys); + in decrypt-script name { + inherit source-file target-host target-file decrypt-key user group + permissions; + }; + }; + path = [ pkgs.age ]; + }; + + secretOpts = { ... }: { + options = with types; { + source-file = mkOption { + type = path; + description = "File from which to load the secret."; + }; + + target-host = mkOption { + type = str; + description = + "Host to which the secret belongs (determins SSH key to encrypt)."; + }; + + target-file = mkOption { + type = str; + description = + "Target file on the host; the secret will be decrypted to this file."; + }; + + user = mkOption { + type = str; + description = "User (on target host) to which the file will belong."; + }; + + group = mkOption { + type = str; + description = "Group (on target host) to which the file will belong."; + default = "nogroup"; + }; + + permissions = mkOption { + type = str; + description = "Permissions to set on the target file."; + default = "0400"; + }; + }; + }; + +in { + options.fudo.secrets = with types; + mkOption { + type = attrsOf (submodule secretOpts); + description = "Map of secrets to secret config."; + default = { }; + }; + + config = { + systemd.services = let + hostname = config.instance.hostname; + host-secrets = + filterAttrs (secret: secretOpts: secretOpts.target-host == hostname) + all-secrets; + in mapAttrs' (secret: secretOpts: + (nameValuePair "fudo-secret-${secret}" + (secret-service secret secretOpts))) host-secrets; + }; +} diff --git a/lib/fudo/sites.nix b/lib/fudo/sites.nix index db12e10..5888406 100644 --- a/lib/fudo/sites.nix +++ b/lib/fudo/sites.nix @@ -196,6 +196,7 @@ in { openssh.authorizedKeys.keys = concatMap (hostOpts: hostOpts.build-pubkeys) (attrValues site-hosts); + shell = pkgs.bash; }; }; diff --git a/static/backplane-dns-client/dns-client.rb b/static/backplane-dns-client/dns-client.rb index 0688cdf..11a4503 100644 --- a/static/backplane-dns-client/dns-client.rb +++ b/static/backplane-dns-client/dns-client.rb @@ -43,9 +43,10 @@ OptionParser.new do |opts| options[:ipv6] = true end - opts.on("-f", "--sshfp", - "Register host SSH key fingerprints with the backplane.") do - options[:sshfp] = true + opts.on("-f", "--sshfp=FILE", + "Register host SSH key fingerprints with the backplane.") do |file| + options[:sshfp] = [] if not options[:sshfp] + options[:sshfp] = options[:sshfp] + [file] end end.parse! @@ -217,11 +218,12 @@ def interface_addresses(interface) end end -def host_sshfp - keys = `ssh-keygen -r hostname`.split("\n").map do |k| - k.match(/[0-9] [0-9] [a-fA-F0-9]{32,64}$/)[0] - end - keys.compact +def host_sshfp(keys) + keys.flat_map { |keyfile| + `ssh-keygen -r hostname #{keyfile}`.split("\n") + }.map { |fp| + fp..match(/[0-9] [0-9] [a-fA-F0-9]{32,64}$/)[0] + }.compact end def hostname @@ -275,7 +277,7 @@ begin end if options[:sshfp] - fps = host_sshfp + fps = host_sshfp(options[:sshfp]) if not fps.empty? puts "#{options[:server]}: #{hostname}.#{options[:domain]} IN SSHFP => #{fps}" if client.send_sshfp(fps)