Browse Source

Go big or go home

master
niten 9 months ago
parent
commit
c204541f38
  1. 2
      flake.nix
  2. 12
      lib.nix
  3. 11
      lib/default.nix
  4. 206
      lib/fudo/acme-certs.nix
  5. 69
      lib/fudo/acme-for-hostname.nix
  6. 67
      lib/fudo/authentication.nix
  7. 154
      lib/fudo/backplane/common.nix
  8. 10
      lib/fudo/backplane/default.nix
  9. 143
      lib/fudo/backplane/dns.nix
  10. 90
      lib/fudo/backplane/jabber.nix
  11. 262
      lib/fudo/chat.nix
  12. 131
      lib/fudo/client/dns.nix
  13. 5
      lib/fudo/common.nix
  14. 49
      lib/fudo/default.nix
  15. 13
      lib/fudo/deploy.nix
  16. 48
      lib/fudo/distributed-builds.nix
  17. 178
      lib/fudo/dns.nix
  18. 69
      lib/fudo/domain/dns.nix
  19. 74
      lib/fudo/domain/kerberos.nix
  20. 94
      lib/fudo/domains.nix
  21. 35
      lib/fudo/garbage-collector.nix
  22. 171
      lib/fudo/git.nix
  23. 5
      lib/fudo/global.nix
  24. 143
      lib/fudo/grafana.nix
  25. 123
      lib/fudo/host-filesystems.nix
  26. 127
      lib/fudo/hosts.nix
  27. 143
      lib/fudo/hosts/local-network.nix
  28. 117
      lib/fudo/include/rainloop.nix
  29. 87
      lib/fudo/initrd-network.nix
  30. 66
      lib/fudo/ipfs.nix
  31. 236
      lib/fudo/jabber.nix
  32. 532
      lib/fudo/kdc.nix
  33. 460
      lib/fudo/ldap.nix
  34. 238
      lib/fudo/local-network.nix
  35. 221
      lib/fudo/mail-container.nix
  36. 225
      lib/fudo/mail.nix
  37. 25
      lib/fudo/mail/clamav.nix
  38. 114
      lib/fudo/mail/dkim.nix
  39. 314
      lib/fudo/mail/dovecot.nix
  40. 15
      lib/fudo/mail/dovecot/imap_sieve/report-ham.sieve
  41. 7
      lib/fudo/mail/dovecot/imap_sieve/report-spam.sieve
  42. 3
      lib/fudo/mail/dovecot/pipe_bin/sa-learn-ham.sh
  43. 3
      lib/fudo/mail/dovecot/pipe_bin/sa-learn-spam.sh
  44. 319
      lib/fudo/mail/postfix.nix
  45. 88
      lib/fudo/mail/rspamd.nix
  46. 64
      lib/fudo/minecraft-server.nix
  47. 93
      lib/fudo/netinfo-email.nix
  48. 25
      lib/fudo/networks.nix
  49. 60
      lib/fudo/node-exporter.nix
  50. 978
      lib/fudo/nsd.nix
  51. 116
      lib/fudo/password.nix
  52. 370
      lib/fudo/postgres.nix
  53. 207
      lib/fudo/prometheus.nix
  54. 221
      lib/fudo/secrets.nix
  55. 103
      lib/fudo/secure-dns-proxy.nix
  56. 240
      lib/fudo/sites.nix
  57. 70
      lib/fudo/slynk.nix
  58. 25
      lib/fudo/ssh.nix
  59. 168
      lib/fudo/system-networking.nix
  60. 500
      lib/fudo/system.nix
  61. 126
      lib/fudo/users.nix
  62. 126
      lib/fudo/vpn.nix
  63. 385
      lib/fudo/webmail.nix
  64. 32
      lib/fudo/wireless-networks.nix
  65. 177
      lib/informis/cl-gemini.nix
  66. 7
      lib/informis/default.nix
  67. 122
      lib/instance.nix
  68. 0
      lib/lib/dns.nix
  69. 0
      lib/lib/filesystem.nix
  70. 0
      lib/lib/ip.nix
  71. 0
      lib/lib/lisp.nix
  72. 0
      lib/lib/network.nix
  73. 0
      lib/lib/passwd.nix
  74. 305
      lib/types/host.nix
  75. 108
      lib/types/network-definition.nix
  76. 32
      lib/types/network-host.nix
  77. 157
      lib/types/user.nix
  78. 7
      module.nix

2
flake.nix

@ -4,6 +4,8 @@
outputs = { self, ... }: {
overlay = import ./overlay.nix;
nixosModule = import ./module.nix;
lib = import ./lib.nix;
};
}

