Add Rupert’s configuration

This commit is contained in:
fruchti 2023-02-26 16:42:49 +01:00
commit 2b4264d32d
31 changed files with 1742 additions and 0 deletions

164
options/auto-upgrade.nix Normal file
View file

@ -0,0 +1,164 @@
{ pkgs, lib, config, ... }:
with lib;
let
fromAddress = config.email.fromAddress;
fromIdentity = config.email.fromIdentity;
toAddress = config.email.adminEmail;
cfg = config.system.autoUpgrade;
in
{
options.system.autoUpgrade = {
sendEmail = mkOption {
type = types.bool;
default = false;
description = mdDoc ''
Whether to send a status email after an upgrade.
'';
};
};
config = mkIf cfg.enable {
systemd.services.nixos-upgrade.script = mkOverride 50 (
let
nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
date = "${pkgs.coreutils}/bin/date";
readlink = "${pkgs.coreutils}/bin/readlink";
grep = "${pkgs.gnugrep}/bin/grep";
shutdown = "${config.systemd.package}/bin/shutdown";
sendmail = "${pkgs.system-sendmail}/bin/sendmail";
upgradeFlag = optional (cfg.channel == null) "--upgrade";
in ''
set -o pipefail
upgrade() {
${nixos-rebuild} ${cfg.operation} ${toString (cfg.flags ++ upgradeFlag)}
}
indent()
{
while read -r line ; do
echo " $line"
done <<< "$1"
}
start_time="$(${date})"
reboot_allowed="no"
do_reboot="no"
exit_code=0
${optionalString cfg.allowReboot ''
reboot_allowed="yes"
${optionalString (cfg.rebootWindow != null) ''
current_time="$(${date} +%H:%M)"
lower="${cfg.rebootWindow.lower}"
upper="${cfg.rebootWindow.upper}"
if [[ "''${lower}" < "''${upper}" ]]; then
if [[ "''${current_time}" > "''${lower}" ]] && \
[[ "''${current_time}" < "''${upper}" ]]; then
reboot_allowed="yes"
else
reboot_allowed="no"
fi
else
# lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
# we want to reboot if cur > 23h or cur < 6h
if [[ "''${current_time}" < "''${upper}" ]] || \
[[ "''${current_time}" > "''${lower}" ]]; then
reboot_allowed="yes"
else
reboot_allowed="no"
fi
fi
''}
''}
output_file="$(mktemp)"
upgrade 2>&1 | tee "$output_file" || exit_code=$?
upgrade_output="$(cat "$output_file")"
rm -f "$output_file"
send_email=no
email_subject="Upgrade succeeded"
email_body="The system upgrade started at $start_time has succeeded."
if [ "$exit_code" -ne 0 ] ; then
send_email=yes
email_subject="Upgrade failed (exit code $exit_code)"
email_body="The system upgrade started at $start_time has failed with exit code $exit_code."
fi
possible_warnings="$(${grep} -e "^trace:" <<<"$upgrade_output" || true)"
if [ "$possible_warnings" != "" ] ; then
send_email=yes
email_subject="$email_subject with warnings"
email_body="$(cat <<-EOF
$email_body
These trace messages and warnings were encountered:
---------------------------------------------------
$possible_warnings
EOF
)"
fi
booted_version="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})"
built_version="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
if [ "$booted_version" != "$built_version" ] ; then
version_comparison="$(cat <<-EOF
The booted kernel version
$(indent "$booted_version")
does not match currently active
$(indent "$built_version")
.
EOF
)"
echo "$version_comparison"
send_email=yes
email_subject="$email_subject, reboot required"
email_body="$(cat <<-EOF
$email_body
A reboot is required, because:
------------------------------
$version_comparison
EOF
)"
if [ "$reboot_allowed" = "yes" ] && [ $exit_code -eq 0 ] ; then
email_body="$(printf "%s\n%s" "$email_body" "The system will reboot now.")"
do_reboot="yes"
fi
fi
${optionalString cfg.sendEmail ''
if [ "$send_email" = "yes" ] ; then
${sendmail} -t -X - <<-EOF
To: ${toAddress}
From: ${fromIdentity}
Subject: $email_subject
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=UTF-8
X-Priority: 3
$email_body
Full upgrade output:
--------------------
$upgrade_output
EOF
fi
''}
if [ "$do_reboot" = "yes" ] ; then
echo "Rebooting system."
${shutdown} -r +1
fi
exit $exit_code
''
);
};
}

