Working secrets implementation
This commit is contained in:
parent
951ffa3ff9
commit
353936d509
@ -64,13 +64,23 @@ in {
|
|||||||
network-definition = config.fudo.networks.${domain-name};
|
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 = {
|
client.dns = {
|
||||||
enable = true;
|
enable = true;
|
||||||
ipv4 = true;
|
ipv4 = true;
|
||||||
ipv6 = true;
|
ipv6 = true;
|
||||||
user = "fudo-client";
|
user = "fudo-client";
|
||||||
external-interface = "enp1s0";
|
external-interface = "enp1s0";
|
||||||
password-file = "/srv/client/secure/client.passwd";
|
password-file =
|
||||||
|
config.fudo.secrets.backplane-client-limina-passwd.target-file;
|
||||||
};
|
};
|
||||||
|
|
||||||
garbage-collector = {
|
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
|
# Support for statelessness
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
nixos.source = "/state/nixos";
|
nixos.source = "/state/nixos";
|
||||||
@ -173,18 +161,87 @@ in {
|
|||||||
"L /etc/ssh/ssh_host_rsa_key - - - - /state/ssh/ssh_host_rsa_key"
|
"L /etc/ssh/ssh_host_rsa_key - - - - /state/ssh/ssh_host_rsa_key"
|
||||||
];
|
];
|
||||||
|
|
||||||
services.openssh = {
|
security.acme.certs."sea-camera.fudo.link".email = "niten@fudo.org";
|
||||||
hostKeys = [
|
|
||||||
{
|
services = {
|
||||||
path = "/state/ssh/ssh_host_ed25519_key";
|
nginx = {
|
||||||
type = "ed25519";
|
enable = true;
|
||||||
}
|
recommendedGzipSettings = true;
|
||||||
{
|
recommendedOptimisation = true;
|
||||||
path = "/state/ssh/ssh_host_rsa_key";
|
recommendedProxySettings = true;
|
||||||
type = "rsa";
|
|
||||||
bits = 4096;
|
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/password.nix
|
||||||
./fudo/postgres.nix
|
./fudo/postgres.nix
|
||||||
./fudo/prometheus.nix
|
./fudo/prometheus.nix
|
||||||
|
./fudo/secrets.nix
|
||||||
./fudo/secure-dns-proxy.nix
|
./fudo/secure-dns-proxy.nix
|
||||||
./fudo/sites.nix
|
./fudo/sites.nix
|
||||||
./fudo/slynk.nix
|
./fudo/slynk.nix
|
||||||
|
@ -4,6 +4,11 @@ with lib;
|
|||||||
let
|
let
|
||||||
cfg = config.fudo.client.dns;
|
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 {
|
in {
|
||||||
options.fudo.client.dns = {
|
options.fudo.client.dns = {
|
||||||
enable = mkEnableOption "Enable Fudo DynDNS Client.";
|
enable = mkEnableOption "Enable Fudo DynDNS Client.";
|
||||||
@ -46,23 +51,24 @@ in {
|
|||||||
|
|
||||||
frequency = mkOption {
|
frequency = mkOption {
|
||||||
type = types.str;
|
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";
|
default = "*:0/15";
|
||||||
};
|
};
|
||||||
|
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
type = types.str;
|
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";
|
default = "backplane-dns-client";
|
||||||
};
|
};
|
||||||
|
|
||||||
external-interface = mkOption {
|
external-interface = mkOption {
|
||||||
type = with types; nullOr str;
|
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;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
# FIXME: take the relevant SSH package
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
@ -81,20 +87,16 @@ in {
|
|||||||
partOf = [ "backplane-dns-client.service" ];
|
partOf = [ "backplane-dns-client.service" ];
|
||||||
wantedBy = [ "timers.target" ];
|
wantedBy = [ "timers.target" ];
|
||||||
requires = [ "network-online.target" ];
|
requires = [ "network-online.target" ];
|
||||||
timerConfig = {
|
timerConfig = { OnCalendar = cfg.frequency; };
|
||||||
OnCalendar = cfg.frequency;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.backplane-dns-client-pw-file = {
|
services.backplane-dns-client-pw-file = {
|
||||||
enable = true;
|
enable = true;
|
||||||
requiredBy = [ "backplane-dns-client.services" ];
|
requiredBy = [ "backplane-dns-client.services" ];
|
||||||
reloadIfChanged = true;
|
reloadIfChanged = true;
|
||||||
serviceConfig = {
|
serviceConfig = { Type = "oneshot"; };
|
||||||
Type = "oneshot";
|
|
||||||
};
|
|
||||||
script = ''
|
script = ''
|
||||||
chmod 600 ${cfg.password-file}
|
chmod 400 ${cfg.password-file}
|
||||||
chown ${cfg.user} ${cfg.password-file}
|
chown ${cfg.user} ${cfg.password-file}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@ -105,12 +107,20 @@ in {
|
|||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
StandardOutput = "journal";
|
StandardOutput = "journal";
|
||||||
User = cfg.user;
|
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 ];
|
path = [ pkgs.openssh ];
|
||||||
reloadIfChanged = true;
|
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 =
|
openssh.authorizedKeys.keys =
|
||||||
concatMap (hostOpts: hostOpts.build-pubkeys)
|
concatMap (hostOpts: hostOpts.build-pubkeys)
|
||||||
(attrValues site-hosts);
|
(attrValues site-hosts);
|
||||||
|
shell = pkgs.bash;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -43,9 +43,10 @@ OptionParser.new do |opts|
|
|||||||
options[:ipv6] = true
|
options[:ipv6] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
opts.on("-f", "--sshfp",
|
opts.on("-f", "--sshfp=FILE",
|
||||||
"Register host SSH key fingerprints with the backplane.") do
|
"Register host SSH key fingerprints with the backplane.") do |file|
|
||||||
options[:sshfp] = true
|
options[:sshfp] = [] if not options[:sshfp]
|
||||||
|
options[:sshfp] = options[:sshfp] + [file]
|
||||||
end
|
end
|
||||||
end.parse!
|
end.parse!
|
||||||
|
|
||||||
@ -217,11 +218,12 @@ def interface_addresses(interface)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def host_sshfp
|
def host_sshfp(keys)
|
||||||
keys = `ssh-keygen -r hostname`.split("\n").map do |k|
|
keys.flat_map { |keyfile|
|
||||||
k.match(/[0-9] [0-9] [a-fA-F0-9]{32,64}$/)[0]
|
`ssh-keygen -r hostname #{keyfile}`.split("\n")
|
||||||
end
|
}.map { |fp|
|
||||||
keys.compact
|
fp..match(/[0-9] [0-9] [a-fA-F0-9]{32,64}$/)[0]
|
||||||
|
}.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def hostname
|
def hostname
|
||||||
@ -275,7 +277,7 @@ begin
|
|||||||
end
|
end
|
||||||
|
|
||||||
if options[:sshfp]
|
if options[:sshfp]
|
||||||
fps = host_sshfp
|
fps = host_sshfp(options[:sshfp])
|
||||||
if not fps.empty?
|
if not fps.empty?
|
||||||
puts "#{options[:server]}: #{hostname}.#{options[:domain]} IN SSHFP => #{fps}"
|
puts "#{options[:server]}: #{hostname}.#{options[:domain]} IN SSHFP => #{fps}"
|
||||||
if client.send_sshfp(fps)
|
if client.send_sshfp(fps)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user