12
lib.nix

@ -1,10 +1,10 @@
{ pkgs, ... }:
{
ip = import ./lib/ip.nix { inherit pkgs; };
dns = import ./lib/dns.nix { inherit pkgs; };
passwd = import ./lib/passwd.nix { inherit pkgs; };
lisp = import ./lib/lisp.nix { inherit pkgs; };
network = import ./lib/network.nix { inherit pkgs; };
fs = import ./lib/filesystem.nix { inherit pkgs; };
ip = import ./lib/lib/ip.nix { inherit pkgs; };
dns = import ./lib/lib/dns.nix { inherit pkgs; };
passwd = import ./lib/lib/passwd.nix { inherit pkgs; };
lisp = import ./lib/lib/lisp.nix { inherit pkgs; };
network = import ./lib/lib/network.nix { inherit pkgs; };
fs = import ./lib/lib/filesystem.nix { inherit pkgs; };
}

11
lib/default.nix

@ -0,0 +1,11 @@
{ lib, config, pkgs, ... }:
{
imports = [
./instance.nix
./fudo
./informis
];
}

206
lib/fudo/acme-certs.nix

@ -0,0 +1,206 @@
{ config, lib, pkgs, ... } @ toplevel:
with lib;
let
hostname = config.instance.hostname;
domainOpts = { name, ... }: let
domain = name;
in {
options = with types; {
email = mkOption {
type = str;
description = "Domain administrator email.";
default = "admin@${domain}";
};
extra-domains = mkOption {
type = listOf str;
description = "List of domains to add to this certificate.";
default = [];
};
local-copies = let
localCopyOpts = { name, ... }: let
copy = name;
in {
options = with types; let
target-path = "/run/ssl-certificates/${domain}/${copy}";
in {
user = mkOption {
type = str;
description = "User to which this copy belongs.";
};
group = mkOption {
type = nullOr str;
description = "Group to which this copy belongs.";
default = null;
};
service = mkOption {
type = str;
description = "systemd job to copy certs.";
default = "fudo-acme-${domain}-${copy}-certs.service";
};
certificate = mkOption {
type = str;
description = "Full path to the local copy certificate.";
default = "${target-path}/cert.pem";
};
full-certificate = mkOption {
type = str;
description = "Full path to the local copy certificate.";
default = "${target-path}/fullchain.pem";
};
chain = mkOption {
type = str;
description = "Full path to the local copy certificate.";
default = "${target-path}/chain.pem";
};
private-key = mkOption {
type = str;
description = "Full path to the local copy certificate.";
default = "${target-path}/key.pem";
};
dependent-services = mkOption {
type = listOf str;
description = "List of systemd services depending on this copy.";
default = [ ];
};
part-of = mkOption {
type = listOf str;
description = "List of systemd targets to which this copy belongs.";
default = [ ];
};
};
};
in mkOption {
type = attrsOf (submodule localCopyOpts);
description = "Map of copies to make for use by services.";
default = {};
};
};
};
head-or-null = lst: if (lst == []) then null else head lst;
rm-service-ext = filename:
head-or-null (builtins.match "^(.+)\.service$" filename);
concatMapAttrs = f: attrs:
foldr (a: b: a // b) {} (mapAttrsToList f attrs);
cfg = config.fudo.acme;
hasLocalDomains = hasAttr hostname cfg.host-domains;
localDomains = if hasLocalDomains then
cfg.host-domains.${hostname} else {};
optionalStringOr = str: default:
if (str != null) then str else default;
in {
options.fudo.acme = with types; {
host-domains = mkOption {
type = attrsOf (attrsOf (submodule domainOpts));
description = "Map of host to domains to domain options.";
default = { };
};
};
config = {
security.acme.certs = mapAttrs (domain: domainOpts: {
email = domainOpts.email;
extraDomainNames = domainOpts.extra-domains;
}) localDomains;
# Assume that if we're acquiring SSL certs, we have a real IP for the
# host. nginx must have an acme dir for security.acme to work.
services.nginx = mkIf hasLocalDomains {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
recommendedProxySettings = true;
virtualHosts.${config.instance.host-fqdn} = {
enableACME = true;
forceSSL = true;
# Just...force override if you want this to point somewhere.
locations."/" = {
return = "403 Forbidden";
};
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
systemd = {
tmpfiles.rules = let
copies = concatMapAttrs (domain: domainOpts:
domainOpts.local-copies) localDomains;
perms = copyOpts: if (copyOpts.group != null) then "0550" else "0500";
copy-paths = mapAttrsToList (copy: copyOpts:
let
dir-entry = copyOpts: file: "d \"${dirOf file}\" ${perms copyOpts} ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -";
in map (dir-entry copyOpts) [
copyOpts.certificate
copyOpts.full-certificate
copyOpts.chain
copyOpts.private-key
]) copies;
in unique (concatMap (i: unique i) copy-paths);
services = concatMapAttrs (domain: domainOpts:
concatMapAttrs (copy: copyOpts: let
key-perms = copyOpts: if (copyOpts.group != null) then "0440" else "0400";
source = config.security.acme.certs.${domain}.directory;
target = copyOpts.path;
owners =
if (copyOpts.group != null) then
"${copyOpts.user}:${copyOpts.group}"
else copyOpts.user;
install-certs = pkgs.writeShellScript "fudo-install-${domain}-${copy}-certs.sh" ''
cp ${source}/cert.pem ${copyOpts.certificate}
chmod 0444 ${copyOpts.certificate}
chown ${owners} ${copyOpts.certificate}
cp ${source}/full.pem ${copyOpts.full-certificate}
chmod 0444 ${copyOpts.full-certificate}
chown ${owners} ${copyOpts.full-certificate}
cp ${source}/chain.pem ${copyOpts.chain}
chmod 0444 ${copyOpts.chain}
chown ${owners} ${copyOpts.chain}
cp ${source}/key.pem ${copyOpts.private-key}
chmod ${key-perms copyOpts} ${copyOpts.private-key}
chown ${owners} ${copyOpts.private-key}
'';
service-name = rm-service-ext copyOpts.service;
in {
${service-name} = {
description = "Copy ${domain} ACME certs for ${copy}.";
after = [ "acme-${domain}.service" ];
before = copyOpts.dependent-services;
wantedBy = [ "multi-user.target" ] ++ copyOpts.dependent-services;
partOf = copyOpts.part-of;
serviceConfig = {
Type = "simple";
ExecStart = install-certs;
RemainAfterExit = true;
StandardOutput = "journal";
};
};
}) domainOpts.local-copies) localDomains;
};
};
}

69
lib/fudo/acme-for-hostname.nix

@ -0,0 +1,69 @@
# Starts an Nginx server on $HOSTNAME just to get a cert for this host
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.acme;
# wwwRoot = hostname:
# pkgs.writeTextFile {
# name = "index.html";
# text = ''
# <html>
# <head>
# <title>${hostname}</title>
# </head>
# <body>
# <h1>${hostname}</title>
# </body>
# </html>
# '';
# destination = "/www";
# };
in {
options.fudo.acme = {
enable = mkEnableOption "Fetch ACME certs for supplied local hostnames.";
hostnames = mkOption {
type = with types; listOf str;
description = "A list of hostnames mapping to this host, for which to acquire SSL certificates.";
default = [];
example = [
"my.hostname.com"
"alt.hostname.com"
];
};
admin-address = mkOption {
type = types.str;
description = "The admin address in charge of these addresses.";
default = "admin@fudo.org";
};
};
config = mkIf cfg.enable {
services.nginx = {
enable = true;
virtualHosts = listToAttrs
(map
(hostname:
nameValuePair hostname
{
enableACME = true;
forceSSL = true;
# root = (wwwRoot hostname) + ("/" + "www");
})
cfg.hostnames);
};
security.acme.certs = listToAttrs
(map (hostname: nameValuePair hostname { email = cfg.admin-address; })
cfg.hostnames);
};
}

67
lib/fudo/authentication.nix

@ -0,0 +1,67 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.authentication;
in {
options.fudo.authentication = {
enable = mkEnableOption "Use Fudo users & groups from LDAP.";
ssl-ca-certificate = mkOption {
type = types.str;
description = "Path to the CA certificate to use to bind to the server.";
};
bind-passwd-file = mkOption {
type = types.str;
description = "Path to a file containing the password used to bind to the server.";
};
ldap-url = mkOption {
type = types.str;
description = "URL of the LDAP server.";
example = "ldaps://auth.fudo.org";
};
base = mkOption {
type = types.str;
description = "The LDAP base in which to look for users.";
default = "dc=fudo,dc=org";
};
bind-dn = mkOption {
type = types.str;
description = "The DN with which to bind the LDAP server.";
default = "cn=auth_reader,dc=fudo,dc=org";
};
};
config = mkIf cfg.enable {
users.ldap = {
enable = true;
base = cfg.base;
bind = {
distinguishedName = cfg.bind-dn;
passwordFile = cfg.bind-passwd-file;
timeLimit = 5;
};
loginPam = true;
nsswitch = true;
server = cfg.ldap-url;
timeLimit = 5;
useTLS = true;
extraConfig = ''
TLS_CACERT ${cfg.ssl-ca-certificate}
TSL_REQCERT allow
'';
daemon = {
enable = true;
extraConfig = ''
tls_cacertfile ${cfg.ssl-ca-certificate}
tls_reqcert allow
'';
};
};
};
}

154
lib/fudo/backplane/common.nix

@ -0,0 +1,154 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.backplane.dns;
powerdns-conf-dir = "${cfg.powerdns.home}/conf.d";
clientHostOpts = { name, ... }: {
options = with types; {
password-file = mkOption {
type = path;
description =
"Location (on the build host) of the file containing the host password.";
};
};
};
serviceOpts = { name, ... }: {
options = with types; {
password-file = mkOption {
type = path;
description =
"Location (on the build host) of the file containing the service password.";
};
};
};
databaseOpts = { ... }: {
options = with types; {
host = mkOption {
type = str;
description = "Hostname or IP of the PostgreSQL server.";
};
database = mkOption {
type = str;
description = "Database to use for DNS backplane.";
default = "backplane_dns";
};
username = mkOption {
type = str;
description = "Database user for DNS backplane.";
default = "backplane_dns";
};
password-file = mkOption {
type = str;
description = "File containing password for database user.";
};
};
};
in {
options.fudo.backplane = with types; {
client-hosts = mkOption {
type = attrsOf (submodule clientHostOpts);
description = "List of backplane client options.";
default = {};
};
services = mkOption {
type = attrsOf (submodule serviceOpts);
description = "List of backplane service options.";
default = {};
};
backplane-host = mkOption {
type = types.str;
description = "Hostname of the backplane XMPP server.";
};
dns = {
enable = mkEnableOption "Enable backplane dynamic DNS server.";
port = mkOption {
type = port;
description = "Port on which to serve authoritative DNS requests.";
default = 53;
};
listen-v4-addresses = mkOption {
type = listOf str;
description = "IPv4 addresses on which to listen for dns requests.";
default = [ "0.0.0.0" ];
};
listen-v6-addresses = mkOption {
type = listOf str;
description = "IPv6 addresses on which to listen for dns requests.";
example = [ "[abcd::1]" ];
default = [ ];
};
required-services = mkOption {
type = listOf str;
description =
"A list of services required before the DNS server can start.";
default = [ ];
};
user = mkOption {
type = str;
description = "User as which to run DNS backplane listener service.";
default = "backplane-dns";
};
group = mkOption {
type = str;
description = "Group as which to run DNS backplane listener service.";
default = "backplane-dns";
};
database = mkOption {
type = submodule databaseOpts;
description = "Database settings for the DNS server.";
};
powerdns = {
home = mkOption {
type = str;
description = "Directory at which to store powerdns configuration and state.";
default = "/run/backplane-dns/powerdns";
};
user = mkOption {
type = str;
description = "Username as which to run PowerDNS.";
default = "backplane-powerdns";
};
database = mkOption {
type = submodule databaseOpts;
description = "Database settings for the DNS server.";
};
};
backplane-role = {
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.";
};
};
};
};
}

10
lib/fudo/backplane/default.nix

@ -0,0 +1,10 @@
{ config, pkgs, lib, ... }:
with lib;
{
imports = [
./common.nix
./dns.nix
./jabber.nix
];
}

143
lib/fudo/backplane/dns.nix

@ -0,0 +1,143 @@
{ config, pkgs, lib, ... }:
with lib;
let
backplane-cfg = config.fudo.backplane;
cfg = backplane-cfg.dns;
powerdns-conf-dir = "${cfg.powerdns.home}/conf.d";
in {
config = mkIf cfg.enable {
users = {
users = {
"${cfg.user}" = {
isSystemUser = true;
group = cfg.group;
createHome = true;
home = "/var/home/${cfg.user}";
};
${cfg.powerdns.user} = {
isSystemUser = true;
home = cfg.powerdns.home;
createHome = true;
};
};
groups = {
${cfg.group} = { members = [ cfg.user ]; };
${cfg.powerdns.user} = { members = [ cfg.powerdns.user ]; };
};
};
fudo = {
system.services = {
backplane-powerdns-config-generator = {
description =
"Generate postgres configuration for backplane DNS server.";
requires = cfg.required-services;
type = "oneshot";
restartIfChanged = true;
partOf = [ "backplane-dns.target" ];
readWritePaths = [ powerdns-conf-dir ];
# This builds the config in a bash script, to avoid storing the password
# in the nix store at any point
script = let
user = cfg.powerdns.user;
db = cfg.powerdns.database;
in ''
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
chmod go-rwx $TMPCONF
chown ${user} $TMPCONF
PASSWORD=$(cat ${db.password-file})
echo "launch+=gpgsql" >> $TMPCONF
echo "gpgsql-host=${db.host}" >> $TMPCONF
echo "gpgsql-dbname=${db.database}" >> $TMPCONF
echo "gpgsql-user=${db.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;
path = with pkgs; [ backplane-dns-server ];
execStart = "launch-backplane-dns.sh";
pidFile = "/run/backplane-dns.$USERNAME.pid";
user = cfg.user;
group = cfg.group;
partOf = [ "backplane-dns.target" ];
requires = cfg.required-services ++ [ "postgresql.service" ];
environment = {
FUDO_DNS_BACKPLANE_XMPP_HOSTNAME = backplane-cfg.backplane-host;
FUDO_DNS_BACKPLANE_XMPP_USERNAME = cfg.backplane-role.role;
FUDO_DNS_BACKPLANE_XMPP_PASSWORD_FILE = cfg.backplane-role.password-file;
FUDO_DNS_BACKPLANE_DATABASE_HOSTNAME = cfg.database.host;
FUDO_DNS_BACKPLANE_DATABASE_NAME = cfg.database.database;
FUDO_DNS_BACKPLANE_DATABASE_USERNAME =
cfg.database.username;
FUDO_DNS_BACKPLANE_DATABASE_PASSWORD_FILE =
cfg.database.password-file;
CL_SOURCE_REGISTRY =
pkgs.lib.fudo.lisp.lisp-source-registry pkgs.backplane-dns-server;
};
};
};
};
systemd = {
tmpfiles.rules = [
"d ${powerdns-conf-dir} 0700 ${cfg.powerdns.user} - - -"
];
targets = {
backplane-dns = {
description = "Fudo DNS backplane services.";
wantedBy = [ "multi-user.target" ];
after = cfg.required-services ++ [ "postgresql.service" ];
};
};
services = {
backplane-powerdns = let
pdns-config-dir = pkgs.writeTextDir "pdns.conf" ''
local-address=${lib.concatStringsSep ", " cfg.listen-v4-addresses}
local-ipv6=${lib.concatStringsSep ", " cfg.listen-v6-addresses}
local-port=${toString cfg.port}
launch=
include-dir=${powerdns-conf-dir}/
'';
in {
description = "Backplane PowerDNS name server";
requires = [
"postgresql.service"
"backplane-powerdns-config-generator.service"
];
after = [ "network.target" ];
path = with pkgs; [ powerdns postgresql ];
serviceConfig = {
ExecStart = "pdns_server --setuid=${cfg.powerdns.user} --setgid=${cfg.powerdns.user} --chroot=${cfg.powerdns.home} --socket-dir=/ --daemon=no --guardian=no --disable-syslog --write-pid=no --config-dir=${pdns-config-dir}";
};
};
};
};
};
}

90
lib/fudo/backplane/jabber.nix

@ -0,0 +1,90 @@
{ config, lib, pkgs, ... }:
with lib;
{
config = mkIf config.fudo.jabber.enable {
fudo = let
cfg = config.fudo.backplane;
hostname = config.instance.hostname;
backplane-server = cfg.backplane-host;
generate-auth-file = name: files: let
make-entry = name: passwd-file:
''("${name}" . "${readFile passwd-file}")'';
entries = mapAttrsToList make-entry files;
content = concatStringsSep "\n" entries;
in pkgs.writeText "${name}-backplane-auth.scm" "'(${content})";
host-auth-file = generate-auth-file "host"
(mapAttrs (hostname: hostOpts: hostOpts.password-file)
cfg.client-hosts);
service-auth-file = generate-auth-file "service"
(mapAttrs (service: serviceOpts: serviceOpts.password-file)
cfg.services);
in {
secrets.host-secrets.${hostname} = {
backplane-host-auth = {
source-file = host-auth-file;
target-file = "/var/backplane/host-passwords.scm";
user = config.fudo.jabber.user;
};
backplane-service-auth = {
source-file = service-auth-file;
target-file = "/var/backplane/service-passwords.scm";
user = config.fudo.jabber.user;
};
};
jabber = {
environment = {
FUDO_HOST_PASSWD_FILE =
secrets.backplane-host-auth.target-file;
FUDO_SERVICE_PASSWD_FILE =
secrets.backplane-service-auth.target-file;
};
sites.${backplane-server} = {
site-config = {
auth_method = "external";
extauth_program =
"${pkgs.guile}/bin/guile -s ${pkgs.backplane-auth}/backplane-auth.scm";
extauth_pool_size = 3;
auth_use_cache = true;
modules = {
mod_adhoc = {};
mod_caps = {};
mod_carboncopy = {};
mod_client_state = {};
mod_configure = {};
mod_disco = {};
mod_fail2ban = {};
mod_last = {};
mod_offline = {
access_max_user_messages = 5000;
};
mod_ping = {};
mod_pubsub = {
access_createnode = "pubsub_createnode";
ignore_pep_from_offline = true;
last_item_cache = false;
plugins = [
"flat"
"pep"
];
};
mod_roster = {};
mod_stream_mgmt = {};
mod_time = {};
mod_version = {};
};
};
};
};
};
};
}

262
lib/fudo/chat.nix

@ -0,0 +1,262 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.fudo.chat;
mattermost-config-target = "/run/chat/mattermost/mattermost-config.json";
in {
options.fudo.chat = with types; {
enable = mkEnableOption "Enable chat server";
hostname = mkOption {
type = str;
description = "Hostname at which this chat server is accessible.";
example = "chat.mydomain.com";
};
site-name = mkOption {
type = str;
description = "The name of this chat server.";
example = "My Fancy Chat Site";
};
smtp = {
server = mkOption {
type = str;
description = "SMTP server to use for sending notification emails.";
example = "mail.my-site.com";
};
user = mkOption {
type = str;
description = "Username with which to connect to the SMTP server.";
};
password-file = mkOption {
type = str;
description =
"Path to a file containing the password to use while connecting to the SMTP server.";
};
};
state-directory = mkOption {
type = str;
description = "Path at which to store server state data.";
default = "/var/lib/mattermost";
};
database = mkOption {
type = (submodule {
options = {
name = mkOption {
type = str;
description = "Database name.";
};
hostname = mkOption {
type = str;
description = "Database host.";
};
user = mkOption {
type = str;
description = "Database user.";
};
password-file = mkOption {
type = str;
description = "Path to file containing database password.";
};
};
});
description = "Database configuration.";
example = {
name = "my_database";
hostname = "my.database.com";
user = "db_user";
password-file = /path/to/some/file.pw;
};
};
};
config = mkIf cfg.enable (let
pkg = pkgs.mattermost;
default-config = builtins.fromJSON (readFile "${pkg}/config/config.json");
modified-config = recursiveUpdate default-config {
ServiceSettings.SiteURL = "https://${cfg.hostname}";
ServiceSettings.ListenAddress = "127.0.0.1:8065";
TeamSettings.SiteName = cfg.site-name;
EmailSettings = {
RequireEmailVerification = true;
SMTPServer = cfg.smtp.server;
SMTPPort = 587;
EnableSMTPAuth = true;
SMTPUsername = cfg.smtp.user;
SMTPPassword = "__SMTP_PASSWD__";
SendEmailNotifications = true;
ConnectionSecurity = "STARTTLS";
FeedbackEmail = "chat@fudo.org";
FeedbackName = "Admin";
};
EnableEmailInvitations = true;
SqlSettings.DriverName = "postgres";
SqlSettings.DataSource = "postgres://${
cfg.database.user
}:__DATABASE_PASSWORD__@${
cfg.database.hostname
}:5432/${
cfg.database.name
}";
};
mattermost-config-file-template =
pkgs.writeText "mattermost-config.json.template" (builtins.toJSON modified-config);
mattermost-user = "mattermost";
mattermost-group = "mattermost";
generate-mattermost-config = target: template: smtp-passwd-file: db-passwd-file:
pkgs.writeScript "mattermost-config-generator.sh" ''
SMTP_PASSWD=$( cat ${smtp-passwd-file} )
DATABASE_PASSWORD=$( cat ${db-passwd-file} )
sed -e 's/__SMTP_PASSWD__/"$SMTP_PASSWD"/' -e 's/__DATABASE_PASSWORD__/"$DATABASE_PASSWORD"/' ${template} > ${target}
'';
in {
users = {
users = {
${mattermost-user} = {
isSystemUser = true;
group = mattermost-group;
};
};
groups = { ${mattermost-group} = { members = [ mattermost-user ]; }; };
};
fudo.system.services.mattermost = {
description = "Mattermost Chat Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
${generate-mattermost-config
mattermost-config-target
mattermost-config-file-template
cfg.smtp.password-file
cfg.database.password-file}
cp ${cfg.smtp.password-file} ${cfg.state-directory}/config/config.json
cp -uRL ${pkg}/client ${cfg.state-directory}
chown ${mattermost-user}:${mattermost-group} ${cfg.state-directory}/client
chmod 0750 ${cfg.state-directory}/client
'';
execStart = "${pkg}/bin/mattermost";
workingDirectory = cfg.state-directory;
user = mattermost-user;
group = mattermost-group;
};
systemd = {
tmpfiles.rules = [
"d ${cfg.state-directory} 0750 ${mattermost-user} ${mattermost-group} - -"
"d ${cfg.state-directory}/config 0750 ${mattermost-user} ${mattermost-group} - -"
"L ${cfg.state-directory}/bin - - - - ${pkg}/bin"
"L ${cfg.state-directory}/fonts - - - - ${pkg}/fonts"
"L ${cfg.state-directory}/i18n - - - - ${pkg}/i18n"
"L ${cfg.state-directory}/templates - - - - ${pkg}/templates"
];
# services.mattermost = {
# description = "Mattermost Chat Server";
# wantedBy = [ "multi-user.target" ];
# after = [ "network.target" ];
# preStart = ''
# ${generate-mattermost-config
# mattermost-config-target
# mattermost-config-file-template
# cfg.smtp.password-file
# cfg.database.password-file}
# cp ${cfg.smtp.password-file} ${cfg.state-directory}/config/config.json
# cp -uRL ${pkg}/client ${cfg.state-directory}
# chown ${mattermost-user}:${mattermost-group} ${cfg.state-directory}/client
# chmod 0750 ${cfg.state-directory}/client
# '';
# serviceConfig = {
# PermissionsStartOnly = true;
# ExecStart = "${pkg}/bin/mattermost";
# WorkingDirectory = cfg.state-directory;
# Restart = "always";
# RestartSec = "10";
# LimitNOFILE = "49152";
# User = mattermost-user;
# Group = mattermost-group;
# };
# };
};
services.nginx = {
enable = true;
appendHttpConfig = ''
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=mattermost_cache:10m max_size=3g inactive=120m use_temp_path=off;
'';
virtualHosts = {
"${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8065";
extraConfig = ''
client_max_body_size 50M;
proxy_set_header Connection "";
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 $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
proxy_read_timeout 600s;
proxy_cache mattermost_cache;
proxy_cache_revalidate on;
proxy_cache_min_uses 2;
proxy_cache_use_stale timeout;
proxy_cache_lock on;
proxy_http_version 1.1;
'';
};
locations."~ /api/v[0-9]+/(users/)?websocket$" = {
proxyPass = "http://127.0.0.1:8065";
extraConfig = ''
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 50M;
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 $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_buffers 256 16k;
proxy_buffer_size 16k;
client_body_timeout 60;
send_timeout 300;
lingering_timeout 5;
proxy_connect_timeout 90;
proxy_send_timeout 300;
proxy_read_timeout 90s;
'';
};
};
};
};
});
}

