Merged upstream
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./dns.nix
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.backplane.dns;
|
||||
|
||||
dns = import ../../../lib/dns.nix { inherit lib; };
|
||||
|
||||
backup-directory = "/var/lib/fudo/backplane/dns";
|
||||
|
||||
powerdns-home = "/var/lib/powerdns";
|
||||
|
||||
powerdns-conf-dir = "${powerdns-home}/conf.d";
|
||||
|
||||
backplaneOpts = { ... }: {
|
||||
options = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
description = "Hostname of the backplane jabber server.";
|
||||
};
|
||||
|
||||
role = mkOption {
|
||||
type = types.str;
|
||||
description = "Backplane XMPP role name for the DNS server.";
|
||||
default = "service-dns";
|
||||
};
|
||||
|
||||
password-file = mkOption {
|
||||
type = types.str;
|
||||
description = "File containing XMPP password for backplane role.";
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = with types; submodule databaseOpts;
|
||||
description = "Database settings for backplane server.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
databaseOpts = { ... }: {
|
||||
options = {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
description = "Hostname or IP of the PostgreSQL server.";
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = types.str;
|
||||
description = "Database to use for DNS backplane.";
|
||||
default = "backplane_dns";
|
||||
};
|
||||
|
||||
username = mkOption {
|
||||
type = types.str;
|
||||
description = "Database user for DNS backplane.";
|
||||
default = "backplane_dns";
|
||||
};
|
||||
|
||||
password-file = mkOption {
|
||||
type = types.str;
|
||||
description = "File containing password for database user.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
lisp-libs = [];
|
||||
|
||||
launchScript = pkgs.writeText "launch-backplane-dns.lisp" ''
|
||||
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
|
||||
(ql:quickload :backplane-dns)
|
||||
(backplane-dns:start-listener-with-env)
|
||||
(loop (sleep 600))
|
||||
'';
|
||||
|
||||
in {
|
||||
options.fudo.backplane.dns = {
|
||||
enable = mkEnableOption "Enable backplane dynamic DNS server.";
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
description = "Port on which to serve authoritative DNS requests.";
|
||||
default = 53;
|
||||
};
|
||||
|
||||
listen-addresses = mkOption {
|
||||
type = with types; listOf str;
|
||||
description = "IP addresses on which to listen for dns requests.";
|
||||
default = [ "0.0.0.0" ];
|
||||
};
|
||||
|
||||
required-services = mkOption {
|
||||
type = with types; listOf str;
|
||||
description = "A list of services required before the DNS server can start.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
description = "User as which to run DNS backplane listener service.";
|
||||
default = "backplane-dns";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
description = "Group as which to run DNS backplane listener service.";
|
||||
default = "backplane-dns";
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = with types; submodule databaseOpts;
|
||||
description = "Database settings for the DNS server.";
|
||||
};
|
||||
|
||||
backplane = mkOption {
|
||||
type = with types; submodule backplaneOpts;
|
||||
description = "Backplane Jabber settings for the DNS server.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users = {
|
||||
users = {
|
||||
"${cfg.user}" = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
createHome = true;
|
||||
home = "/var/home/${cfg.user}";
|
||||
};
|
||||
backplane-powerdns = {
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
groups = {
|
||||
"${cfg.group}" = {
|
||||
members = [cfg.user];
|
||||
};
|
||||
backplane-powerdns = {
|
||||
members = [ "backplane-powerdns" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd = {
|
||||
targets = {
|
||||
backplane-dns = {
|
||||
description = "Fudo DNS backplane services.";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
services = {
|
||||
|
||||
backplane-powerdns = let
|
||||
configDir = pkgs.writeTextDir "pdns.conf" ''
|
||||
local-address=${lib.concatStringsSep ", " cfg.listen-addresses}
|
||||
local-port=${toString cfg.port}
|
||||
launch=
|
||||
include-dir=${powerdns-conf-dir}/
|
||||
'';
|
||||
|
||||
psql-user = config.services.postgresql.superUser;
|
||||
|
||||
in {
|
||||
unitConfig.Documentation = "man:pdns_server(1) man:pdns_control(1)";
|
||||
description = "Backplane PowerDNS name server";
|
||||
requires = [
|
||||
"postgresql.service"
|
||||
"backplane-dns-config-generator.service"
|
||||
"backplane-dns.target"
|
||||
];
|
||||
after = [
|
||||
"network.target"
|
||||
"postgresql.service"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = with pkgs; [ postgresql ];
|
||||
|
||||
serviceConfig = {
|
||||
Restart="on-failure";
|
||||
RestartSec="10";
|
||||
StartLimitInterval="0";
|
||||
PrivateDevices=true;
|
||||
# CapabilityBoundingSet="CAP_CHOWN CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_SYS_CHROOT";
|
||||
# NoNewPrivileges=true;
|
||||
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${powerdns-home}";
|
||||
ExecStart = "${pkgs.powerdns}/bin/pdns_server --setuid=backplane-powerdns --setgid=backplane-powerdns --chroot=${powerdns-home} --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${configDir}";
|
||||
ProtectSystem="full";
|
||||
# ProtectHome=true;
|
||||
RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
|
||||
};
|
||||
};
|
||||
|
||||
backplane-dns-config-generator = {
|
||||
description = "Generate postgres configuration for backplane DNS server.";
|
||||
requiredBy = [ "backplane-powerdns.service" ];
|
||||
requires = cfg.required-services;
|
||||
serviceConfig.Type = "oneshot";
|
||||
restartIfChanged = true;
|
||||
partOf = [ "backplane-dns.target" ];
|
||||
|
||||
preStart = ''
|
||||
mkdir -p ${powerdns-conf-dir}
|
||||
chown backplane-powerdns:backplane-powerdns ${powerdns-conf-dir}
|
||||
'';
|
||||
|
||||
# This builds the config in a bash script, to avoid storing the password
|
||||
# in the nix store at any point
|
||||
script = ''
|
||||
if [ ! -d ${powerdns-conf-dir} ]; then
|
||||
mkdir ${powerdns-conf-dir}
|
||||
fi
|
||||
|
||||
TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t pdns-XXXXXXXXXX)
|
||||
TMPCONF=$TMPDIR/pdns.local.gpgsql.conf
|
||||
|
||||
if [ ! -f ${cfg.database.password-file} ]; then
|
||||
echo "${cfg.database.password-file} does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch $TMPCONF
|
||||
chown backplane-powerdns:backplane-powerdns $TMPCONF
|
||||
chmod go-rwx $TMPCONF
|
||||
PASSWORD=$(cat ${cfg.database.password-file})
|
||||
echo "launch+=gpgsql" >> $TMPCONF
|
||||
echo "gpgsql-host=${cfg.database.host}" >> $TMPCONF
|
||||
echo "gpgsql-dbname=${cfg.database.database}" >> $TMPCONF
|
||||
echo "gpgsql-user=${cfg.database.username}" >> $TMPCONF
|
||||
echo "gpgsql-password=$PASSWORD" >> $TMPCONF
|
||||
echo "gpgsql-dnssec=yes" >> $TMPCONF
|
||||
|
||||
mv $TMPCONF ${powerdns-conf-dir}/pdns.local.gpgsql.conf
|
||||
|
||||
rm -rf $TMPDIR
|
||||
|
||||
exit 0
|
||||
'';
|
||||
};
|
||||
|
||||
backplane-dns = {
|
||||
description = "Fudo DNS Backplane Server";
|
||||
restartIfChanged = true;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
|
||||
ExecStart = "${pkgs.sbcl}/bin/sbcl --load ${launchScript}";
|
||||
Restart = "on-failure";
|
||||
PIDFile = "/run/backplane-dns.$USERNAME.pid";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
};
|
||||
|
||||
environment = {
|
||||
LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib";
|
||||
|
||||
FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = cfg.backplane.host;
|
||||
FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane.role;
|
||||
FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane.password-file;
|
||||
FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.backplane.database.host;
|
||||
FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.backplane.database.database;
|
||||
FUDO_DNS_BACKPLANE_DATABASE_USERNAME = cfg.backplane.database.username;
|
||||
FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE = cfg.backplane.database.password-file;
|
||||
|
||||
CL_SOURCE_REGISTRY = lib.concatStringsSep ":" (map (pkg: "${pkg}//")
|
||||
(lisp-libs ++ [pkgs.backplane-dns]));
|
||||
};
|
||||
|
||||
requires = cfg.required-services;
|
||||
partOf = [ "backplane-dns.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.client.dns;
|
||||
|
||||
in {
|
||||
options.fudo.client.dns = {
|
||||
enable = mkEnableOption "Enable Fudo DynDNS Client.";
|
||||
|
||||
ipv4 = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Report host external IPv4 address to Fudo DynDNS server.";
|
||||
};
|
||||
|
||||
ipv6 = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Report host external IPv6 address to Fudo DynDNS server.";
|
||||
};
|
||||
|
||||
sshfp = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Report host SSH fingerprints to the Fudo DynDNS server.";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain under which this host is registered.";
|
||||
default = "fudo.link";
|
||||
};
|
||||
|
||||
server = mkOption {
|
||||
type = types.str;
|
||||
description = "Backplane DNS server to which changes will be reported.";
|
||||
default = "backplane.fudo.org";
|
||||
};
|
||||
|
||||
password-file = mkOption {
|
||||
type = types.str;
|
||||
description = "File containing host password for backplane.";
|
||||
example = "/path/to/secret.passwd";
|
||||
};
|
||||
|
||||
frequency = mkOption {
|
||||
type = types.str;
|
||||
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).";
|
||||
default = "backplane-dns-client";
|
||||
};
|
||||
|
||||
external-interface = mkOption {
|
||||
type = with types; nullOr str;
|
||||
description = "Interface with which this host communicates with the larger internet.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
# FIXME: take the relevant SSH package
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users = {
|
||||
"${cfg.user}" = {
|
||||
isSystemUser = true;
|
||||
createHome = true;
|
||||
home = "/var/home/${cfg.user}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd = {
|
||||
timers.backplane-dns-client = {
|
||||
enable = true;
|
||||
description = "Report local IP addresses to Fudo backplane.";
|
||||
partOf = [ "backplane-dns-client.service" ];
|
||||
wantedBy = [ "timers.target" ];
|
||||
requires = [ "network-online.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.frequency;
|
||||
};
|
||||
};
|
||||
|
||||
services.backplane-dns-client = {
|
||||
enable = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
StandardOutput = "journal";
|
||||
User = cfg.user;
|
||||
};
|
||||
path = [ pkgs.openssh ];
|
||||
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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.garbage-collector;
|
||||
|
||||
in {
|
||||
|
||||
options.fudo.garbage-collector = {
|
||||
enable = mkEnableOption "Enable periodic NixOS garbage collection";
|
||||
|
||||
timing = mkOption {
|
||||
type = types.str;
|
||||
default = "weekly";
|
||||
description = "Period (systemd format) at which to run garbage collector.";
|
||||
};
|
||||
|
||||
age = mkOption {
|
||||
type = types.str;
|
||||
default = "30d";
|
||||
description = "Age of garbage to collect (eg. 30d).";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd = {
|
||||
timers.fudo-garbage-collector = {
|
||||
enable = true;
|
||||
description = "Collect NixOS garbage older than ${cfg.age}";
|
||||
partOf = [ "fudo-garbage-collector.service" ];
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.timing;
|
||||
};
|
||||
};
|
||||
|
||||
services.fudo-garbage-collector = {
|
||||
enable = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
StandardOutput = "journal";
|
||||
};
|
||||
script = ''
|
||||
${pkgs.nix}/bin/nix-collect-garbage --delete-older-than ${cfg.age}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,8 @@ let
|
||||
|
||||
join-lines = concatStringsSep "\n";
|
||||
|
||||
ip = import ../../lib/ip.nix { lib = lib; };
|
||||
ip = import ../../lib/ip.nix { inherit lib; };
|
||||
dns = import ../../lib/dns.nix { inherit lib; };
|
||||
|
||||
hostOpts = { hostname, ... }: {
|
||||
options = {
|
||||
@@ -19,10 +20,11 @@ let
|
||||
};
|
||||
|
||||
mac-address = mkOption {
|
||||
type = types.str;
|
||||
type = with types; nullOr types.str;
|
||||
description = ''
|
||||
The MAC address of a given host, if desired for IP reservation.
|
||||
'';
|
||||
default = null;
|
||||
};
|
||||
|
||||
ssh-fingerprints = mkOption {
|
||||
@@ -35,33 +37,6 @@ let
|
||||
|
||||
traceout = out: builtins.trace out out;
|
||||
|
||||
srvRecordOpts = with types; {
|
||||
options = {
|
||||
weight = mkOption {
|
||||
type = int;
|
||||
description = "Weight relative to other records.";
|
||||
default = 1;
|
||||
};
|
||||
|
||||
priority = mkOption {
|
||||
type = int;
|
||||
description = "Priority to give this record.";
|
||||
default = 0;
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = port;
|
||||
description = "Port to use when connecting.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = str;
|
||||
description = "Host to contact for this service.";
|
||||
example = "my-host.my-domain.com.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
|
||||
options.fudo.local-network = {
|
||||
@@ -69,7 +44,7 @@ in {
|
||||
enable = mkEnableOption "Enable local network configuration (DHCP & DNS).";
|
||||
|
||||
hosts = mkOption {
|
||||
type = with types; loaOf (submodule hostOpts);
|
||||
type = with types; attrsOf (submodule hostOpts);
|
||||
default = {};
|
||||
description = "A map of hostname => { host_attributes }.";
|
||||
};
|
||||
@@ -100,7 +75,7 @@ in {
|
||||
};
|
||||
|
||||
aliases = mkOption {
|
||||
type = with types; loaOf str;
|
||||
type = with types; attrsOf str;
|
||||
default = {};
|
||||
description = "A mapping of host-alias => hostname to use on the local network.";
|
||||
};
|
||||
@@ -143,7 +118,7 @@ in {
|
||||
};
|
||||
|
||||
srv-records = mkOption {
|
||||
type = with types; attrsOf (attrsOf (listOf (submodule srvRecordOpts)));
|
||||
type = dns.srvRecords;
|
||||
description = "Map of traffic type to srv records.";
|
||||
default = {};
|
||||
example = {
|
||||
@@ -174,7 +149,7 @@ in {
|
||||
ethernetAddress = hostOpts.mac-address;
|
||||
hostName = hostname;
|
||||
ipAddress = hostOpts.ip-address;
|
||||
}) cfg.hosts;
|
||||
}) (filterAttrs (host: hostOpts: hostOpts.mac-address != null) cfg.hosts);
|
||||
|
||||
interfaces = cfg.dhcp-interfaces;
|
||||
|
||||
@@ -231,17 +206,18 @@ in {
|
||||
hostSshFpRecords = host: data: join-lines (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints);
|
||||
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
|
||||
|
||||
makeSrvRecords = protocol: type: records:
|
||||
join-lines (map (record: "_${type}._${protocol} IN SRV ${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.")
|
||||
records);
|
||||
|
||||
makeSrvProtocolRecords = protocol: types: join-lines (mapAttrsToList (makeSrvRecords protocol) types);
|
||||
|
||||
in {
|
||||
enable = true;
|
||||
cacheNetworks = [ cfg.network "localhost" "localnets" ];
|
||||
forwarders = [ cfg.recursive-resolver ];
|
||||
listenOn = cfg.dns-serve-ips;
|
||||
extraOptions = concatStringsSep "\n" [
|
||||
"dnssec-enable yes;"
|
||||
"dnssec-validation yes;"
|
||||
"auth-nxdomain no;"
|
||||
"recursion yes;"
|
||||
"allow-recursion { any; };"
|
||||
];
|
||||
zones = [
|
||||
{
|
||||
master = true;
|
||||
@@ -267,7 +243,7 @@ in {
|
||||
${join-lines (mapAttrsToList hostSshFpRecords cfg.hosts)}
|
||||
${join-lines (mapAttrsToList cnameRecord cfg.aliases)}
|
||||
${join-lines cfg.extra-dns-records}
|
||||
${join-lines (mapAttrsToList makeSrvProtocolRecords cfg.srv-records)}
|
||||
${dns.srvRecordsToBindZone cfg.srv-records}
|
||||
'';
|
||||
}
|
||||
] ++ blockZones;
|
||||
|
||||
@@ -160,22 +160,6 @@ in rec {
|
||||
};
|
||||
};
|
||||
|
||||
# users = {
|
||||
# users = {
|
||||
# ${container-mail-user} = {
|
||||
# isSystemUser = true;
|
||||
# uid = container-mail-user-id;
|
||||
# group = "mailer";
|
||||
# };
|
||||
# };
|
||||
|
||||
# groups = {
|
||||
# ${container-mail-group} = {
|
||||
# members = ["mailer"];
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
|
||||
fudo.mail-server = {
|
||||
enable = true;
|
||||
hostname = cfg.hostname;
|
||||
|
||||
@@ -232,6 +232,10 @@ in {
|
||||
user = root
|
||||
}
|
||||
|
||||
service imap {
|
||||
vsz_limit = 1024M
|
||||
}
|
||||
|
||||
namespace inbox {
|
||||
separator = "/"
|
||||
inbox = yes
|
||||
|
||||
@@ -130,7 +130,7 @@ in {
|
||||
virtual = ''
|
||||
${make-user-aliases cfg.user-aliases}
|
||||
|
||||
${make-alias-users cfg.local-domains cfg.alias-users}
|
||||
${make-alias-users ([cfg.domain] ++ cfg.local-domains) cfg.alias-users}
|
||||
'';
|
||||
|
||||
sslCert = cfg.postfix.ssl-certificate;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.netinfo-email;
|
||||
|
||||
make-script = server: port: target: pkgs.writeText "netinfo-script.rb" ''
|
||||
#!${pkgs.ruby}/bin/ruby
|
||||
|
||||
require 'net/smtp'
|
||||
|
||||
raise RuntimeError.new("NETINFO_SMTP_USERNAME not set!") if not ENV['NETINFO_SMTP_USERNAME']
|
||||
user = ENV['NETINFO_SMTP_USERNAME']
|
||||
|
||||
raise RuntimeError.new("NETINFO_SMTP_PASSWD not set!") if not ENV['NETINFO_SMTP_PASSWD']
|
||||
passwd = ENV['NETINFO_SMTP_PASSWD']
|
||||
|
||||
hostname = `${pkgs.inetutils}/bin/hostname -f`.strip
|
||||
date = `${pkgs.coreutils}/bin/date +%Y-%m-%d`.strip
|
||||
email_date = `${pkgs.coreutils}/bin/date`
|
||||
ipinfo = `${pkgs.iproute}/bin/ip addr`
|
||||
|
||||
message = <<EOM
|
||||
From: #{user}@fudo.org
|
||||
To: ${target}
|
||||
Subject: #{hostname} network info for #{date}
|
||||
Date: #{email_date}
|
||||
|
||||
#{ipinfo}
|
||||
EOM
|
||||
|
||||
smtp = Net::SMTP.new("${server}", ${toString port})
|
||||
smtp.enable_starttls
|
||||
|
||||
smtp.start('localhost', user, passwd) do |server|
|
||||
server.send_message(message, "#{user}@fudo.org", ["${target}"])
|
||||
end
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
options.fudo.netinfo-email = {
|
||||
enable = mkEnableOption "Enable netinfo email (hacky way to keep track of a host's IP";
|
||||
|
||||
smtp-server = mkOption {
|
||||
type = types.str;
|
||||
default = "mail.fudo.org";
|
||||
};
|
||||
|
||||
smtp-port = mkOption {
|
||||
type = types.port;
|
||||
default = 587;
|
||||
};
|
||||
|
||||
env-file = mkOption {
|
||||
type = types.str;
|
||||
description = "Path to file containing NETINFO_SMTP_USERNAME and NETINFO_SMTP_PASSWD";
|
||||
};
|
||||
|
||||
target-email = mkOption {
|
||||
type = types.str;
|
||||
default = "network-info@fudo.link";
|
||||
description = "Email to which to send network info report.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd = {
|
||||
timers.netinfo = {
|
||||
enable = true;
|
||||
description = "Send network info to ${cfg.target-email}";
|
||||
partOf = ["netinfo.service"];
|
||||
wantedBy = [ "timers.target" ];
|
||||
requires = [ "network-online.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
};
|
||||
};
|
||||
|
||||
services.netinfo = {
|
||||
enable = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
StandardOutput = "journal";
|
||||
EnvironmentFile = cfg.env-file;
|
||||
};
|
||||
script = ''
|
||||
${pkgs.ruby}/bin/ruby ${make-script cfg.smtp-server cfg.smtp-port cfg.target-email}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.password;
|
||||
|
||||
genOpts = {
|
||||
options = {
|
||||
file = mkOption {
|
||||
type = types.str;
|
||||
description = "Password file in which to store a generated password.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
description = "User to which the file should belong.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = with types; nullOr str;
|
||||
description = "Group to which the file should belong.";
|
||||
default = "nogroup";
|
||||
};
|
||||
|
||||
restart-services = mkOption {
|
||||
type = with types; listOf str;
|
||||
description = "List of services to restart when the password file is generated.";
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
generate-passwd-file = file: user: group: pkgs.writeShellScriptBin "generate-passwd-file.sh" ''
|
||||
mkdir -p $(dirname ${file})
|
||||
|
||||
if touch ${file}; then
|
||||
chown ${user}${optionalString (group != null) ":${group}"} ${file}
|
||||
if [ $? -ne 0 ]; then
|
||||
rm ${file}
|
||||
echo "failed to set permissions on ${file}"
|
||||
exit 4
|
||||
fi
|
||||
${pkgs.pwgen}/bin/pwgen 30 1 > ${file}
|
||||
else
|
||||
echo "cannot write to ${file}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -f ${file} ]; then
|
||||
echo "Failed to create file ${file}"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
${if (group != null) then
|
||||
"chmod 640 ${file}"
|
||||
else
|
||||
"chmod 600 ${file}"}
|
||||
|
||||
echo "created password file ${file}"
|
||||
exit 0
|
||||
'';
|
||||
|
||||
restart-script = service-name: ''
|
||||
SYSCTL=${pkgs.systemd}/bin/systemctl
|
||||
JOBTYPE=$(${pkgs.systemd}/bin/systemctl show ${service-name} -p Type)
|
||||
if $SYSCTL is-active --quiet ${service-name} ||
|
||||
[ $JOBTYPE == "Type=simple" ] ||
|
||||
[ $JOBTYPE == "Type=oneshot" ] ; then
|
||||
echo "restarting service ${service-name} because password has changed."
|
||||
$SYSCTL restart ${service-name}
|
||||
fi
|
||||
'';
|
||||
|
||||
filterForRestarts = filterAttrs (name: opts: opts.restart-services != []);
|
||||
|
||||
in {
|
||||
options.fudo.password = {
|
||||
file-generator = mkOption {
|
||||
type = with types; loaOf (submodule genOpts);
|
||||
description = "List of password files to generate.";
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
systemd.targets.fudo-passwords = {
|
||||
description = "Target indicating that all Fudo passwords have been generated.";
|
||||
wantedBy = [ "default.target" ];
|
||||
};
|
||||
|
||||
systemd.services = fold (a: b: a // b) {} (mapAttrsToList (name: opts: {
|
||||
"file-generator-${name}" = {
|
||||
enable = true;
|
||||
partOf = [ "fudo-passwords.target" ];
|
||||
serviceConfig.Type = "oneshot";
|
||||
description = "Generate password file for ${name}.";
|
||||
script = "${generate-passwd-file opts.file opts.user opts.group}/bin/generate-passwd-file.sh";
|
||||
reloadIfChanged = true;
|
||||
};
|
||||
|
||||
"file-generator-watcher-${name}" = mkIf (! (opts.restart-services == [])) {
|
||||
description = "Restart services upon regenerating password for ${name}";
|
||||
after = [ "file-generator-${name}.service" ];
|
||||
partOf = [ "fudo-passwords.target" ];
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = concatStringsSep "\n" (map restart-script opts.restart-services);
|
||||
};
|
||||
}) cfg.file-generator);
|
||||
|
||||
systemd.paths = mapAttrs' (name: opts:
|
||||
nameValuePair "file-generator-watcher-${name}" {
|
||||
partOf = [ "fudo-passwords.target"];
|
||||
pathConfig.PathChanged = opts.file;
|
||||
}) (filterForRestarts cfg.file-generator);
|
||||
};
|
||||
}
|
||||
+161
-57
@@ -2,23 +2,51 @@
|
||||
|
||||
with lib;
|
||||
let
|
||||
|
||||
cfg = config.fudo.postgresql;
|
||||
|
||||
utils = import ../../lib/utils.nix { inherit lib; };
|
||||
|
||||
join-lines = lib.concatStringsSep "\n";
|
||||
|
||||
userDatabaseOpts = { database, ... }: {
|
||||
options = {
|
||||
access = mkOption {
|
||||
type = types.str;
|
||||
description = "Privileges for user on this database.";
|
||||
default = "CONNECT";
|
||||
};
|
||||
|
||||
entity-access = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
description = "A list of entities mapped to the access this user should have.";
|
||||
default = {};
|
||||
example = {
|
||||
"TABLE users" = "SELECT,DELETE";
|
||||
"ALL SEQUENCES IN public" = "SELECT";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
userOpts = { username, ... }: {
|
||||
options = {
|
||||
password = mkOption {
|
||||
password-file = mkOption {
|
||||
type = with types; nullOr str;
|
||||
description = "The user's (plaintext) password.";
|
||||
description = "A file containing the user's (plaintext) password.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
databases = mkOption {
|
||||
type = with types; loaOf str;
|
||||
description = "Map of databases to which this user has access, to the required perms.";
|
||||
type = with types; attrsOf (submodule userDatabaseOpts);
|
||||
description = "Map of databases to required database/table perms.";
|
||||
default = {};
|
||||
example = {
|
||||
my_database = "ALL PRIVILEGES";
|
||||
my_database = {
|
||||
access = "ALL PRIVILEGES";
|
||||
entity-access = {
|
||||
"ALL TABLES" = "SELECT";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -34,43 +62,68 @@ let
|
||||
};
|
||||
};
|
||||
|
||||
userDatabaseAccess = user: databases:
|
||||
mapAttrs' (database: perms:
|
||||
nameValuePair "DATABASE ${database}" perms)
|
||||
databases;
|
||||
filterPasswordedUsers = filterAttrs (user: opts: opts.password-file != null);
|
||||
|
||||
stringJoin = joiner: els:
|
||||
if (length els) == 0 then
|
||||
""
|
||||
else
|
||||
foldr(lel: rel: "${lel}${joiner}${rel}") (last els) (init els);
|
||||
password-setter-script = user: password-file: sql-file: ''
|
||||
unset PASSWORD
|
||||
if [ ! -f ${password-file} ]; then
|
||||
echo "file does not exist: ${password-file}"
|
||||
exit 1
|
||||
fi
|
||||
PASSWORD=$(cat ${password-file})
|
||||
echo "setting password for user ${user}"
|
||||
echo "ALTER USER ${user} ENCRYPTED PASSWORD '$PASSWORD';" >> ${sql-file}
|
||||
'';
|
||||
|
||||
passwords-setter-script = users:
|
||||
pkgs.writeScriptBin "postgres-set-passwords.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "usage: $0 output-file.sql"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT_FILE=$1
|
||||
|
||||
if [ ! -f $OUTPUT_FILE ]; then
|
||||
echo "file doesn't exist: $OUTPUT_FILE"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
${join-lines
|
||||
(mapAttrsToList
|
||||
(user: opts: password-setter-script user opts.password-file "$OUTPUT_FILE")
|
||||
(filterPasswordedUsers users))}
|
||||
'';
|
||||
|
||||
userDatabaseAccess = user: databases:
|
||||
mapAttrs' (database: databaseOpts:
|
||||
nameValuePair "DATABASE ${database}" databaseOpts.access)
|
||||
databases;
|
||||
|
||||
makeEntry = nw:
|
||||
"host all all ${nw} gss include_realm=0 krb_realm=FUDO.ORG";
|
||||
|
||||
makeNetworksEntry = networks:
|
||||
stringJoin "\n" (map makeEntry networks);
|
||||
|
||||
setPasswordSql = username: attrs:
|
||||
"ALTER USER ${username} ENCRYPTED PASSWORD '${attrs.password}';";
|
||||
|
||||
setPasswordsSql = users:
|
||||
(stringJoin "\n"
|
||||
(mapAttrsToList (username: attrs: setPasswordSql username attrs)
|
||||
(filterAttrs (user: attrs: attrs.password != null) users))) + "\n";
|
||||
makeNetworksEntry = networks: join-lines (map makeEntry networks);
|
||||
|
||||
makeLocalUserPasswordEntries = users:
|
||||
stringJoin "\n"
|
||||
(mapAttrsToList
|
||||
(username: attrs:
|
||||
stringJoin "\n"
|
||||
(map (db: ''
|
||||
local ${db} ${username} md5
|
||||
host ${db} ${username} 127.0.0.1/16 md5
|
||||
host ${db} ${username} ::1/128 md5
|
||||
'') (attrNames attrs.databases)))
|
||||
users);
|
||||
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
|
||||
'') (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)}
|
||||
'';
|
||||
userAccessSql = user: userOpts: join-lines (mapAttrsToList (userDatabaseAccessSql user) userOpts.databases);
|
||||
usersAccessSql = users: join-lines (mapAttrsToList userAccessSql users);
|
||||
|
||||
in {
|
||||
|
||||
@@ -106,8 +159,15 @@ in {
|
||||
description = "A map of users to user attributes.";
|
||||
example = {
|
||||
sampleUser = {
|
||||
password = "some-password";
|
||||
databases = [ "sample_user_db" ];
|
||||
password-file = "/path/to/password/file";
|
||||
databases = {
|
||||
some_database = {
|
||||
access = "CONNECT";
|
||||
entity-access = {
|
||||
"TABLE some_table" = "SELECT,UPDATE";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
@@ -136,6 +196,13 @@ in {
|
||||
description = "Users able to access the server via local socket.";
|
||||
default = [];
|
||||
};
|
||||
|
||||
required-services = mkOption {
|
||||
type = with types; listOf str;
|
||||
description = "List of services that should run before postgresql.";
|
||||
default = [];
|
||||
example = [ "password-generator.service" ];
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
@@ -166,13 +233,6 @@ in {
|
||||
group = "postgres";
|
||||
source = cfg.keytab;
|
||||
};
|
||||
|
||||
"postgresql/private/user-script.sql" = {
|
||||
mode = "0400";
|
||||
user = "postgres";
|
||||
group = "postgres";
|
||||
text = setPasswordsSql cfg.users;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -187,15 +247,20 @@ in {
|
||||
package = pkgs.postgresql_11_gssapi;
|
||||
enableTCPIP = true;
|
||||
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
|
||||
ensureUsers = mapAttrsToList
|
||||
ensureUsers = ((mapAttrsToList
|
||||
(username: attrs:
|
||||
{
|
||||
name = username;
|
||||
ensurePermissions =
|
||||
#{ "DATABASE ${username}" = "ALL PRIVILEGES"; };
|
||||
(userDatabaseAccess username attrs.databases);
|
||||
ensurePermissions = userDatabaseAccess username attrs.databases;
|
||||
})
|
||||
cfg.users;
|
||||
cfg.users) ++ (flatten (mapAttrsToList
|
||||
(database: opts:
|
||||
(map (username: {
|
||||
name = username;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${database}" = "ALL PRIVILEGES";
|
||||
};
|
||||
}) opts.users)) cfg.databases)));
|
||||
|
||||
extraConfig = ''
|
||||
krb_server_keyfile = '/etc/postgresql/private/postgres.keytab'
|
||||
@@ -221,15 +286,54 @@ in {
|
||||
# local networks
|
||||
${makeNetworksEntry cfg.local-networks}
|
||||
'';
|
||||
|
||||
# initialScript = pkgs.writeText "database-init.sql" ''
|
||||
# ${setPasswordsSql cfg.users}
|
||||
# '';
|
||||
};
|
||||
|
||||
systemd.services.postgresql.postStart = ''
|
||||
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -f /etc/postgresql/private/user-script.sql -d postgres
|
||||
${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
|
||||
'';
|
||||
systemd = {
|
||||
|
||||
services = {
|
||||
|
||||
postgresql-password-setter = let
|
||||
passwords-script = passwords-setter-script cfg.users;
|
||||
password-wrapper-script = pkgs.writeScriptBin "password-script-wrapper.sh" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t postgres-XXXXXXXXXX)
|
||||
echo "using temp dir $TMPDIR"
|
||||
PASSWORD_SQL_FILE=$TMPDIR/user-passwords.sql
|
||||
echo "password file $PASSWORD_SQL_FILE"
|
||||
touch $PASSWORD_SQL_FILE
|
||||
chown ${config.services.postgresql.superUser} $PASSWORD_SQL_FILE
|
||||
chmod go-rwx $PASSWORD_SQL_FILE
|
||||
${passwords-script}/bin/postgres-set-passwords.sh $PASSWORD_SQL_FILE
|
||||
echo "executing $PASSWORD_SQL_FILE"
|
||||
${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f $PASSWORD_SQL_FILE
|
||||
echo rm $PASSWORD_SQL_FILE
|
||||
echo "Postgresql user passwords set.";
|
||||
exit 0
|
||||
'';
|
||||
|
||||
in {
|
||||
description = "A service to set postgresql user passwords after the server has started.";
|
||||
after = [ "postgresql.service" ] ++ cfg.required-services;
|
||||
requires = [ "postgresql.service" ] ++ cfg.required-services;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = config.services.postgresql.superUser;
|
||||
};
|
||||
script = "${password-wrapper-script}/bin/password-script-wrapper.sh";
|
||||
};
|
||||
|
||||
postgresql.postStart = let
|
||||
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))}
|
||||
${usersAccessSql cfg.users}
|
||||
'';
|
||||
in ''
|
||||
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f ${extra-settings-sql}
|
||||
${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,6 +111,8 @@ in {
|
||||
|
||||
webExternalUrl = "https://${cfg.hostname}";
|
||||
|
||||
listenAddress = "127.0.0.1:9090";
|
||||
|
||||
scrapeConfigs = [
|
||||
{
|
||||
job_name = "docker";
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ in {
|
||||
|
||||
peers = mkOption {
|
||||
type = loaOf str;
|
||||
description = "A map of peer name to a path pointing to that client's private key.";
|
||||
description = "A map of peers to shared private keys.";
|
||||
default = {};
|
||||
example = {
|
||||
peer0 = "/path/to/priv.key";
|
||||
|
||||
@@ -329,6 +329,7 @@ in {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
description = "Change ownership of the phpfpm socket for webmail once it's started.";
|
||||
requires = [ "phpfpm-webmail.service" ];
|
||||
after = [ "phpfpm.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = ''
|
||||
${pkgs.coreutils}/bin/chown ${webmail-user}:${webmail-group} ${config.services.phpfpm.pools.webmail.socket}
|
||||
@@ -337,8 +338,10 @@ in {
|
||||
};
|
||||
|
||||
nginx = {
|
||||
requires = [ "webmail-init.service" ];
|
||||
wantedBy = [ "phpfpm-webmail-socket-perm.service" ];
|
||||
requires = [
|
||||
"webmail-init.service"
|
||||
"phpfpm-webmail-socket-perm.service"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,9 +5,12 @@ with lib;
|
||||
imports = [
|
||||
./fudo/acme-for-hostname.nix
|
||||
./fudo/authentication.nix
|
||||
./fudo/backplane
|
||||
./fudo/chat.nix
|
||||
./fudo/client/dns.nix
|
||||
./fudo/common.nix
|
||||
./fudo/dns.nix
|
||||
./fudo/garbage-collector.nix
|
||||
./fudo/git.nix
|
||||
./fudo/grafana.nix
|
||||
./fudo/kdc.nix
|
||||
@@ -16,7 +19,9 @@ with lib;
|
||||
./fudo/mail.nix
|
||||
./fudo/mail-container.nix
|
||||
./fudo/minecraft-server.nix
|
||||
./fudo/netinfo-email.nix
|
||||
./fudo/node-exporter.nix
|
||||
./fudo/password.nix
|
||||
./fudo/postgres.nix
|
||||
./fudo/prometheus.nix
|
||||
./fudo/secure-dns-proxy.nix
|
||||
|
||||
Reference in New Issue
Block a user