56
options/btrfs-scrub.nix Normal file
View file

@ -0,0 +1,56 @@
{ pkgs, utils, lib, config, ... }:
with lib;
let
cfg = config.services.btrfsScrub;
in
{
options.services.btrfsScrub = {
enable = mkEnableOption "Periodically run btrfs-scrub on filesystems";
enableFailureEmail = mkOption {
type = types.bool;
default = true;
description = mdDoc "Send e-mail on scrub failure";
};
paths = mkOption {
type = with types; attrsOf (submodule {
options = {
onCalendar = mkOption {
type = str;
default = "*-*-* 02:00:00";
};
};
});
};
};
config.services.statusEmail.enable = mkIf (cfg.enable && cfg.enableFailureEmail) true;
config.systemd.services."btrfs-scrub@" = mkIf cfg.enable {
unitConfig = {
Description = "Scrub btrfs volume %I";
OnFailure = mkIf cfg.enableFailureEmail "status-email@%n.service";
};
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B -d %f";
# ExecStop = "-${pkgs.btrfs-progs}/bin/btrfs scrub cancel %f";
Nice = 19;
};
};
config.systemd.timers = mkIf cfg.enable (mapAttrs' (name: value:
let
pathEscaped = utils.escapeSystemdPath name;
in
nameValuePair "btrfs-scrub-${pathEscaped}"
{
wantedBy = [ "timers.target" ];
enable = true;
timerConfig = {
Unit = "btrfs-scrub@${pathEscaped}.service";
OnCalendar = value.onCalendar;
RandomizedDelaySec = "1h";
Persistent = true;
};
}) cfg.paths);
}

432
options/burp.nix Normal file
View file