131
lib/fudo/client/dns.nix

@ -0,0 +1,131 @@
{ config, pkgs, lib, ... }:
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 = {
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;
};
};
config = {
users.users = {
"${cfg.user}" = {
isSystemUser = true;
createHome = true;
home = "/var/home/${cfg.user}";
};
};
systemd = {
tmpfiles.rules = [
"d /var/home 755 root - - -"
"d /var/home/${cfg.user} 700 ${cfg.user} - - -"
];
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-pw-file = {
enable = true;
requiredBy = [ "backplane-dns-client.services" ];
reloadIfChanged = true;
serviceConfig = { Type = "oneshot"; };
script = ''
chmod 400 ${cfg.password-file}
chown ${cfg.user} ${cfg.password-file}
'';
};
services.backplane-dns-client = {
enable = true;
serviceConfig = {
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;
};
};
};
}

5
lib/fudo/common.nix

@ -0,0 +1,5 @@
# General Fudo config, shared across packages
{ config, lib, pkgs, ... }:
with lib;
{ }

49
lib/fudo/default.nix

@ -0,0 +1,49 @@
{ config, lib, pkgs, ... }:
with lib; {
imports = [
./acme-certs.nix
./acme-for-hostname.nix
./authentication.nix
./backplane
./chat.nix
./client/dns.nix
./deploy.nix
./distributed-builds.nix
./dns.nix
./domains.nix
./garbage-collector.nix
./git.nix
./global.nix
./grafana.nix
./hosts.nix
./host-filesystems.nix
./initrd-network.nix
./ipfs.nix
./jabber.nix
./kdc.nix
./ldap.nix
./local-network.nix
./mail.nix
./mail-container.nix
./minecraft-server.nix
./netinfo-email.nix
./networks.nix
./node-exporter.nix
./nsd.nix
./password.nix
./postgres.nix
./prometheus.nix
./secrets.nix
./secure-dns-proxy.nix
./sites.nix
./slynk.nix
./ssh.nix
./system.nix
./system-networking.nix
./users.nix
./vpn.nix
./webmail.nix
./wireless-networks.nix
];
}

