Working secrets implementation
This commit is contained in:
		
							parent
							
								
									951ffa3ff9
								
							
						
					
					
						commit
						353936d509
					
				@ -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;
 | 
			
		||||
          }
 | 
			
		||||
        ];
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
        '';
 | 
			
		||||
      };
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										112
									
								
								lib/fudo/secrets.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								lib/fudo/secrets.nix
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -196,6 +196,7 @@ in {
 | 
			
		||||
            openssh.authorizedKeys.keys =
 | 
			
		||||
              concatMap (hostOpts: hostOpts.build-pubkeys)
 | 
			
		||||
              (attrValues site-hosts);
 | 
			
		||||
            shell = pkgs.bash;
 | 
			
		||||
          };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user