@ -0,0 +1,432 @@
{ config, lib, pkgs, ... }:
with lib;
let
libDir = "/var/lib/burp";
clientCertDir = "${libDir}/CA-client";
cfg = config.services.burp;
clientConf = pkgs.writeText "burp.conf" ''
mode = client
pidfile = /run/burp/burp.pid
port = ${toString cfg.client.port}
status_port = ${toString cfg.client.statusPort}
server = ${cfg.client.server}
password = ${cfg.client.password}
cname = ${cfg.client.clientName}
ca_burp_ca = ${cfg.package}/bin/burp_ca
ca_csr_dir = ${clientCertDir}
ssl_cert_ca = ${libDir}/ssl_cert_ca.pem
ssl_cert = ${libDir}/ssl_cert-client.pem
ssl_key = ${libDir}/ssl_cert-client.key
ssl_key_password = ${cfg.client.sslKeyPassword}
ssl_peer_cn = burpserver
${concatMapStringsSep "\n" (x: "include = " + x) cfg.client.includes}
${concatMapStringsSep "\n" (x: "exclude = " + x) cfg.client.excludes}
nobackup = .nobackup
${cfg.client.extraConfig}
'';
serverHome = "/var/lib/burp";
serverClientConfDir = serverHome + "/clientconfdir";
serverCaConf = pkgs.writeText "CA.cnf" ''
CA_DIR = ${serverHome}/CA
[ ca ]
dir = $ENV::CA_DIR
database = $dir/index.txt
serial = $dir/serial.txt
certs = $dir/certs
new_certs_dir = $dir/newcerts
crlnumber = $dir/crlnumber.txt
unique_subject = no
default_md = sha256
default_days = 7300
default_crl_days = 7300
#????
name_opt = ca_default
cert_opt = ca_default
x509_extensions = usr_cert
copy_extensions = copy
policy = policy_anything
[ usr_cert ]
basicConstraints = CA:FALSE
[ policy_anything ]
commonName = supplied
'';
serverConf = pkgs.writeText "burp-server.conf" ''
mode = server
pidfile = /run/burp/burp-server.pid
listen = ${cfg.server.listen}
max_children = 5;
listen_status = ${cfg.server.listenStatus}
max_status_children = 5;
user = burp
group = burp
umask = 0022
clientconfdir = ${serverClientConfDir}
directory = ${cfg.server.dataDirectory}
ca_conf = ${serverCaConf}
ca_name = burpCA
ca_server_name = burpserver
ca_burp_ca = ${cfg.package}/bin/burp_ca
ca_crl_check = 1
ssl_cert_ca = ${serverHome}/ssl_cert_ca.pem
ssl_cert = ${serverHome}/ssl_cert-server.pem
ssl_key = ${serverHome}/ssl_cert-server.key
ssl_dhfile = ${serverHome}/dhfile.pem
ssl_key_password = ${cfg.server.sslKeyPassword}
${concatMapStringsSep "\n" (x: "keep = " + toString x) cfg.server.keep}
timer_script = ${cfg.server.timerScript}
${concatMapStringsSep "\n" (x: "timer_arg = " + x) cfg.server.timerArgs}
dedup_group = global
working_dir_recovery_method = ${cfg.server.workingDirRecoveryMethod}
max_resume_attempts = ${toString cfg.server.maxResumeAttempts}
${concatMapStringsSep "\n" (x: "super_client = " + x) cfg.server.superClients}
${concatMapStringsSep "\n" (x: "restore_client = " + x) cfg.server.restoreClients}
${cfg.server.extraConfig}
'';
clientConfigs = lib.attrsets.mapAttrs (name: config: (pkgs.writeText name ''
password = ${config.password}
${config.extraConfig}
'')) cfg.server.clients;
in {
options = {
services.burp.package = mkOption {
type = types.package;
default = pkgs.burp;
description = ''
The package which is used as a burp server/client.
'';
};
services.burp.server = {
enable = mkEnableOption "Burp server";
listen = mkOption {
type = types.str;
default = ":::4971";
description = ''
Listen address used for backup communication.
'';
};
listenStatus = mkOption {
type = types.str;
default = ":::4972";
description = ''
Listen address used for querying backup and server status.
'';
};
dataDirectory = mkOption {
type = types.str;
default = "/var/spool/burp";
description = ''
Where the backup data is stored.
'';
};
sslKeyPassword = mkOption {
type = types.str;
default = "change-this-password";
description = ''
SSL key password for loading a certificate with encryption.
'';
};
keep = mkOption {
type = types.listOf types.int;
default = [ 7 ];
description = ''
How many backups to keep.
'';
};
timerScript = mkOption {
type = types.str;
default = "${cfg.package}/share/burp/scripts/timer_script";
description = ''
Timer script used to evaluate if it's backup time.
'';
};
timerArgs = mkOption {
type = types.listOf types.str;
default = [
"20h"
"Mon,Tue,Wed,Thu,Fri,00,01,02,03,04,05,19,20,21,22,23"
"Sat,Sun,00,01,02,03,04,05,06,07,08,17,18,19,20,21,22,23"
];
description = ''
How often should backups be triggered.
'';
};
superClients = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Clients that are permitted to list, verify, restore, delete, and diff files belonging to any other client, according to the super_client's client_can permissions (eg, 'client_can_list'). If this is too permissive, you may set a super_client for individual original clients in the individual clientconfdir files, or look at the 'restoreClients' option.
'';
};
restoreClients = mkOption {
type = types.listOf types.str;
default = [];
description = ''
Clients that are permitted to list, verify, restore, delete, and diff files belonging to any other client, according to the client_can permissions on both the restore_client and the original_client (eg, 'client_can_list'). If this is too permissive, you may set a restoreClient for individual original clients in the individual clientconfdir files.
'';
};
workingDirRecoveryMethod = mkOption {
type = types.str;
default = "delete";
description = ''
This option tells the server what to do when it finds the working directory of an interrupted backup (perhaps somebody pulled the plug on the server, or something). This can be overridden by the client configurations files in clientconfdir on the server. Options are...
delete: Just delete the old working directory.
resume: Continue the previous backup from the point at which it left off. NOTE: If the client has changed its include/exclude configuration since the backup was interrupted, the recovery method will automatically switch to 'delete'. See also the 'resume attempts' option.
'';
};
maxResumeAttempts = mkOption {
type = types.int;
default = 0;
description = ''
If workingDirRecoveryMethod is 'resume', this option tells the server how many times to attempt to resume before giving up and deleting the working directory. The default is 0, which means to never give up.
'';
};
clients = mkOption {
type = types.attrsOf (types.submodule ({ ... }: {
options = {
password = mkOption {
type = types.str;
default = "change-this-password";
description = ''
Password used by the client for first contact with the server.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration for burp client. Contents will be added verbatim to the
configuration file.
'';
};
};
}));
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration for burp client. Contents will be added verbatim to the
configuration file.
'';
};
};
services.burp.client = {
enable = mkEnableOption "Burp client";
frequency = mkOption {
type = types.str;
default = "hourly";
description = ''
Controls how frequently the systemd timer is triggered and a backup
is attempted.
Backup frequency is still determined by the server, this
controls only how often the client tries.
'';
};
server = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
Address of burp server the client should contact.
'';
};
port = mkOption {
type = types.port;
default = 4971;
description = ''
Port used for backup communication.
'';
};
statusPort = mkOption {
type = types.port;
default = 4972;
description = ''
Port used for querying backup and server status.
'';
};
clientName = mkOption {
type = types.str;
default = "${config.networking.hostName}";
description = ''
Name the client should use to identify itself to the server.
'';
};
password = mkOption {
type = types.str;
default = "change-this-password";
description = ''
Password used by the client for first contact with the server.
'';
};
sslKeyPassword = mkOption {
type = types.str;
default = "change-this-password";
description = ''
SSL key password for loading a certificate with encryption.
'';
};
includes = mkOption {
type = types.listOf types.str;
default = [ "/etc" "/root" "/var" "/home" ];
description = ''
List of locations to include in backups.
'';
};
excludes = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of locations to exclude from the backup.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration for burp client. Contents will be added verbatim to the
configuration file.
'';
};
};
};
config = {
environment.systemPackages = mkIf cfg.client.enable [ cfg.package ];
systemd.timers.burp-client = mkIf cfg.client.enable {
description = "Timer for triggering Burp client and start a backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.client.frequency;
Unit = "burp-client.service";
};
};
systemd.services.burp-client = mkIf cfg.client.enable {
description = "Burp client";
after = [ "network.target" ];
path = [ cfg.package pkgs.nettools pkgs.openssl ];
preStart = ''
if [ ! -d "${libDir}" ]; then
mkdir -m 0755 -p ${libDir}
mkdir -m 0700 -p ${clientCertDir}
${cfg.package}/bin/burp -c ${libDir}/burp.conf -g
fi
ln -f -s ${clientConf} ${libDir}/burp.conf
'';
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/burp -c ${libDir}/burp.conf -a t";
SuccessExitStatus = "3";
};
};
users = mkIf cfg.server.enable {
users.burp = {
group = "burp";
uid = 497;
home = "${serverHome}";
createHome = true;
description = "Burp Server user";
shell = "${pkgs.bash}/bin/bash";
isSystemUser = true;
};
groups.burp = {
gid = 497;
};
};
systemd.services.burp-server = mkIf cfg.server.enable {
description = "Burp Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package pkgs.nettools pkgs.openssl ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/burp -F -v -c ${serverConf}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = "burp";
Group = "burp";
UMask = "0077";
RuntimeDirectory = "burp";
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = true;
ProtectHome = true;
NoNewPrivileges = true;
ReadWriteDirectories = [
cfg.server.dataDirectory
serverHome
];
ExecStartPre = "+${pkgs.writeScript "burp-prestart" ''
#!/${pkgs.bash}/bin/bash
if ! [ -d "${cfg.server.dataDirectory}" ] ; then
mkdir -p "${cfg.server.dataDirectory}"
fi
if ! [ -d "${serverClientConfDir}" ] ; then
mkdir -p "${serverClientConfDir}"
fi
chown burp:burp "${cfg.server.dataDirectory}" "${serverClientConfDir}"
chmod 700 "${cfg.server.dataDirectory}" "${serverClientConfDir}"
${concatStringsSep "\n" (mapAttrsToList(name: file:
"ln -fs " + file + " " + serverClientConfDir + "/" + name) clientConfigs)}
''}";
Nice = 19;
IOSchedulingClass = "idle";
CPUSchedulingPolicy = "idle";
};
};
};
}