13
lib/fudo/deploy.nix

@ -0,0 +1,13 @@
{ config, lib, pkgs, ... }:
with lib;
let
site-cfg = config.fudo.sites.${config.instance.local-site};
in {
config = {
users.users.root.openssh.authorizedKeys.keys =
mkIf (site-cfg.deploy-pubkeys != null)
site-cfg.deploy-pubkeys;
};
}

48
lib/fudo/distributed-builds.nix

@ -0,0 +1,48 @@
{ config, lib, pkgs, ... }:
with lib;
let
hostname = config.instance.hostname;
site-cfg = config.fudo.sites.${config.instance.local-site};
has-build-servers = (length (attrNames site-cfg.build-servers)) > 0;
build-keypair = config.fudo.secrets.host-secrets.${hostname}.build-keypair;
enable-distributed-builds =
site-cfg.enable-distributed-builds && has-build-servers && build-keypair != null;
local-build-cfg = if (hasAttr hostname site-cfg.build-servers) then
site-cfg.build-servers.${hostname}
else null;
in {
config = {
nix = mkIf enable-distributed-builds {
buildMachines = mapAttrsToList (hostname: buildOpts: {
hostName = "${hostname}.${domain-name}";
maxJobs = buildOpts.max-jobs;
speedFactor = buildOpts.speed-factor;
supportedFeatures = buildOpts.supportedFeatures;
sshKey = build-keypair.private-key;
sshUser = buildOpts.user;
}) site-cfg.build-servers;
distributedBuilds = true;
trustedUsers = mkIf (local-build-cfg != null) [
local-build-host.build-user
];
};
users.users = mkIf (local-build-cfg != null) {
${local-build-cfg.build-user} = {
isSystemUser = true;
openssh.authorizedKeys.keyFiles =
concatLists
(mapAttrsToList (host: hostOpts: hostOpts.build-pubkeys)
config.instance.local-hosts);
};
};
};
}

178
lib/fudo/dns.nix

@ -0,0 +1,178 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.fudo.dns;
join-lines = concatStringsSep "\n";
domainOpts = { domain, ... }: {
options = with types; {
dnssec = mkOption {
type = bool;
description = "Enable DNSSEC security for this zone.";
default = true;
};
dmarc-report-address = mkOption {
type = nullOr str;
description = "The email to use to recieve DMARC reports, if any.";
example = "admin-user@domain.com";
default = null;
};
network-definition = mkOption {
type = submodule (import ../types/network-definition.nix);
description = "Definition of network to be served by local server.";
};
default-host = mkOption {
type = str;
description = "The host to which the domain should map by default.";
};
mx = mkOption {
type = listOf str;
description = "The hosts which act as the domain mail exchange.";
default = [];
};
gssapi-realm = mkOption {
type = nullOr str;
description = "The GSSAPI realm of this domain.";
default = null;
};
};
};
networkHostOpts = import ../types/network-host.nix { inherit lib; };
hostRecords = hostname: nethost-data: let
# FIXME: RP doesn't work.
# generic-host-records = let
# host-data = if (hasAttr hostname config.fudo.hosts) then config.fudo.hosts.${hostname} else null;
# in
# if (host-data == null) then [] else (
# (map (sshfp: "${hostname} IN SSHFP ${sshfp}") host-data.ssh-fingerprints) ++ (optional (host-data.rp != null) "${hostname} IN RP ${host-data.rp}")
# );
sshfp-records = if (hasAttr hostname config.fudo.hosts) then (map (sshfp: "${hostname} IN SSHFP ${sshfp}") config.fudo.hosts.${hostname}.ssh-fingerprints) else [];
a-record = optional (nethost-data.ipv4-address != null) "${hostname} IN A ${nethost-data.ipv4-address}";
aaaa-record = optional (nethost-data.ipv6-address != null) "${hostname} IN AAAA ${nethost-data.ipv6-address}";
description-record = optional (nethost-data.description != null) "${hostname} IN TXT \"${nethost-data.description}\"";
in
join-lines (a-record ++ aaaa-record ++ description-record ++ sshfp-records);
makeSrvRecords = protocol: type: records:
join-lines (map (record:
"_${type}._${protocol} IN SRV ${toString record.priority} ${
toString record.weight