10
options/default.nix Normal file
View file

@ -0,0 +1,10 @@
{ ... }:
{
imports = [
./email.nix
./burp.nix
./auto-upgrade.nix
./status-email.nix
./btrfs-scrub.nix
];
}

28
options/email.nix Normal file
View file

@ -0,0 +1,28 @@
{ config, lib, ... }:
with lib;
{
options.email = {
fromAddress = mkOption {
type = types.str;
example = "noreply@example.com";
description = ''
E-Mail address automated e-mails are sent from.
'';
};
fromIdentity = mkOption {
type = types.str;
default = "${config.networking.hostName} <${config.email.fromAddress}>";
example = "Maintenance (no reply) <noreply@example.com>";
description = ''
E-Mail identity automated e-mails are sent from.
'';
};
adminEmail = mkOption {
type = types.str;
example = "admin@example.com";
description = ''
E-Mail address automated e-mails are sent to.
'';
};
};
}

95
options/status-email.nix Normal file
View file

@ -0,0 +1,95 @@
{ pkgs, lib, config, ... }:
let
fromAddress = config.email.fromAddress;
fromIdentity = config.email.fromIdentity;
toAddress = config.email.adminEmail;
cfg = config.services.statusEmail;
in
{
options.services.statusEmail = {
enable = lib.mkEnableOption "Send systemd status e-mails";
};
config.programs.msmtp = lib.mkIf cfg.enable {
enable = true;
setSendmail = true;
accounts = {
default = {
auth = true;
host = "gvfr.de";
passwordeval = "cat /secrets/email_password.txt";
user = fromAddress;
from = fromAddress;
port = 465;
tls = true;
tls_starttls = false;
};
};
};
config.systemd.services."status-email@" = let
sendStatusEmail = pkgs.writeScript "send-status-email" ''
#!${pkgs.bash}/bin/bash
from="${fromIdentity}"
to="${toAddress}"
service="$1"
full_status="$(systemctl status --full --lines 200 "$service")"
exit_code="$(echo "$full_status" | head -n5 | tail -1 | sed -e 's/.*status=\(.*\))$/\1/g')"
# state="$(systemctl is-failed "$service")"
fail_priority=1
fail_subject="Unit \"$service\" failure report (exit code $exit_code)"
success_priority=3
success_subject="Unit \"$service\" report (success)"
shift
while [ $# -gt 0 ] ; do
case "$1" in
'-s'|'--fail-subject')
fail_subject="$2"
shift 2
;;
'-p'|'--fail-priority')
fail_priority="$2"
shift 2
;;
*)
break
;;
esac
done
if [ "$exit_code" != "0/SUCCESS" ] ; then
subject="$fail_subject"
priority="$fail_priority"
else
subject="$success_subject"
priority="$success_priority"
fi
echo "Sending e-mail \"$subject\" to \"$to\"."
${pkgs.system-sendmail}/bin/sendmail -t -X - <<ERRMAIL
To: $to
From: $from
Subject: $subject
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=UTF-8
X-Priority: $priority
$full_status
ERRMAIL
'';
in
lib.mkIf cfg.enable {
unitConfig = {
Description = "Send a status e-mail for %I";
};
serviceConfig = {
Type = "oneshot";
ExecStart = "${sendStatusEmail} %i";
};
};
}