Still very broken...moving to start testing

This commit is contained in:
nostoromo root
2021-02-23 12:58:29 -08:00
parent d34fc495b4
commit 372cf5fc6a
91 changed files with 3535 additions and 488 deletions
+36
View File
@@ -0,0 +1,36 @@
{ lib, config, pkgs, ... }:
with lib; {
imports = [
./instance.nix
./fudo/acme-for-hostname.nix
./fudo/authentication.nix
./fudo/backplane
./fudo/chat.nix
./fudo/client/dns.nix
./fudo/dns.nix
./fudo/garbage-collector.nix
./fudo/git.nix
./fudo/grafana.nix
./fudo/ipfs.nix
./fudo/kdc.nix
./fudo/ldap.nix
./fudo/local-network.nix
./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
./fudo/slynk.nix
./fudo/system.nix
./fudo/vpn.nix
./fudo/webmail.nix
./informis/cl-gemini.nix
];
}
+69
View File
@@ -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
View File
@@ -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
'';
};
};
};
}
+7
View File
@@ -0,0 +1,7 @@
{ ... }:
{
imports = [
./dns.nix
];
}
+326
View File
@@ -0,0 +1,326 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.fudo.backplane.dns;
lisp-pkgs = with pkgs.localLispPackages; [
arrows
backplane-dns
backplane-server
cl-sasl
cl-xmpp
ip-utils
alexandria
babel
bordeaux-threads
cffi
cl-base64
cl-json
cl-postgres
cl-ppcre
cl-unicode
cl_plus_ssl
closer-mop
closure-common
cxml
flexi-streams
global-vars
introspect-environment
ironclad
iterate
lisp-namespace
md5
nibbles
postmodern
puri
s-sql
split-sequence
trivia
trivia_dot_balland2006
trivia_dot_level0
trivia_dot_level1
trivia_dot_level2
trivia_dot_trivial
trivial-cltl2
trivial-features
trivial-garbage
trivial-gray-streams
type-i
uax-15
usocket
];
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.";
};
cl-wrapper-package = mkOption {
type = types.package;
description = "Common Lisp wrapper package to use.";
default = pkgs.lispPackages.clwrapper;
};
};
};
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.";
};
};
};
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-v4-addresses = mkOption {
type = with types; listOf str;
description = "IPv4 addresses on which to listen for dns requests.";
default = [ "0.0.0.0" ];
};
listen-v6-addresses = mkOption {
type = with types; listOf str;
description = "IPv6 addresses on which to listen for dns requests.";
example = [ "[abcd::1]" ];
default = [ ];
};
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-v4-addresses}
local-ipv6=${lib.concatStringsSep ", " cfg.listen-v6-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 = {
ExecStart =
"${pkgs.backplane-dns-server}/bin/launch-backplane-dns.sh";
Restart = "on-failure";
PIDFile = "/run/backplane-dns.$USERNAME.pid";
User = cfg.user;
Group = cfg.group;
StandardOutput = "journal";
};
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 = "${pkgs.localLispPackages.backplane-dns}//";
CL_SOURCE_REGISTRY =
lib.concatStringsSep ":" (map (pkg: "${pkg}//") lisp-pkgs);
};
requires = cfg.required-services;
partOf = [ "backplane-dns.target" ];
wantedBy = [ "multi-user.target" ];
};
};
};
};
}
+222
View File
@@ -0,0 +1,222 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.fudo.chat;
in {
options.fudo.chat = {
enable = mkEnableOption "Enable chat server";
hostname = mkOption {
type = types.str;
description = "Hostname at which this chat server is accessible.";
example = "chat.mydomain.com";
};
site-name = mkOption {
type = types.str;
description = "The name of this chat server.";
example = "My Fancy Chat Site";
};
smtp-server = mkOption {
type = types.str;
description = "SMTP server to use for sending notification emails.";
example = "mail.my-site.com";
};
smtp-user = mkOption {
type = types.str;
description = "Username with which to connect to the SMTP server.";
};
smtp-password-file = mkOption {
type = types.str;
description = "Path to a file containing the password to use while connecting to the SMTP server.";
};
state-directory = mkOption {
type = types.str;
description = "Path at which to store server state data.";
default = "/var/lib/mattermost";
};
database = mkOption {
type = (types.submodule {
options = {
name = mkOption {
type = types.str;
description = "Database name.";
};
hostname = mkOption {
type = types.str;
description = "Database host.";
};
user = mkOption {
type = types.str;
description = "Database user.";
};
password-file = mkOption {
type = types.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 = (fileContents cfg.smtp-password-file);
SendEmailNotifications = true;
ConnectionSecurity = "STARTTLS";
FeedbackEmail = "chat@fudo.org";
FeedbackName = "Admin";
};
EnableEmailInvitations = true;
SqlSettings.DriverName = "postgres";
SqlSettings.DataSource =
"postgres://${cfg.database.user}:${fileContents cfg.database.password-file}@${cfg.database.hostname}:5432/${cfg.database.name}";
};
mattermost-config-file = pkgs.writeText "mattermost-config.json" (builtins.toJSON modified-config);
mattermost-user = "mattermost";
mattermost-group = "mattermost";
in {
users = {
users = {
${mattermost-user} = {
isSystemUser = true;
group = mattermost-group;
};
};
groups = {
${mattermost-group} = {
members = [ mattermost-user ];
};
};
};
system.activationScripts.mattermost = ''
mkdir -p ${cfg.state-directory}
'';
systemd.services.mattermost = {
description = "Mattermost Chat Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
mkdir -p ${cfg.state-directory}/config
cp ${mattermost-config-file} ${cfg.state-directory}/config/config.json
ln -sf ${pkg}/bin ${cfg.state-directory}
ln -sf ${pkg}/fonts ${cfg.state-directory}
ln -sf ${pkg}/i18n ${cfg.state-directory}
ln -sf ${pkg}/templates ${cfg.state-directory}
cp -uRL ${pkg}/client ${cfg.state-directory}
chown -R ${mattermost-user}:${mattermost-group} ${cfg.state-directory}
chmod u+w -R ${cfg.state-directory}/client
chmod o-rwx -R ${cfg.state-directory}
'';
serviceConfig = {
PermissionsStartOnly = true;
ExecStart = "${pkg}/bin/mattermost";
WorkingDirectory = cfg.state-directory;
Restart = "always";
RestartSec = "10";
LimitNOFILE = "49152";
User = mattermost-user;
Group = mattermost-group;
};
};
security.acme.certs.${cfg.hostname}.email = config.fudo.common.admin-email;
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;
'';
};
};
};
};
});
}
+117
View File
@@ -0,0 +1,117 @@
{ 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-pw-file = {
enable = true;
requiredBy = [ "backplane-dns-client.services" ];
reloadIfChanged = true;
serviceConfig = {
Type = "oneshot";
};
script = ''
chmod 600 ${cfg.password-file}
chown ${cfg.user} ${cfg.password-file}
'';
};
services.backplane-dns-client = {
enable = true;
serviceConfig = {
Type = "oneshot";
StandardOutput = "journal";
User = cfg.user;
};
path = [ pkgs.openssh ];
reloadIfChanged = true;
script = ''
${pkgs.backplane-dns-client}/bin/backplane-dns-client ${optionalString cfg.ipv4 "-4"} ${optionalString cfg.ipv6 "-6"} ${optionalString cfg.sshfp "-f"} ${optionalString (cfg.external-interface != null) "--interface=${cfg.external-interface}"} --domain=${cfg.domain} --server=${cfg.server} --password-file=${cfg.password-file}
'';
};
};
};
}
+65
View File
@@ -0,0 +1,65 @@
# General Fudo config, shared across packages
{ config, lib, pkgs, ... }:
with lib; {
options.fudo.common = {
hostname = mkOption {
type = types.str;
description = ''
Hostname of the local host (without domain).
'';
};
# domain = mkOption {
# type = types.str;
# description = ''
# Domain of the local network.
# '';
# };
# local-networks = mkOption {
# type = with types; listOf str;
# description = ''
# A list of networks to consider 'local'. Used by various services to
# limit access to the external world.
# '';
# default = [ ];
# };
# profile = mkOption {
# type = with types; nullOr str;
# example = "desktop";
# description = ''
# The profile to use for this host. This will do some profile-dependent
# configuration, for example removing X-libs from servers and adding UI
# packages to desktops.
# '';
# default = null;
# };
# site = mkOption {
# type = with types; nullOr str;
# example = "seattle";
# description = ''
# The site at which this host is located. This will do some site-dependent
# configuration.
# '';
# default = null;
# };
# www-root = mkOption {
# type = types.path;
# description = "Path at which to store www files for serving.";
# example = /var/www;
# };
# admin-email = mkOption {
# type = types.str;
# description = "Email for administrator of this system.";
# default = "admin@fudo.org";
# };
# enable-gui = mkEnableOption "Install desktop GUI software.";
};
}
+250
View File
@@ -0,0 +1,250 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.fudo.dns;
join-lines = concatStringsSep "\n";
hostOpts = { host, ... }: {
options = {
ip-addresses = mkOption {
type = with types; listOf str;
description = ''
A list of IPv4 addresses assigned to this host.
'';
default = [ ];
};
ipv6-addresses = mkOption {
type = with types; listOf str;
description = ''
A list of IPv6 addresses assigned to this host.
'';
default = [ ];
};
ssh-fingerprints = mkOption {
type = with types; listOf str;
description = ''
A list of DNS SSHFP records for this host.
'';
default = [ ];
};
description = mkOption {
type = with types; nullOr str;
description = "Description of this host for a TXT record.";
default = null;
};
rp = mkOption {
type = with types; nullOr str;
description = "Responsible person.";
default = null;
};
};
};
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 while connecting to this service.";
};
host = mkOption {
type = str;
description = "Host that provides this service.";
example = "my-host.my-domain.com";
};
};
};
domainOpts = { domain, ... }:
with types; {
options = {
hosts = mkOption {
type = loaOf (submodule hostOpts);
default = { };
description = "A map of hostname to { host_attributes }.";
};
dnssec = mkOption {
type = bool;
description = "Enable DNSSEC security for this zone.";
default = true;
};
mx = mkOption {
type = listOf str;
description = "A list of mail servers serving this domain.";
default = [ ];
};
srv-records = mkOption {
type = attrsOf (attrsOf (listOf (submodule srvRecordOpts)));
description = "Map of traffic type to srv records.";
default = { };
example = {
tcp = {
kerberos = {
port = 88;
host = "auth-host.my-domain.com";
};
};
};
};
aliases = mkOption {
type = loaOf str;
default = { };
description = "A mapping of host-alias => hostnames to add to DNS.";
example = {
"music" = "host.dom.com.";
"mail" = "hostname";
};
};
extra-dns-records = mkOption {
type = listOf str;
description = "Records to be inserted verbatim into the DNS zone.";
example = [ "some-host IN CNAME base-host" ];
default = [ ];
};
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;
};
default-host = mkOption {
type = nullOr str;
description =
"IP of the host which will act as the default server for this domain, if any.";
default = null;
};
};
};
hostRecords = host: data:
join-lines ((map (ip: "${host} IN A ${ip}") data.ip-addresses)
++ (map (ip: "${host} IN AAAA ${ip}") data.ipv6-addresses)
++ (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints)
++ (optional (data.rp != null) "${host} IN RP ${data.rp}")
++ (optional (data.description != null)
"${host} IN TXT ${data.description}"));
makeSrvRecords = protocol: type: records:
join-lines (map (record:
"_${type}._${protocol} IN SRV ${toString record.priority} ${
toString record.weight
} ${toString record.port} ${toString record.host}.") records);
makeSrvProtocolRecords = protocol: types:
join-lines (mapAttrsToList (makeSrvRecords protocol) types);
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
mxRecords = mxs: concatStringsSep "\n" (map (mx: "@ IN MX 10 ${mx}.") mxs);
dmarcRecord = dmarc-email:
optionalString (dmarc-email != null) ''
_dmarc IN TXT "v=DMARC1;p=quarantine;sp=quarantine;rua=mailto:${dmarc-email};"'';
nsRecords = dom: ns-hosts:
join-lines (mapAttrsToList (host: _: "@ IN NS ${host}.${dom}.") ns-hosts);
in {
options.fudo.dns = with types; {
enable = mkEnableOption "Enable master DNS services.";
# FIXME: This should allow for AAAA addresses too...
nameservers = mkOption {
type = loaOf (submodule hostOpts);
description = "Map of domain nameserver FQDNs to IP.";
example = {
"ns1.domain.com" = {
ip-addresses = [ "1.1.1.1" ];
ipv6-addresses = [ ];
description = "my fancy dns server";
};
};
};
identity = mkOption {
type = str;
description = "The identity (CH TXT ID.SERVER) of this host.";
};
domains = mkOption {
type = loaOf (submodule domainOpts);
default = { };
description = "A map of domain to domain options.";
};
listen-ips = mkOption {
type = listOf str;
description = "A list of IPs on which to listen for DNS queries.";
example = [ "1.2.3.4" ];
};
};
config = mkIf cfg.enable {
services.nsd = {
enable = true;
identity = cfg.identity;
interfaces = cfg.listen-ips;
zones = mapAttrs' (dom: dom-cfg:
nameValuePair "${dom}." {
dnssec = dom-cfg.dnssec;
data = ''
$ORIGIN ${dom}.
$TTL 12h
@ IN SOA ns1.${dom}. hostmaster.${dom}. (
${toString builtins.currentTime}
5m
2m
6w
5m)
${optionalString (dom-cfg.default-host != null)
"@ IN A ${dom-cfg.default-host}"}
${mxRecords dom-cfg.mx}
$TTL 6h
${nsRecords dom cfg.nameservers}
${join-lines (mapAttrsToList hostRecords cfg.nameservers)}
${dmarcRecord dom-cfg.dmarc-report-address}
${join-lines
(mapAttrsToList makeSrvProtocolRecords dom-cfg.srv-records)}
${join-lines (mapAttrsToList hostRecords dom-cfg.hosts)}
${join-lines (mapAttrsToList cnameRecord dom-cfg.aliases)}
${join-lines dom-cfg.extra-dns-records}
'';
}) cfg.domains;
};
};
}
+52
View File
@@ -0,0 +1,52 @@
{ config, lib, pkgs, ... }:
let
domainOpts = { domain, ... }: {
options = {
domain = mkOption {
type = types.str;
description = "Domain name.";
default = domain;
};
local-networks = mkOption {
type = with types; listOf str;
description =
"A list of networks to be considered trusted on this network.";
default = [ ];
};
local-users = mkOption {
type = with types; listOf str;
description =
"A list of users who should have local (i.e. login) access to _all_ hosts in this domain.";
default = [ ];
};
local-admins = mkOption {
type = with types; listOf str;
description =
"A list of users who should have admin access to _all_ hosts in this domain.";
default = [ ];
};
admin-email = mkOption {
type = types.str;
description = "Email for the administrator of this domain.";
default = "admin@fudo.org";
};
gssapi-realm = mkOption {
type = with types; nullOr str;
description = "GSSAPI (i.e. Kerberos) realm of this domain.";
};
};
};
in {
options.fudo.domains = mkOption {
type = with types; attrsOf (submodule domainOpts);
description = "Domain configurations for all domains known to the system.";
default = { };
};
}
+49
View 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}
'';
};
};
};
}
+151
View File
@@ -0,0 +1,151 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.fudo.git;
databaseOpts = { ... }: {
options = {
name = mkOption {
type = types.str;
description = "Database name.";
};
hostname = mkOption {
type = types.str;
description = "Hostname of the database server.";
};
user = mkOption {
type = types.str;
description = "Database username.";
};
password-file = mkOption {
type = types.path;
description = "File containing the database user's password.";
};
};
};
sshOpts = { ... }: with types; {
options = {
listen-ip = mkOption {
type = str;
description = "IP on which to listen for SSH connections.";
};
listen-port = mkOption {
type = port;
description = "Port on which to listen for SSH connections, on <listen-ip>.";
default = 22;
};
};
};
in {
options.fudo.git = with types; {
enable = mkEnableOption "Enable Fudo git web server.";
hostname = mkOption {
type = str;
description = "Hostname at which this git server is accessible.";
example = "git.fudo.org";
};
site-name = mkOption {
type = str;
description = "Name to use for the git server.";
default = "Fudo Git";
};
database = mkOption {
type = (submodule databaseOpts);
description = "Gitea database options.";
};
repository-dir = mkOption {
type = path;
description = "Path at which to store repositories.";
example = /srv/git/repo;
};
state-dir = mkOption {
type = path;
description = "Path at which to store server state.";
example = /srv/git/state;
};
user = mkOption {
type = with types; nullOr str;
description = "System user as which to run.";
default = "git";
};
local-port = mkOption {
type = port;
description = "Local port to which the Gitea server will bind. Not globally accessible.";
default = 3543;
};
ssh = mkOption {
type = nullOr (submodule sshOpts);
description = "SSH listen configuration.";
default = null;
};
};
config = mkIf cfg.enable {
security.acme.certs.${cfg.hostname}.email = config.fudo.common.admin-email;
services = {
gitea = {
enable = true;
appName = cfg.site-name;
database = {
createDatabase = true;
host = cfg.database.hostname;
name = cfg.database.name;
user = cfg.database.user;
passwordFile = cfg.database.password-file;
type = "postgres";
};
domain = cfg.hostname;
httpAddress = "127.0.0.1";
httpPort = cfg.local-port;
repositoryRoot = toString cfg.repository-dir;
stateDir = toString cfg.state-dir;
rootUrl = "https://${cfg.hostname}/";
user = mkIf (cfg.user != null) cfg.user;
extraConfig = mkIf (cfg.ssh != null) ''
[server]
START_SSH_SERVER = true
SSH_DOMAIN = ${cfg.hostname}
SSH_PORT = ${toString cfg.ssh.listen-port}
SSH_LISTEN_PORT = ${toString cfg.ssh.listen-port}
SSH_LISTEN_HOST = ${cfg.ssh.listen-ip}
'';
};
nginx = {
enable = true;
virtualHosts = {
"${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString cfg.local-port}";
extraConfig = ''
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;
'';
};
};
};
};
};
};
}
+143
View File
@@ -0,0 +1,143 @@
# NOTE: this assumes that postgres is running locally.
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.grafana;
fudo-cfg = config.fudo.common;
database-name = "grafana";
database-user = "grafana";
databaseOpts = { ... }: {
options = {
name = mkOption {
type = types.str;
description = "Database name.";
};
hostname = mkOption {
type = types.str;
description = "Hostname of the database server.";
};
user = mkOption {
type = types.str;
description = "Database username.";
};
password-file = mkOption {
type = types.path;
description = "File containing the database user's password.";
};
};
};
in {
options.fudo.grafana = {
enable = mkEnableOption "Fudo Metrics Display Service";
hostname = mkOption {
type = types.str;
description = "Grafana site hostname.";
example = "fancy-graphs.fudo.org";
};
smtp-username = mkOption {
type = types.str;
description = "Username with which to send email.";
};
smtp-password-file = mkOption {
type = types.path;
description = "Path to a file containing the email user's password.";
};
database = mkOption {
type = (types.submodule databaseOpts);
description = "Grafana database configuration.";
};
admin-password-file = mkOption {
type = types.path;
description = "Path to a file containing the admin user's password.";
};
secret-key-file = mkOption {
type = types.path;
description = "Path to a file containing the server's secret key, used for signatures.";
};
prometheus-host = mkOption {
type = types.str;
description = "The URL of the prometheus data source.";
};
};
config = mkIf cfg.enable {
security.acme.certs.${cfg.hostname}.email = fudo-cfg.admin-email;
services.nginx = {
enable = true;
virtualHosts = {
"${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:3000";
extraConfig = ''
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;
'';
};
};
};
};
services.grafana = {
enable = true;
addr = "127.0.0.1";
protocol = "http";
port = 3000;
domain = "${cfg.hostname}";
rootUrl = "https://${cfg.hostname}/";
security = {
adminPasswordFile = cfg.admin-password-file;
secretKeyFile = cfg.secret-key-file;
};
smtp = {
enable = true;
fromAddress = "metrics@fudo.org";
host = "mail.fudo.org:25";
user = cfg.smtp-username;
passwordFile = cfg.smtp-password-file;
};
database = {
host = cfg.database.hostname;
name = cfg.database.name;
user = cfg.database.user;
passwordFile = cfg.database.password-file;
type = "postgres";
};
provision.datasources = [
{
editable = false;
isDefault = true;
name = cfg.prometheus-host;
type = "prometheus";
url = "https://${cfg.prometheus-host}/";
}
];
};
};
}
+107
View File
@@ -0,0 +1,107 @@
{ config, lib, pkgs, ... }:
with lib;
let
hostOpts = { hostname, ... }: {
options = {
hostname = mkOption {
type = types.str;
description = "Hostname (without domain name).";
default = hostname;
};
domain = mkOption {
type = types.str;
description =
"Domain to which the host belongs, in the form of a domain name.";
default = "fudo.org";
};
local-networks = mkOption {
type = with types; listof str;
description =
"A list of networks to be considered trusted by this host.";
default = [ "127.0.0.0/8" ];
};
profile = mkOption {
# FIXME: get this list from profiles directly
type = with types;
listof (enum "desktop" "laptop" "server" "gateway-server");
description =
"The profile to be applied to the host, determining what software is included.";
};
admin-email = mkOption {
type = with types; nullOr str;
description = "Email for the administrator of this host.";
default = null;
};
hardware-configuration = mkOption {
type = types.attrs;
description =
"The hardware configuration of the host (i.e. the contents of hardware-configuration.nix)";
};
local-users = mkOption {
type = with types; listOf str;
description =
"List of users who should have local (i.e. login) access to the host.";
default = [ ];
};
local-admins = mkOption {
type = with types; listOf str;
description =
"A list of users who should have admin access to this host.";
default = [ ];
};
enable-gui = mkEnableOption "Install desktop GUI software.";
docker-server = mkEnableOption "Enable Docker on the current host.";
};
};
in {
options.fudo.hosts = mkOption {
type = with types; attrsOf (submodule hostOpts);
description = "Host configurations for all hosts known to the system.";
default = { };
};
config = let
hostname = config.instance.hostname;
host-cfg = config.fudo.hosts.${hostname};
site-name = host-cfg.site;
site = config.fudo.site.${site-name};
domain-name = host-cfg.domain;
domain = config.fudo.domain.${domain-name};
in {
networking = {
hostName = config.instance.hostname;
nameservers = site.nameservers;
defaultGateway = site.gateway-v4;
defaultGateway6 = site.gateway-v6;
# Necessary to ensure that Kerberos and Avahi both work. Kerberos needs
# the fqdn of the host, whereas Avahi wants just the simple hostname.`
hosts = { "127.0.0.1" = [ "${hostname}.${domain-name}" "${hostname}" ]; };
};
krb5.libdefaults.default_realm = domain.gssapi-realm;
services.cron.mailto = domain.admin-email;
environment.systemPackages = with pkgs;
mkIf (cfg.docker-server) [ docker nix-prefetch-docker ];
virtualisation.docker = mkIf (cfg.docker-server) {
enable = true;
enableOnBoot = true;
autoprune.enable = true;
};
};
}
+142
View File
@@ -0,0 +1,142 @@
# THROW THIS AWAY, NOT USED
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.hosts.local-network;
gatewayServerOpts = { ... }: {
options = {
enable = mkEnableOption "Turn this host into a network gateway.";
internal-interfaces = mkOption {
type = with types; listOf str;
description =
"List of internal interfaces from which to forward traffic.";
default = [ ];
};
external-interface = mkOption {
type = types.str;
description =
"Interface facing public internet, to which traffic is forwarded.";
};
external-tcp-ports = mkOption {
type = with types; listOf port;
description = "List of TCP ports to open to the outside world.";
default = [ ];
};
external-udp-ports = mkOption {
type = with types; listOf port;
description = "List of UDP ports to open to the outside world.";
default = [ ];
};
};
};
dnsOverHttpsProxy = {
options = {
enable = mkEnableOption "Enable a DNS-over-HTTPS proxy server.";
listen-port = mkOption {
type = types.port;
description = "Port on which to listen for DNS requests.";
default = 53;
};
upstream-dns = mkOption {
type = with types; listOf str;
description = "List of DoH DNS servers to use for recursion.";
default = [ ];
};
bootstrap-dns = mkOption {
type = types.str;
description = "DNS server used to bootstrap the proxy server.";
default = "1.1.1.1";
};
};
};
networkDhcpServerOpts = mkOption {
options = {
enable = mkEnableOption "Enable local DHCP server.";
dns-servers = mkOption {
type = with types; listOf str;
description = "List of DNS servers for clients to use.";
default = [ ];
};
listen-interfaces = mkOption {
type = with types; listOf str;
description = "List of interfaces on which to serve DHCP requests.";
default = [ ];
};
server-ip = mkOption {
type = types.str;
description = "IP address of the server host.";
};
};
};
networkServerOpts = {
options = {
enable = mkEnableOption "Enable local networking server (DNS & DHCP).";
domain = mkOption {
type = types.str;
description = "Local network domain which this host will serve.";
};
dns-listen-addrs = mkOption {
type = with types; listOf str;
description = "List of IP addresses on which to listen for requests.";
default = [ ];
};
dhcp = mkOption {
type = types.submodule networkDhcpServerOpts;
description = "Local DHCP server options.";
};
};
};
in {
options.fudo.hosts.local-network = with types; {
recursive-resolvers = mkOption {
type = listOf str;
description = "DNS server to use for recursive lookups.";
example = "1.2.3.4 port 53";
};
gateway-server = mkOption {
type = submodule gatewayServerOpts;
description = "Gateway server options.";
};
dns-over-https-proxy = mkOption {
type = submodule dnsOverHttpsProxy;
description = "DNS-over-HTTPS proxy server.";
};
networkServerOpts = mkOption {
type = submodule networkServerOpts;
description = "Networking (DNS & DHCP) server for a local network.";
};
};
config = {
fudo.secure-dns-proxy = mkIf cfg.dns-over-https-proxy.enable {
enable = true;
port = cfg.dns-over-https-proxy.listen-port;
upstream-dns = cfg.dns-over-https-proxy.upstream-dns;
bootstrap-dns = cfg.dns-over-https-proxy.bootstrap-dns;
listen-ips = cfg.dns-over-https-proxy.listen-ips;
};
};
}
+117
View File
@@ -0,0 +1,117 @@
lib: site: config: version:
with lib;
let
db-config = if (config.database != null) then
''
type = "${config.database.type}"
pdo_dsn = "${config.database.type}:host=${config.database.hostname};port=${toString config.database.port};dbname=${config.database.name}"
pdo_user = "${config.database.user}"
pdo_password = "${fileContents config.database.password-file}"
''
else "";
in ''
[webmail]
title = "${config.title}"
loading_description = "${config.title}"
favicon_url = "https://${site}/favicon.ico"
theme = "${config.theme}"
allow_themes = On
allow_user_background = Off
language = "en"
language_admin = "en"
allow_languages_on_settings = On
allow_additional_accounts = On
allow_additional_identities = On
messages_per_page = ${toString config.messages-per-page}
attachment_size_limit = ${toString config.max-upload-size}
[interface]
show_attachment_thumbnail = On
new_move_to_folder_button = On
[branding]
[contacts]
enable = On
allow_sync = On
sync_interval = 20
suggestions_limit = 10
${db-config}
[security]
csrf_protection = On
custom_server_signature = "RainLoop"
x_frame_options_header = ""
openpgp = On
admin_login = "admin"
admin_password = ""
allow_admin_panel = Off
allow_two_factor_auth = On
force_two_factor_auth = Off
hide_x_mailer_header = Off
admin_panel_host = ""
admin_panel_key = "admin"
content_security_policy = ""
core_install_access_domain = ""
[login]
default_domain = "${config.domain}"
allow_languages_on_login = On
determine_user_language = On
determine_user_domain = Off
welcome_page = Off
hide_submit_button = On
[plugins]
enable = Off
[defaults]
view_editor_type = "${config.edit-mode}"
view_layout = ${if (config.layout-mode == "bottom") then "2" else "1"}
contacts_autosave = On
mail_use_threads = ${if config.enable-threading then "On" else "Off"}
allow_draft_autosave = On
mail_reply_same_folder = Off
show_images = On
[logs]
enable = ${if config.debug then "On" else "Off"}
[debug]
enable = ${if config.debug then "On" else "Off"}
hide_passwords = On
filename = "log-{date:Y-m-d}.txt"
[social]
google_enable = Off
fb_enable = Off
twitter_enable = Off
dropbox_enable = Off
[cache]
enable = On
index = "v1"
fast_cache_driver = "files"
fast_cache_index = "v1"
http = On
http_expires = 3600
server_uids = On
[labs]
allow_mobile_version = ${if config.enable-mobile then "On" else "Off"}
check_new_password_strength = On
allow_gravatar = On
allow_prefetch = On
allow_smart_html_links = On
cache_system_data = On
date_from_headers = On
autocreate_system_folders = On
allow_ctrl_enter_on_compose = On
favicon_status = On
use_local_proxy_for_external_images = On
detect_image_exif_orientation = On
[version]
current = "${version}"
''
+71
View File
@@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.ipfs;
user-group-entry = group: user:
nameValuePair user { extraGroups = [ group ]; };
user-home-entry = ipfs-path: user:
nameValuePair user { home.sessionVariables = { IPFS_PATH = ipfs-path; }; };
in {
options.fudo.ipfs = with types; {
enable = mkEnableOption "Fudo IPFS";
users = mkOption {
type = listOf str;
description = "List of users with IPFS access.";
default = [ ];
};
user = mkOption {
type = str;
description = "User as which to run IPFS user.";
default = "ipfs";
};
group = mkOption {
type = str;
description = "Group as which to run IPFS user.";
default = "ipfs";
};
api-address = mkOption {
type = str;
description = "Address on which to listen for requests.";
default = "/ip4/127.0.0.1/tcp/5001";
};
automount = mkOption {
type = bool;
description = "Whether to automount /ipfs and /ipns on boot.";
default = true;
};
data-dir = mkOption {
type = str;
description = "Path to store data for IPFS.";
default = "/var/lib/ipfs";
};
};
config = mkIf cfg.enable {
users.users = listToAttrs (map (user-group-entry cfg.group) cfg.users);
services.ipfs = {
enable = true;
apiAddress = cfg.api-address;
autoMount = cfg.automount;
enableGC = true;
user = cfg.user;
group = cfg.group;
dataDir = cfg.data-dir;
};
home-manager.users =
listToAttrs (map (user-home-entry cfg.data-dir) cfg.users);
};
}
+101
View File
@@ -0,0 +1,101 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.auth.kdc;
stringJoin = joiner: attrList:
if (length attrList) == 0 then
""
else
foldr(lAttr: rAttr: "${lAttr}${joiner}${rAttr}") (last attrList) (init attrList);
in {
options.fudo.auth.kdc = {
enable = mkEnableOption "Fudo KDC";
database-path = mkOption {
type = types.str;
description = ''
The path at which to store the database files.
'';
default = "/var/heimdal/heimdal";
};
realm = mkOption {
type = types.str;
description = ''
The realm for which we are the acting KDC.
'';
};
mkey-file = mkOption {
type = types.str;
description = ''
The path to the master key file.
'';
};
acl-file = mkOption {
type = types.str;
description = ''
The path to the Access Control file.
'';
};
bind-addresses = mkOption {
type = with types; listOf str;
description = ''
A list of IP addresses on which to bind.
'';
default = [];
};
};
config = mkIf cfg.enable {
environment = {
systemPackages = [
pkgs.heimdalFull
];
etc."krb5.conf" = {
text = mkAfter ''
[kdc]
database = {
realm = ${cfg.realm}
mkey_file = ${cfg.mkey-file}
acl_file = ${cfg.acl-file}
}
addresses = ${stringJoin " " cfg.bind-addresses}
# Binds to port 80!
enable-http = false
'';
};
};
systemd.services = {
heimdal-kdc = {
enable = true;
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "Heimdal Kerberos Key Distribution Center (ticket server)";
serviceConfig = {
ExecStart = ''${pkgs.heimdalFull}/libexec/heimdal/kdc'';
};
};
heimdal-admin-server = {
enable = true;
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "Heimdal Kerberos Remote Administration Server";
serviceConfig = {
ExecStart = ''${pkgs.heimdalFull}/libexec/heimdal/kadmind'';
};
};
};
};
}
+412
View File
@@ -0,0 +1,412 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.auth.server;
ldapSystemUserOpts = { name, ... }: {
options = {
description = mkOption {
type = types.str;
description = ''
The description of this system user.
'';
};
hashed-password = mkOption {
type = types.str;
description = ''
The password for this user, hashed with ldappasswd.
'';
default = "";
};
};
};
ldapGroupOpts = { name, ... }: {
options = {
gid = mkOption {
type = types.int;
description = ''
The GID number of this group.
'';
};
description = mkOption {
type = types.str;
description = ''
The description of this group.
'';
};
members = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
A list of usernames representing the members of this group.
'';
};
};
};
ldapUserOpts = { name, ... }: {
options = {
uid = mkOption {
type = types.int;
description = ''
The UID number of this user.
'';
};
common-name = mkOption {
type = types.str;
description = ''
The given name of this user.
'';
};
group = mkOption {
type = types.str;
description = ''
The name of the user's primary group.
'';
};
login-shell = mkOption {
type = types.str;
default = "/bin/bash";
description = ''
The user's preferred shell. Default is /bin/bash.
'';
};
description = mkOption {
type = types.str;
default = "Fudo Member";
description = ''
The description of this user.
'';
};
hashed-password = mkOption {
type = types.str;
description = ''
The password for this user, hashed with ldappasswd.
'';
default = "";
};
};
};
stringJoin = joiner: attrList:
if (length attrList) == 0 then
""
else
foldr (lAttr: rAttr: "${lAttr}${joiner}${rAttr}") (last attrList)
(init attrList);
getUserGidNumber = user: group-map: group-map.${user.group}.gid;
attrOr = attrs: attr: value: if attrs ? ${attr} then attrs.${attr} else value;
mkHomeDir = username: user-opts:
if (user-opts.group == "admin") then
"/home/${username}"
else
"/home/${user-opts.group}/${username}";
userLdif = base: name: group-map: opts: ''
dn: uid=${name},ou=members,${base}
uid: ${name}
objectClass: account
objectClass: shadowAccount
objectClass: posixAccount
cn: ${opts.common-name}
uidNumber: ${toString (opts.uid)}
gidNumber: ${toString (getUserGidNumber opts group-map)}
homeDirectory: ${mkHomeDir name opts}
description: ${opts.description}
shadowLastChange: 12230
shadowMax: 99999
shadowWarning: 7
userPassword: ${opts.hashed-password}
'';
systemUserLdif = base: name: opts: ''
dn: cn=${name},${base}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: ${name}
description: ${opts.description}
userPassword: ${opts.hashed-password}
'';
toMemberList = userList:
stringJoin "\n" (map (username: "memberUid: ${username}") userList);
groupLdif = base: name: opts: ''
dn: cn=${name},ou=groups,${base}
objectClass: posixGroup
cn: ${name}
gidNumber: ${toString (opts.gid)}
description: ${opts.description}
${toMemberList opts.members}
'';
systemUsersLdif = base: user-map:
stringJoin "\n"
(mapAttrsToList (name: opts: systemUserLdif base name opts) user-map);
groupsLdif = base: group-map:
stringJoin "\n"
(mapAttrsToList (name: opts: groupLdif base name opts) group-map);
usersLdif = base: group-map: user-map:
stringJoin "\n"
(mapAttrsToList (name: opts: userLdif base name group-map opts) user-map);
in {
options = {
fudo = {
auth = {
server = {
enable = mkEnableOption "Fudo Authentication";
kerberos-host = mkOption {
type = types.str;
description = ''
The name of the host to use for Kerberos authentication.
'';
};
kerberos-keytab = mkOption {
type = types.str;
description = ''
The path to a keytab for the LDAP server, containing a principal for ldap/<hostname>.
'';
};
sslCert = mkOption {
type = types.str;
description = ''
The path to the SSL certificate to use for the server.
'';
};
sslKey = mkOption {
type = types.str;
description = ''
The path to the SSL key to use for the server.
'';
};
sslCACert = mkOption {
type = with types; nullOr str;
description = ''
The path to the SSL CA cert used to sign the certificate.
'';
default = null;
};
organization = mkOption {
type = types.str;
description = ''
The name to use for the organization.
'';
};
base = mkOption {
type = types.str;
description = ''
The base dn of the LDAP server (eg. "dc=fudo,dc=org").
'';
};
rootpw-file = mkOption {
default = "";
type = types.str;
description = ''
The path to a file containing the root password for this database.
'';
};
listen-uris = mkOption {
default = [ ];
type = with types; listOf str;
description = ''
A list of URIs on which the ldap server should listen.
'';
example = [ "ldap://auth.fudo.org" "ldaps://auth.fudo.org" ];
};
users = mkOption {
default = { };
type = with types; loaOf (submodule ldapUserOpts);
example = {
tester = {
uid = 10099;
common-name = "Joe Blow";
hashed-password = "<insert password hash>";
};
};
description = ''
Users to be added to the Fudo LDAP database.
'';
};
groups = mkOption {
default = { };
type = with types; loaOf (submodule ldapGroupOpts);
example = {
admin = {
gid = 1099;
members = [ "tester" ];
};
};
description = ''
Groups to be added to the Fudo LDAP database.
'';
};
system-users = mkOption {
default = { };
type = with types; loaOf (submodule ldapSystemUserOpts);
example = {
replicator = {
description = "System user for database sync";
hashed-password = "<insert password hash>";
};
};
description = ''
System users to be added to the Fudo LDAP database.
'';
};
};
};
};
};
config = mkIf cfg.enable {
environment = {
etc = {
"openldap/sasl2/slapd.conf" = {
mode = "0400";
user = "openldap";
group = "openldap";
text = ''
mech_list: gssapi external
keytab: /etc/ldap/ldap.keytab
'';
};
};
};
systemd.services.openldap = {
environment = { KRB5_KTNAME = cfg.kerberos-keytab; };
};
services.openldap = {
enable = true;
suffix = cfg.base;
rootdn = "cn=admin,${cfg.base}";
rootpwFile = "${cfg.rootpw-file}";
urlList = cfg.listen-uris;
extraConfig = ''
TLSCertificateFile ${cfg.sslCert}
TLSCertificateKeyFile ${cfg.sslKey}
${optionalString (cfg.sslCACert != null)
"TLSCACertificateFile ${cfg.sslCACert}"}
authz-regexp "^uid=auth/([^.]+)\.fudo\.org,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org"
authz-regexp "^uid=[^,/]+/root,cn=fudo\.org,cn=gssapi,cn=auth$" "cn=admin,dc=fudo,dc=org"
authz-regexp "^uid=([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "uid=$1,ou=members,dc=fudo,dc=org"
authz-regexp "^uid=host/([^,/]+),cn=fudo\.org,cn=gssapi,cn=auth$" "cn=$1,ou=hosts,dc=fudo,dc=org"
authz-regexp "^gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth$" "cn=admin,dc=fudo,dc=org"
'';
extraDatabaseConfig = ''
# access to dn=base=""
# by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
# by * read
access to attrs=userPassword,shadowLastChange
by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
by group.exact="cn=admin,ou=members,${cfg.base}" write
by dn.exact="cn=auth_reader,${cfg.base}" read
by dn.exact="cn=replicator,${cfg.base}" read
by self write
by * auth
access to dn.exact="cn=admin,ou=groups,${cfg.base}"
by dn.exact="cn=admin,${cfg.base}" write
by users read
by * none
access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid
by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write
by group.exact="cn=admin,ou=groups,${cfg.base}" write
by users read
by * none
access to dn.subtree="ou=members,${cfg.base}" attrs=cn,sn,homeDirectory,loginShell,gecos,description,homeDirectory,uidNumber,gidNumber
by group.exact="cn=admin,ou=groups,${cfg.base}" write
by dn.exact="cn=user_db_reader,${cfg.base}" read
by users read
by * none
access to dn.exact="cn=admin,ou=groups,${cfg.base}"
by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
by users read
by * none
access to dn.subtree="ou=groups,${cfg.base}" attrs=memberUid
by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
by dn.regex="cn=[a-zA-Z][a-zA-Z0-9_]+,ou=hosts,${cfg.base}" write
by group.exact="cn=admin,ou=groups,${cfg.base}" write
by users read
by * none
access to *
by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage
by users read
by * none
index objectClass,uid eq
'';
declarativeContents = ''
dn: ${cfg.base}
objectClass: top
objectClass: dcObject
objectClass: organization
o: ${cfg.organization}
dn: ou=groups,${cfg.base}
objectClass: organizationalUnit
description: ${cfg.organization} groups
dn: ou=members,${cfg.base}
objectClass: organizationalUnit
description: ${cfg.organization} members
dn: cn=admin,${cfg.base}
objectClass: organizationalRole
cn: admin
description: "Admin User"
${systemUsersLdif cfg.base cfg.system-users}
${groupsLdif cfg.base cfg.groups}
${usersLdif cfg.base cfg.groups cfg.users}
'';
};
};
}
+252
View File
@@ -0,0 +1,252 @@
{ lib, config, pkgs, ... }:
with lib;
let
cfg = config.fudo.local-network;
ip = import ../../lib/ip.nix {};
dns = import ../../lib/dns.nix {};
join-lines = concatStringsSep "\n";
hostOpts = { hostname, ... }: {
options = {
ip-address = mkOption {
type = types.str;
description = ''
The V4 IP of a given host, if any.
'';
};
mac-address = mkOption {
type = with types; nullOr types.str;
description = ''
The MAC address of a given host, if desired for IP reservation.
'';
default = null;
};
ssh-fingerprints = mkOption {
type = with types; listOf str;
description = "A list of DNS SSHFP records for this host.";
default = [];
};
};
};
traceout = out: builtins.trace out out;
in {
options.fudo.local-network = {
enable = mkEnableOption "Enable local network configuration (DHCP & DNS).";
hosts = mkOption {
type = with types; attrsOf (submodule hostOpts);
default = {};
description = "A map of hostname => { host_attributes }.";
};
domain = mkOption {
type = types.str;
description = "The domain to use for the local network.";
};
dns-servers = mkOption {
type = with types; listOf str;
description = "A list of domain name server to use for the local network.";
};
dhcp-interfaces = mkOption {
type = with types; listOf str;
description = "A list of interfaces on which to serve DHCP.";
};
dns-serve-ips = mkOption {
type = with types; listOf str;
description = "A list of IPs on which to server DNS queries.";
};
gateway = mkOption {
type = types.str;
description = "The gateway to use for the local network.";
};
aliases = mkOption {
type = with types; attrsOf str;
default = {};
description = "A mapping of host-alias => hostname to use on the local network.";
};
network = mkOption {
type = types.str;
description = "Network to treat as local.";
};
enable-reverse-mappings = mkOption {
type = types.bool;
description = "Genereate PTR reverse lookup records.";
default = false;
};
dhcp-dynamic-network = mkOption {
type = types.str;
description = ''
The network from which to dynamically allocate IPs via DHCP.
Must be a subnet of <network>.
'';
};
recursive-resolver = mkOption {
type = types.str;
description = "DNS nameserver to use for recursive resolution.";
};
server-ip = mkOption {
type = types.str;
description = "IP of the DNS server.";
};
extra-dns-records = mkOption {
type = with types; listOf str;
description = "Records to be inserted verbatim into the DNS zone.";
example = ["some-host IN CNAME other-host"];
default = [];
};
srv-records = mkOption {
type = dns.srvRecords;
description = "Map of traffic type to srv records.";
default = {};
example = {
tcp = {
kerberos = {
port = 88;
host = "auth-host.my-domain.com";
};
};
};
};
search-domains = mkOption {
type = with types; listOf str;
description = "A list of domains to search for DNS names.";
example = ["my-domain.com" "other-domain.com"];
default = [];
};
# TODO: srv records
};
config = mkIf cfg.enable {
services.dhcpd4 = {
enable = true;
machines = mapAttrsToList (hostname: hostOpts: {
ethernetAddress = hostOpts.mac-address;
hostName = hostname;
ipAddress = hostOpts.ip-address;
}) (filterAttrs (host: hostOpts: hostOpts.mac-address != null) cfg.hosts);
interfaces = cfg.dhcp-interfaces;
extraConfig = ''
subnet ${ip.getNetworkBase cfg.network} netmask ${ip.maskFromV32Network cfg.network} {
authoritative;
option subnet-mask ${ip.maskFromV32Network cfg.network};
option broadcast-address ${ip.networkMaxIp cfg.network};
option routers ${cfg.gateway};
option domain-name-servers ${concatStringsSep " " cfg.dns-servers};
option domain-name "${cfg.domain}";
option domain-search ${join-lines (map (dom: "\"${dom}\"") ([cfg.domain] ++ cfg.search-domains))};
range ${ip.networkMinIp cfg.dhcp-dynamic-network} ${ip.networkMaxButOneIp cfg.dhcp-dynamic-network};
}
'';
};
services.bind = let
blockHostsToZone = block: hosts-data: {
master = true;
name = "${block}.in-addr.arpa";
file = let
# We should add these...but need a domain to assign them to.
# ip-last-el = ip: toInt (last (splitString "." ip));
# used-els = map (host-data: ip-last-el host-data.ip-address) hosts-data;
# unused-els = subtractLists used-els (map toString (range 1 255));
in pkgs.writeText "db.${block}-zone" ''
$ORIGIN ${block}.in-addr.arpa.
$TTL 1h
@ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. (
${toString builtins.currentTime}
1800
900
604800
1800)
@ IN NS ns1.${cfg.domain}.
${join-lines (map hostPtrRecord hosts-data)}
'';
};
ipToBlock = ip: concatStringsSep "." (reverseList (take 3 (splitString "." ip)));
compactHosts = mapAttrsToList (host: data: data // { host = host; }) cfg.hosts;
hostsByBlock = groupBy (host-data: ipToBlock host-data.ip-address) compactHosts;
hostPtrRecord = host-data:
"${last (splitString "." host-data.ip-address)} IN PTR ${host-data.host}.${cfg.domain}.";
blockZones = mapAttrsToList blockHostsToZone hostsByBlock;
hostARecord = host: data: "${host} IN A ${data.ip-address}";
hostSshFpRecords = host: data: join-lines (map (sshfp: "${host} IN SSHFP ${sshfp}") data.ssh-fingerprints);
cnameRecord = alias: host: "${alias} IN CNAME ${host}";
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;
name = cfg.domain;
file = pkgs.writeText "${cfg.domain}-zone" ''
@ IN SOA ns1.${cfg.domain}. hostmaster.${cfg.domain}. (
${toString builtins.currentTime}
5m
2m
6w
5m)
$TTL 1h
@ IN NS ns1.${cfg.domain}.
$ORIGIN ${cfg.domain}.
$TTL 30m
ns1 IN A ${cfg.server-ip}
${join-lines (mapAttrsToList hostARecord cfg.hosts)}
${join-lines (mapAttrsToList hostSshFpRecords cfg.hosts)}
${join-lines (mapAttrsToList cnameRecord cfg.aliases)}
${join-lines cfg.extra-dns-records}
${dns.srvRecordsToBindZone cfg.srv-records}
'';
}
] ++ blockZones;
};
};
}
+205
View File
@@ -0,0 +1,205 @@
{ lib, config, ... }:
with lib;
let
cfg = config.fudo.mail-server;
container-maildir = "/var/lib/mail";
container-statedir = "/var/lib/mail-state";
container-shared = "container/mail-server";
container-postfix-cert = "${container-shared}/postfix/cert.pem";
container-postfix-key = "${container-shared}/postfix/key.pem";
container-dovecot-cert = "${container-shared}/dovecot/cert.pem";
container-dovecot-key = "${container-shared}/dovecot/key.pem";
container-fudo-ca-cert = "${container-shared}/fudo-ca.pem";
# Don't bother with group-id, nixos doesn't seem to use it anyway
container-mail-user = "mailer";
container-mail-user-id = 542;
container-mail-group = "mailer";
fudo-cfg = config.fudo.common;
in rec {
options.fudo.mail-server.container = {
ldap-url = mkOption {
type = types.str;
description = "URL of the LDAP server to use for authentication.";
example = "ldaps://auth.fudo.org/";
};
};
config = mkIf (cfg.enableContainer && !cfg.enable) {
# Disable postfix on thi host--it'll be run in the container instead
services.postfix.enable = false;
# Copy data intended for the container to a path in /etc which can be
# bind-mounted.
environment.etc = {
"${container-postfix-cert}" = {
mode = "0444";
source = cfg.postfix.ssl-certificate;
};
"${container-postfix-key}" = {
mode = "0400";
source = cfg.postfix.ssl-private-key;
};
"${container-dovecot-cert}" = {
mode = "0444";
source = cfg.dovecot.ssl-certificate;
};
"${container-dovecot-key}" = {
mode = "0400";
source = cfg.dovecot.ssl-private-key;
};
"${container-fudo-ca-cert}" = {
mode = "0444";
source = "/etc/nixos/static/fudo_ca.pem";
};
};
security.acme.certs.${cfg.hostname}.email = fudo-cfg.admin-email;
services.nginx = mkIf cfg.monitoring {
enable = true;
virtualHosts = let
proxy-headers = ''
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
'';
trusted-network-string =
optionalString ((length fudo-cfg.local-networks) > 0)
(concatStringsSep "\n"
(map (network: "allow ${network};") fudo-cfg.local-networks)) + ''
deny all;'';
in {
"${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/metrics/postfix" = {
proxyPass = "http://127.0.0.1:9154/metrics";
extraConfig = ''
${proxy-headers}
${trusted-network-string}
'';
};
locations."/metrics/dovecot" = {
proxyPass = "http://127.0.0.1:9166/metrics";
extraConfig = ''
${proxy-headers}
${trusted-network-string}
'';
};
locations."/metrics/rspamd" = {
proxyPass = "http://127.0.0.1:7980/metrics";
extraConfig = ''
${proxy-headers}
${trusted-network-string}
'';
};
};
};
};
containers.mail-server = {
autoStart = true;
bindMounts = {
"${container-maildir}" = {
hostPath = cfg.mail-directory;
isReadOnly = false;
};
"${container-statedir}" = {
hostPath = cfg.state-directory;
isReadOnly = false;
};
"/etc/${container-shared}" = {
hostPath = "/etc/${container-shared}";
isReadOnly = true;
};
};
config = { config, pkgs, ... }: {
environment.systemPackages = with pkgs; [ nmap ];
imports = [ ./mail.nix ];
environment = {
etc = {
"postfix-certs/key.pem" = {
source = "/etc/${container-postfix-key}";
user = config.services.postfix.user;
mode = "0400";
};
"dovecot-certs/key.pem" = {
source = "/etc/${container-dovecot-key}";
user = config.services.dovecot2.user;
mode = "0400";
};
};
};
fudo.mail-server = {
enable = true;
hostname = cfg.hostname;
domain = cfg.domain;
debug = cfg.debug;
monitoring = cfg.monitoring;
state-directory = container-statedir;
mail-directory = container-maildir;
postfix.ssl-certificate = "/etc/${container-postfix-cert}";
postfix.ssl-private-key = "/etc/postfix-certs/key.pem";
dovecot = {
ssl-certificate = "/etc/${container-dovecot-cert}";
ssl-private-key = "/etc/dovecot-certs/key.pem";
ldap = {
# ca = "/etc/${container-fudo-ca-cert}";
server-urls = cfg.dovecot.ldap.server-urls;
reader-dn = cfg.dovecot.ldap.reader-dn;
reader-passwd = cfg.dovecot.ldap.reader-passwd;
};
};
local-domains = cfg.local-domains;
alias-users = cfg.alias-users;
user-aliases = cfg.user-aliases;
sender-blacklist = cfg.sender-blacklist;
recipient-blacklist = cfg.recipient-blacklist;
trusted-networks = cfg.trusted-networks;
mail-user = container-mail-user;
mail-user-id = container-mail-user-id;
mail-group = container-mail-group;
clamav.enable = cfg.clamav.enable;
dkim.signing = cfg.dkim.signing;
};
};
};
};
}
+189
View File
@@ -0,0 +1,189 @@
{ config, lib, pkgs, environment, ... }:
with lib;
let
inherit (lib.strings) concatStringsSep;
cfg = config.fudo.mail-server;
in {
options.fudo.mail-server = {
enable = mkEnableOption "Fudo Email Server";
enableContainer = mkEnableOption ''
Run the mail server in a container.
Mutually exclusive with mail-server.enable.
'';
domain = mkOption {
type = types.str;
description = "The main and default domain name for this email server.";
};
hostname = mkOption {
type = types.str;
description = "The domain name to use for the mail server.";
};
monitoring = mkEnableOption "Enable monitoring for the mail server.";
mail-user = mkOption {
type = types.str;
description = "User to use for mail delivery.";
};
# No group id, because NixOS doesn't seem to use it
mail-group = mkOption {
type = types.str;
description = "Group to use for mail delivery.";
};
mail-user-id = mkOption {
type = types.int;
description = "UID of mail-user.";
};
local-domains = mkOption {
type = with types; listOf str;
description = "A list of domains for which we accept mail.";
default = ["localhost" "localhost.localdomain"];
example = [
"localhost"
"localhost.localdomain"
"somedomain.com"
"otherdomain.org"
];
};
mail-directory = mkOption {
type = types.str;
description = "Path to use for mail storage.";
};
state-directory = mkOption {
type = types.str;
description = "Path to use for state data.";
};
trusted-networks = mkOption {
type = with types; listOf str;
description = "A list of trusted networks, for which we will happily relay without auth.";
example = [
"10.0.0.0/16"
"192.168.0.0/24"
];
};
sender-blacklist = mkOption {
type = with types; listOf str;
description = "A list of email addresses for whom we will not send email.";
default = [];
example = [
"baduser@test.com"
"change-pw@test.com"
];
};
recipient-blacklist = mkOption {
type = with types; listOf str;
description = "A list of email addresses for whom we will not accept email.";
default = [];
example = [
"baduser@test.com"
"change-pw@test.com"
];
};
message-size-limit = mkOption {
type = types.int;
description = "Size of max email in megabytes.";
default = 30;
};
user-aliases = mkOption {
type = with types; loaOf(listOf str);
description = "A map of real user to list of aliases.";
default = {};
example = {
someuser = ["alias0" "alias1"];
};
};
alias-users = mkOption {
type = with types; loaOf(listOf str);
description = "A map of email alias to a list of users.";
example = {
alias = ["realuser0" "realuser1"];
};
};
mailboxes = mkOption {
description = ''
The mailboxes for dovecot.
Depending on the mail client used it might be necessary to change some mailbox's name.
'';
default = [
{
name = "Trash";
auto = "no";
specialUse = "Trash";
}
{
name = "Junk";
auto = "subscribe";
specialUse = "Junk";
}
{
name = "Drafts";
auto = "subscribe";
specialUse = "Drafts";
}
{
name = "Sent";
auto = "subscribe";
specialUse = "Sent";
}
];
};
debug = mkOption {
description = "Enable debugging on mailservers.";
type = types.bool;
default = false;
};
max-user-connections = mkOption {
description = "Max simultaneous connections per user.";
type = types.int;
default = 20;
};
};
imports = [
./mail/dkim.nix
./mail/dovecot.nix
./mail/postfix.nix
./mail/rspamd.nix
./mail/clamav.nix
];
config = mkIf cfg.enable {
users = {
users = {
mailuser = {
isSystemUser = true;
uid = cfg.mail-user-id;
group = "mailgroup";
};
};
groups = {
mailgroup = {
members = ["mailuser"];
};
};
};
};
}
+28
View File
@@ -0,0 +1,28 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.fudo.mail-server;
in {
options.fudo.mail-server.clamav = {
enable = mkOption {
description = "Enable virus scanning with ClamAV.";
type = types.bool;
default = true;
};
};
config = mkIf (cfg.enable && cfg.clamav.enable) {
services.clamav = {
daemon = {
enable = true;
extraConfig = ''
PhishingScanURLs no
'';
};
updater.enable = true;
};
};
}
+114
View File
@@ -0,0 +1,114 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.mail-server;
createDomainDkimCert = dom:
let
dkim_key = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key";
dkim_txt = "${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.txt";
in
''
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then
${cfg.dkim.package}/bin/opendkim-genkey -s "${cfg.dkim.selector}" \
-d "${dom}" \
--bits="${toString cfg.dkim.key-bits}" \
--directory="${cfg.dkim.key-directory}"
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.private" "${dkim_key}"
mv "${cfg.dkim.key-directory}/${cfg.dkim.selector}.txt" "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkim.selector}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.local-domains);
keyTable = pkgs.writeText "opendkim-KeyTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.local-domains
(dom: "${dom} ${dom}:${cfg.dkim.selector}:${cfg.dkim.key-directory}/${dom}.${cfg.dkim.selector}.key")));
signingTable = pkgs.writeText "opendkim-SigningTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.local-domains (dom: "${dom} ${dom}")));
dkim = config.services.opendkim;
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
in
{
options.fudo.mail-server.dkim = {
signing = mkOption {
type = types.bool;
default = true;
description = "Enable dkim signatures for mail.";
};
key-directory = mkOption {
type = types.str;
default = "/var/dkim";
description = "Path to use to store DKIM keys.";
};
selector = mkOption {
type = types.str;
default = "mail";
description = "Name to use for mail-signing keys.";
};
key-bits = mkOption {
type = types.int;
default = 2048;
description = ''
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
If you have already deployed a key with a different number of bits than specified
here, then you should use a different selector (dkimSelector). In order to get
this package to generate a key with the new number of bits, you will either have to
change the selector or delete the old key file.
'';
};
package = mkOption {
type = types.package;
default = pkgs.opendkim;
description = "OpenDKIM package to use.";
};
};
config = mkIf (cfg.dkim.signing && cfg.enable) {
services.opendkim = {
enable = true;
selector = cfg.dkim.selector;
domains = "csl:${builtins.concatStringsSep "," cfg.local-domains}";
configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization relaxed/simple
UMask 0002
Socket ${dkim.socket}
KeyTable file:${keyTable}
SigningTable file:${signingTable}
'' + (lib.optionalString cfg.debug ''
Syslog yes
SyslogSuccess yes
LogWhy yes
''));
};
users.users = {
"${config.services.postfix.user}" = {
extraGroups = [ "${config.services.opendkim.group}" ];
};
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart = lib.mkForce "${cfg.dkim.package}/bin/opendkim ${escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
};
};
systemd.tmpfiles.rules = [
"d '${cfg.dkim.key-directory}' - ${config.services.opendkim.user} ${config.services.opendkim.group} - -"
];
};
}
+286
View File
@@ -0,0 +1,286 @@
{ config, lib, pkgs, environment, ... }:
with lib;
let
cfg = config.fudo.mail-server;
state-directory = "${cfg.state-directory}/dovecot";
pipe-bin = pkgs.stdenv.mkDerivation {
name = "pipe_bin";
src = ./dovecot/pipe_bin;
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
buildCommand = ''
mkdir -p $out/pipe/bin
cp $src/* $out/pipe/bin/
chmod a+x $out/pipe/bin/*
patchShebangs $out/pipe/bin
for file in $out/pipe/bin/*; do
wrapProgram $file \
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
done
'';
};
ldap-conf = filename: config:
let
ssl-config = if config.ca == null then ''
tls = no
tls_require_cert = try
'' else ''
tls_ca_cert_file = ${config.ca}
tls = yes
tls_require_cert = try
'';
in
pkgs.writeText filename ''
uris = ${concatStringsSep " " config.server-urls}
ldap_version = 3
dn = ${config.reader-dn}
dnpass = ${config.reader-passwd}
auth_bind = yes
auth_bind_userdn = uid=%u,ou=members,dc=fudo,dc=org
base = dc=fudo,dc=org
${ssl-config}
'';
ldap-passwd-entry = ldap-config: ''
passdb {
driver = ldap
args = ${ldap-conf "ldap-passdb.conf" ldap-config}
}
'';
ldapOpts = {
options = with types; {
ca = mkOption {
type = nullOr str;
description = "The path to the CA cert used to sign the LDAP server certificate.";
default = null;
};
server-urls = mkOption {
type = listOf str;
description = "A list of LDAP server URLs used for authentication.";
};
reader-dn = mkOption {
type = str;
description = ''
DN to use for reading user information. Needs access to homeDirectory,
uidNumber, gidNumber, and uid, but not password attributes.
'';
};
reader-passwd = mkOption {
type = str;
description = ''
Password for the user specified in ldap-reader-dn.
'';
};
};
};
dovecot-user = config.services.dovecot2.user;
in {
options.fudo.mail-server.dovecot = with types; {
ssl-private-key = mkOption {
type = str;
description = "Location of the server SSL private key.";
};
ssl-certificate = mkOption {
type = str;
description = "Location of the server SSL certificate.";
};
ldap = mkOption {
type = nullOr (submodule ldapOpts);
default = null;
description = ''
LDAP auth server configuration. If omitted, the server will use local authentication.
'';
};
};
config = mkIf cfg.enable {
services.prometheus.exporters.dovecot = mkIf cfg.monitoring {
enable = true;
scopes = ["user" "global"];
listenAddress = "127.0.0.1";
port = 9166;
socketPath = "/var/run/dovecot2/old-stats";
};
services.dovecot2 = {
enable = true;
enableImap = true;
enableLmtp = true;
enablePop3 = true;
enablePAM = cfg.dovecot.ldap == null;
createMailUser = true;
mailUser = cfg.mail-user;
mailGroup = cfg.mail-group;
mailLocation = "maildir:${cfg.mail-directory}/%u/";
sslServerCert = cfg.dovecot.ssl-certificate;
sslServerKey = cfg.dovecot.ssl-private-key;
modules = [ pkgs.dovecot_pigeonhole ];
protocols = [ "sieve" ];
sieveScripts = {
after = builtins.toFile "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
fileinto "Junk";
stop;
}
'';
};
mailboxes = cfg.mailboxes;
extraConfig = ''
#Extra Config
${optionalString cfg.monitoring ''
# The prometheus exporter still expects an older style of metrics
mail_plugins = $mail_plugins old_stats
service old-stats {
unix_listener old-stats {
user = dovecot-exporter
group = dovecot-exporter
}
}
''}
${lib.optionalString cfg.debug ''
mail_debug = yes
auth_debug = yes
verbose_ssl = yes
''}
protocol imap {
mail_max_userip_connections = ${toString cfg.max-user-connections}
mail_plugins = $mail_plugins imap_sieve
}
protocol pop3 {
mail_max_userip_connections = ${toString cfg.max-user-connections}
}
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
mail_access_groups = ${cfg.mail-group}
ssl = required
# When looking up usernames, just use the name, not the full address
auth_username_format = %n
service lmtp {
# Enable logging in debug mode
${optionalString cfg.debug "executable = lmtp -L"}
# Unix socket for postfix to deliver messages via lmtp
unix_listener dovecot-lmtp {
user = "postfix"
group = ${cfg.mail-group}
mode = 0600
}
# Drop privs, since all mail is owned by one user
# user = ${cfg.mail-user}
# group = ${cfg.mail-group}
user = root
}
auth_mechanisms = login plain
${optionalString (cfg.dovecot.ldap != null)
(ldap-passwd-entry cfg.dovecot.ldap)}
userdb {
driver = static
args = uid=${toString cfg.mail-user-id} home=${cfg.mail-directory}/%u
}
# Used by postfix to authorize users
service auth {
unix_listener auth {
mode = 0660
user = "${config.services.postfix.user}"
group = ${cfg.mail-group}
}
unix_listener auth-userdb {
mode = 0660
user = "${config.services.postfix.user}"
group = ${cfg.mail-group}
}
}
service auth-worker {
user = root
}
service imap {
vsz_limit = 1024M
}
namespace inbox {
separator = "/"
inbox = yes
}
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve
sieve_default = file:/var/sieve/%u/default.sieve
sieve_default_name = default
# From elsewhere to Spam folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:${state-directory}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${state-directory}/imap_sieve/report-ham.sieve
sieve_pipe_bin_dir = ${pipe-bin}/pipe/bin
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}
recipient_delimiter = +
lmtp_save_to_detail_mailbox = yes
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
'';
};
systemd.services.dovecot2.preStart = ''
mkdir -p '${state-directory}'
chown ${dovecot-user}:${cfg.mail-group} '${state-directory}'
rm -rf '${state-directory}/imap_sieve'
mkdir '${state-directory}/imap_sieve'
cp -p "${./dovecot/imap_sieve}"/*.sieve '${state-directory}/imap_sieve/'
for k in "${state-directory}/imap_sieve"/*.sieve ; do
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
done
chown -R '${dovecot-user}:${cfg.mail-group}' '${state-directory}/imap_sieve'
chown '${cfg.mail-user}:${cfg.mail-group}' ${cfg.mail-directory}
chmod g+w ${cfg.mail-directory}
'';
};
}
@@ -0,0 +1,15 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "${1}";
}
if string "${mailbox}" "Trash" {
stop;
}
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
@@ -0,0 +1,7 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
+319
View File
@@ -0,0 +1,319 @@
{ config, pkgs, lib, ... }:
with lib;
let
inherit (lib.strings) concatStringsSep;
cfg = config.fudo.mail-server;
# The final newline is important
write-entries = filename: entries:
let
entries-string = (concatStringsSep "\n" entries);
in builtins.toFile filename ''
${entries-string}
'';
make-user-aliases = entries:
concatStringsSep "\n"
(mapAttrsToList (user: aliases:
concatStringsSep "\n"
(map (alias: "${alias} ${user}") aliases))
entries);
make-alias-users = domains: entries:
concatStringsSep "\n"
(flatten
(mapAttrsToList (alias: users:
(map (domain:
"${alias}@${domain} ${concatStringsSep "," users}")
domains))
entries));
policyd-spf = pkgs.writeText "policyd-spf.conf" (
cfg.postfix.policy-spf-extra-config
+ (lib.optionalString cfg.debug ''
debugLevel = 4
''));
submission-header-cleanup-rules = pkgs.writeText "submission_header_cleanup_rules" (''
# Removes sensitive headers from mails handed in via the submission port.
# See https://thomas-leister.de/mailserver-debian-stretch/
# Uses "pcre" style regex.
/^Received:/ IGNORE
/^X-Originating-IP:/ IGNORE
/^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE
'');
blacklist-postfix-entry = sender: "${sender} REJECT";
blacklist-postfix-file = filename: entries:
write-entries filename entries;
sender-blacklist-file = blacklist-postfix-file "reject_senders"
(map blacklist-postfix-entry cfg.sender-blacklist);
recipient-blacklist-file = blacklist-postfix-file "reject_recipients"
(map blacklist-postfix-entry cfg.recipient-blacklist);
# A list of domains for which we accept mail
virtual-mailbox-map-file = write-entries "virtual_mailbox_map"
(map (domain: "@${domain} OK") (cfg.local-domains ++ [cfg.domain]));
sender-login-map-file = let
escapeDot = (str: replaceStrings ["."] ["\\."] str);
in write-entries "sender_login_maps"
(map (domain: "/^(.*)@${escapeDot domain}$/ \${1}") (cfg.local-domains ++ [cfg.domain]));
mapped-file = name: "hash:/var/lib/postfix/conf/${name}";
pcre-file = name: "pcre:/var/lib/postfix/conf/${name}";
in {
options.fudo.mail-server.postfix = {
ssl-private-key = mkOption {
type = types.str;
description = "Location of the server SSL private key.";
};
ssl-certificate = mkOption {
type = types.str;
description = "Location of the server SSL certificate.";
};
policy-spf-extra-config = mkOption {
type = types.lines;
default = "";
example = ''
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
'';
description = ''
Extra configuration options for policyd-spf. This can be use to among
other things skip spf checking for some IP addresses.
'';
};
};
config = mkIf cfg.enable {
services.prometheus.exporters.postfix = mkIf cfg.monitoring {
enable = true;
systemd.enable = true;
showqPath = "/var/lib/postfix/queue/public/showq";
user = config.services.postfix.user;
group = config.services.postfix.group;
};
services.postfix = {
enable = true;
domain = cfg.domain;
origin = cfg.domain;
hostname = cfg.hostname;
destination = ["localhost" "localhost.localdomain"];
# destination = ["localhost" "localhost.localdomain" cfg.hostname] ++
# cfg.local-domains;;
enableHeaderChecks = true;
enableSmtp = true;
enableSubmission = true;
mapFiles."reject_senders" = sender-blacklist-file;
mapFiles."reject_recipients" = recipient-blacklist-file;
mapFiles."virtual_mailbox_map" = virtual-mailbox-map-file;
mapFiles."sender_login_map" = sender-login-map-file;
# TODO: enable!
# headerChecks = [ { action = "REDIRECT spam@example.com"; pattern = "/^X-Spam-Flag:/"; } ];
networks = cfg.trusted-networks;
virtual = ''
${make-user-aliases cfg.user-aliases}
${make-alias-users ([cfg.domain] ++ cfg.local-domains) cfg.alias-users}
'';
sslCert = cfg.postfix.ssl-certificate;
sslKey = cfg.postfix.ssl-private-key;
config = {
virtual_mailbox_domains = cfg.local-domains ++ [cfg.domain];
# virtual_mailbox_base = "${cfg.mail-directory}/";
virtual_mailbox_maps = mapped-file "virtual_mailbox_map";
virtual_uid_maps = "static:${toString cfg.mail-user-id}";
virtual_gid_maps = "static:${toString config.users.groups."${cfg.mail-group}".gid}";
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# NOTE: it's important that this ends with /, to indicate Maildir format!
# mail_spool_directory = "${cfg.mail-directory}/";
message_size_limit = toString(cfg.message-size-limit * 1024 * 1024);
smtpd_banner = "${cfg.hostname} ESMTP NO UCE";
tls_eecdh_strong_curve = "prime256v1";
tls_eecdh_ultra_curve = "secp384r1";
policy-spf_time_limit = "3600s";
smtp_host_lookup = "dns, native";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_local_domain = "fudo.org";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_tls_security_options = "noanonymous";
smtpd_sender_login_maps = (pcre-file "sender_login_map");
disable_vrfy_command = "yes";
recipient_delimiter = "+";
milter_protocol = "6";
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
smtpd_milters = [
"unix:/run/rspamd/rspamd-milter.sock"
"unix:/var/run/opendkim/opendkim.sock"
];
non_smtpd_milters = [
"unix:/run/rspamd/rspamd-milter.sock"
"unix:/var/run/opendkim/opendkim.sock"
];
smtpd_relay_restrictions = [
"permit_mynetworks"
"permit_sasl_authenticated"
"reject_unauth_destination"
"reject_unauth_pipelining"
"reject_unauth_destination"
"reject_unknown_sender_domain"
];
smtpd_sender_restrictions = [
"check_sender_access ${mapped-file "reject_senders"}"
"permit_mynetworks"
"permit_sasl_authenticated"
"reject_unknown_sender_domain"
];
smtpd_recipient_restrictions = [
"check_sender_access ${mapped-file "reject_recipients"}"
"permit_mynetworks"
"permit_sasl_authenticated"
"check_policy_service unix:private/policy-spf"
"reject_unknown_recipient_domain"
"reject_unauth_pipelining"
"reject_unauth_destination"
"reject_invalid_hostname"
"reject_non_fqdn_hostname"
"reject_non_fqdn_sender"
"reject_non_fqdn_recipient"
];
smtpd_helo_restrictions = [
"permit_mynetworks"
"reject_invalid_hostname"
"permit"
];
# Handled by submission
smtpd_tls_security_level = "may";
smtpd_tls_eecdh_grade = "ultra";
# Disable obselete protocols
smtpd_tls_protocols = [
"TLSv1.2"
"TLSv1.1"
"!TLSv1"
"!SSLv2"
"!SSLv3"
];
smtp_tls_protocols = [
"TLSv1.2"
"TLSv1.1"
"!TLSv1"
"!SSLv2"
"!SSLv3"
];
smtpd_tls_mandatory_protocols = [
"TLSv1.2"
"TLSv1.1"
"!TLSv1"
"!SSLv2"
"!SSLv3"
];
smtp_tls_mandatory_protocols = [
"TLSv1.2"
"TLSv1.1"
"!TLSv1"
"!SSLv2"
"!SSLv3"
];
smtp_tls_ciphers = "high";
smtpd_tls_ciphers = "high";
smtp_tls_mandatory_ciphers = "high";
smtpd_tls_mandatory_ciphers = "high";
smtpd_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
smtpd_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
smtp_tls_mandatory_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
smtp_tls_exclude_ciphers = ["MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL"];
tls_preempt_cipherlist = "yes";
smtpd_tls_auth_only = "yes";
smtpd_tls_loglevel = "1";
tls_random_source = "dev:/dev/urandom";
};
submissionOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = cfg.domain;
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_restrictions = "reject_sender_login_mismatch,reject_unknown_sender_domain";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup";
};
masterConfig = {
"policy-spf" = {
type = "unix";
privileged = true;
chroot = false;
command = "spawn";
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";
private = false;
chroot = false;
maxproc = 0;
command = "cleanup";
args = ["-o" "header_checks=pcre:${submission-header-cleanup-rules}"];
};
};
};
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = {
after = [ "dovecot2.service" ]
++ (lib.optional cfg.dkim.signing "opendkim.service");
requires = [ "dovecot2.service" ]
++ (lib.optional cfg.dkim.signing "opendkim.service");
};
};
}
+88
View File
@@ -0,0 +1,88 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.fudo.mail-server;
in {
config = mkIf cfg.enable {
services.prometheus.exporters.rspamd.enable = true;
services.rspamd = {
enable = true;
locals = {
"milter_headers.conf" = {
text = ''
extended_spam_headers = yes;
'';
};
"antivirus.conf" = {
text = ''
clamav {
action = "reject";
symbol = "CLAM_VIRUS";
type = "clamav";
log_clean = true;
servers = "/run/clamav/clamd.ctl";
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
}
'';
};
};
overrides = {
"milter_headers.conf" = {
text = ''
extended_spam_headers = true;
'';
};
};
workers.rspamd_proxy = {
type = "rspamd_proxy";
bindSockets = [{
socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664";
}];
count = 1; # Do not spawn too many processes of this type
extraConfig = ''
milter = yes; # Enable milter mode
timeout = 120s; # Needed for Milter usually
upstream "local" {
default = yes; # Self-scan upstreams are always default
self_scan = yes; # Enable self-scan
}
'';
};
workers.controller = {
type = "controller";
count = 1;
bindSockets = [
"localhost:11334"
{
socket = "/run/rspamd/worker-controller.sock";
mode = "0666";
}
];
includes = [];
};
};
systemd.services.rspamd = {
requires = (optional cfg.clamav.enable "clamav-daemon.service");
after = (optional cfg.clamav.enable "clamav-daemon.service");
};
systemd.services.postfix = {
after = [ "rspamd.service" ];
requires = [ "rspamd.service" ];
};
users.extraUsers.${config.services.postfix.user}.extraGroups = [ config.services.rspamd.group ];
};
}
+64
View File
@@ -0,0 +1,64 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.minecraft-server;
in {
options.fudo.minecraft-server = {
enable = mkEnableOption "Start a minecraft server.";
package = mkOption {
type = types.package;
description = "Minecraft package to use.";
default = pkgs.minecraft-server_1_15_1;
};
data-dir = mkOption {
type = types.path;
description = "Path at which to store minecraft data.";
};
world-name = mkOption {
type = types.str;
description = "Name of the server world (used in saves etc).";
};
motd = mkOption {
type = types.str;
description = "Welcome message for newcomers.";
};
game-mode = mkOption {
type = types.enum ["survival" "creative" "adventure" "spectator"];
description = "Game mode of the server.";
default = "survival";
};
difficulty = mkOption {
type = types.int;
description = "Difficulty level, where 0 is peaceful and 3 is hard.";
default = 2;
};
};
config = mkIf cfg.enable {
environment.systemPackages = [
cfg.package
];
services.minecraft-server = {
enable = true;
package = cfg.package;
dataDir = cfg.data-dir;
eula = true;
declarative = true;
serverProperties = {
level-name = cfg.world-name;
motd = cfg.motd;
difficulty = cfg.difficulty;
gamemode = cfg.game-mode;
};
};
};
}
+93
View File
@@ -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}
'';
};
};
};
}
+60
View File
@@ -0,0 +1,60 @@
{ lib, config, pkgs, ... }:
with lib;
let
inherit (lib.strings) concatStringsSep;
cfg = config.fudo.node-exporter;
fudo-cfg = config.fudo.common;
allow-network = network: "allow ${network};";
in {
options.fudo.node-exporter = {
enable = mkEnableOption "Enable a Prometheus node exporter with some reasonable settings.";
hostname = mkOption {
type = types.str;
description = "Hostname from which to export statistics.";
};
};
config = mkIf cfg.enable {
security.acme.certs.${cfg.hostname}.email = fudo-cfg.admin-email;
services = {
# This'll run an exporter at localhost:9100
prometheus.exporters.node = {
enable = true;
enabledCollectors = [ "systemd" ];
listenAddress = "127.0.0.1";
port = 9100;
user = "node";
};
# ...And this'll expose the above to the outside world, or at least the
# list of trusted networks, with SSL protection.
nginx = {
enable = true;
virtualHosts."${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/metrics/node" = {
extraConfig = ''
${concatStringsSep "\n" (map allow-network fudo-cfg.local-networks)}
allow 127.0.0.0/16;
deny all;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
'';
proxyPass = "http://127.0.0.1:9100/metrics";
};
};
};
};
};
}
+116
View File
@@ -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);
};
}
+329
View File
@@ -0,0 +1,329 @@
{ config, lib, pkgs, environment, ... }:
with lib;
let
cfg = config.fudo.postgresql;
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-file = mkOption {
type = with types; nullOr str;
description = "A file containing the user's (plaintext) password.";
default = null;
};
databases = mkOption {
type = with types; attrsOf (submodule userDatabaseOpts);
description = "Map of databases to required database/table perms.";
default = { };
example = {
my_database = {
access = "ALL PRIVILEGES";
entity-access = { "ALL TABLES" = "SELECT"; };
};
};
};
};
};
databaseOpts = { dbname, ... }: {
options = {
users = mkOption {
type = with types; listOf str;
description =
"A list of users who should have full access to this database.";
default = [ ];
};
};
};
filterPasswordedUsers = filterAttrs (user: opts: opts.password-file != null);
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: join-lines (map makeEntry networks);
makeLocalUserPasswordEntries = 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 {
options.fudo.postgresql = {
enable = mkEnableOption "Fudo PostgreSQL Server";
ssl-private-key = mkOption {
type = types.str;
description = "Location of the server SSL private key.";
};
ssl-certificate = mkOption {
type = types.str;
description = "Location of the server SSL certificate.";
};
keytab = mkOption {
type = types.str;
description = "Location of the server Kerberos keytab.";
};
local-networks = mkOption {
type = with types; listOf str;
description = "A list of networks from which to accept connections.";
example = [ "10.0.0.1/16" ];
default = [ ];
};
users = mkOption {
type = with types; loaOf (submodule userOpts);
description = "A map of users to user attributes.";
example = {
sampleUser = {
password-file = "/path/to/password/file";
databases = {
some_database = {
access = "CONNECT";
entity-access = { "TABLE some_table" = "SELECT,UPDATE"; };
};
};
};
};
default = { };
};
databases = mkOption {
type = with types; loaOf (submodule databaseOpts);
description = "A map of databases to database options.";
default = { };
};
socket-directory = mkOption {
type = types.str;
description = "Directory in which to place unix sockets.";
default = "/run/postgresql";
};
socket-group = mkOption {
type = types.str;
description = "Group for accessing sockets.";
default = "postgres_local";
};
local-users = mkOption {
type = with types; listOf str;
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 {
environment = {
systemPackages = with pkgs; [ postgresql_11_gssapi ];
etc = {
"postgresql/private/privkey.pem" = {
mode = "0400";
user = "postgres";
group = "postgres";
source = cfg.ssl-private-key;
};
"postgresql/cert.pem" = {
mode = "0444";
user = "postgres";
group = "postgres";
source = cfg.ssl-certificate;
};
"postgresql/private/postgres.keytab" = {
mode = "0400";
user = "postgres";
group = "postgres";
source = cfg.keytab;
};
};
};
users.groups = {
${cfg.socket-group} = { members = [ "postgres" ] ++ cfg.local-users; };
};
services.postgresql = {
enable = true;
package = pkgs.postgresql_11_gssapi;
enableTCPIP = true;
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
ensureUsers = ((mapAttrsToList (username: attrs: {
name = username;
ensurePermissions = userDatabaseAccess username attrs.databases;
}) 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'
ssl = true
ssl_cert_file = '/etc/postgresql/cert.pem'
ssl_key_file = '/etc/postgresql/private/privkey.pem'
unix_socket_directories = '${cfg.socket-directory}'
unix_socket_group = '${cfg.socket-group}'
unix_socket_permissions = 0777
'';
authentication = lib.mkForce ''
${makeLocalUserPasswordEntries cfg.users}
local all all ident
# host-local
host all all 127.0.0.1/32 gss include_realm=0 krb_realm=FUDO.ORG
host all all ::1/128 gss include_realm=0 krb_realm=FUDO.ORG
# local networks
${makeNetworksEntry cfg.local-networks}
'';
};
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*
'';
};
};
};
}
+208
View File
@@ -0,0 +1,208 @@
{ config, lib, pkgs, ... }:
with lib;
let
inherit (lib.strings) concatStringsSep;
cfg = config.fudo.prometheus;
fudo-cfg = config.fudo.common;
in {
options.fudo.prometheus = {
enable = mkEnableOption "Fudo Prometheus Data-Gathering Server";
service-discovery-dns = mkOption {
type = with types; loaOf (listOf str);
description = ''
A map of exporter type to a list of domains to use for service discovery.
'';
example = {
node = [ "node._metrics._tcp.my-domain.com" ];
postfix = [ "postfix._metrics._tcp.my-domain.com" ];
};
default = {
dovecot = [];
node = [];
postfix = [];
rspamd = [];
};
};
static-targets = mkOption {
type = with types; loaOf (listOf str);
description = ''
A map of exporter type to a list of host:ports from which to collect metrics.
'';
example = {
node = [ "my-host.my-domain:1111" ];
};
default = {
dovecot = [];
node = [];
postfix = [];
rspamd = [];
};
};
docker-hosts = mkOption {
type = with types; listOf str;
description = ''
A list of explicit <host:port> docker targets from which to gather node data.
'';
default = [];
};
push-url = mkOption {
type = with types; nullOr str;
description = ''
The <host:port> that services can use to manually push data.
'';
default = null;
};
push-address = mkOption {
type = with types; nullOr str;
description = ''
The <host:port> address on which to listen for incoming data.
'';
default = null;
};
hostname = mkOption {
type = with types; str;
description = "The hostname upon which Prometheus will serve.";
example = "my-metrics-server.fudo.org";
};
};
config = mkIf cfg.enable {
security.acme.certs.${cfg.hostname}.email = fudo-cfg.admin-email;
services.nginx = {
enable = true;
virtualHosts = {
"${cfg.hostname}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:9090";
extraConfig = ''
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;
${optionalString ((length fudo-cfg.local-networks) > 0)
(concatStringsSep "\n" (map (network: "allow ${network};") fudo-cfg.local-networks)) + "\ndeny all;"}
'';
};
};
};
};
services.prometheus = {
enable = true;
webExternalUrl = "https://${cfg.hostname}";
listenAddress = "127.0.0.1:9090";
scrapeConfigs = [
{
job_name = "docker";
honor_labels = false;
static_configs = [
{
targets = cfg.docker-hosts;
}
];
}
{
job_name = "node";
scheme = "https";
metrics_path = "/metrics/node";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.node;
}
];
static_configs = [
{
targets = cfg.static-targets.node;
}
];
}
{
job_name = "dovecot";
scheme = "https";
metrics_path = "/metrics/dovecot";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.dovecot;
}
];
static_configs = [
{
targets = cfg.static-targets.dovecot;
}
];
}
{
job_name = "postfix";
scheme = "https";
metrics_path = "/metrics/postfix";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.postfix;
}
];
static_configs = [
{
targets = cfg.static-targets.postfix;
}
];
}
{
job_name = "rspamd";
scheme = "https";
metrics_path = "/metrics/rspamd";
honor_labels = false;
dns_sd_configs = [
{
names = cfg.service-discovery-dns.rspamd;
}
];
static_configs = [
{
targets = cfg.static-targets.rspamd;
}
];
}
];
pushgateway = {
enable = if (cfg.push-url != null) then true else false;
web = {
external-url = if cfg.push-url == null then
cfg.push-address
else
cfg.push-url;
listen-address = cfg.push-address;
};
};
};
};
}
+62
View File
@@ -0,0 +1,62 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.fudo.secure-dns-proxy;
in {
options.fudo.secure-dns-proxy = {
enable = mkEnableOption "Enable a DNS server using an encrypted upstream source.";
port = mkOption {
type = types.port;
description = "Port on which to listen for DNS queries.";
default = 53;
};
upstream-dns = mkOption {
type = with types; listOf str;
description = ''
The upstream DNS services to use, in a format useable by dnsproxy.
See: https://github.com/AdguardTeam/dnsproxy
'';
default = ["https://cloudflare-dns.com/dns-query"];
};
bootstrap-dns = mkOption {
type = types.str;
description = "A simple DNS server from which HTTPS DNS can be bootstrapped, if necessary.";
default = "1.1.1.1";
};
listen-ips = mkOption {
type = with types; listOf str;
description = "A list of local IP addresses on which to listen.";
default = ["0.0.0.0"];
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
dnsproxy
];
systemd.services.secure-dns-proxy = {
enable = true;
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "DNS Proxy for secure DNS lookups";
serviceConfig = let
upstreams = map (upstream: "-u ${upstream}") cfg.upstream-dns;
upstream-line = concatStringsSep " " upstreams;
listen-line = concatStringsSep " "
(map (listen: "-l ${listen}") cfg.listen-ips);
cmd = "${pkgs.dnsproxy}/bin/dnsproxy -p ${toString cfg.port} ${upstream-line} ${listen-line} -b ${cfg.bootstrap-dns}";
in {
ExecStart = cmd;
};
};
};
}
+47
View File
@@ -0,0 +1,47 @@
{ config, lib, pkgs, ... }:
let
siteOpts = { site, ... }: {
options = {
site = mkOption {
type = types.str;
description = "Site name.";
default = site;
};
gateway-v4 = mkOption {
type = with types; nullOr str;
description = "Gateway to use for public ipv4 internet access.";
default = null;
};
gateway-v6 = mkOption {
type = with types; nullOr str;
description = "Gateway to use for public ipv6 internet access.";
default = null;
};
gateway-host = mkOption {
type = with types; nullOr str;
description = "Identity of the host to act as a gateway.";
default = null;
};
enable-monitoring =
mkEnableOption "Enable site-wide monitoring with prometheus.";
nameservers = mkOption {
type = with types; listOf str;
description = "List of nameservers to be used by hosts at this site.";
default = [ ];
};
};
};
in {
options.fudo.sites = mkOption {
type = with types; attrsOf (submodule domainOpts);
description = "Site configurations for all sites known to the system.";
default = { };
};
}
+70
View File
@@ -0,0 +1,70 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.slynk;
initScript = port: load-paths: let
load-path-string =
concatStringsSep " " (map (path: "\"${path}\"") load-paths);
in pkgs.writeText "slynk.lisp" ''
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
(ql:quickload :slynk)
(setf asdf:*central-registry*
(append asdf:*central-registry*
(list ${load-path-string})))
(slynk:create-server :port ${toString port} :dont-close t)
(dolist (var '("LD_LIBRARY_PATH"))
(format t "~S: ~S~%" var (sb-unix::posix-getenv var)))
(loop (sleep 60))
'';
lisp-libs = with pkgs.lispPackages; [
alexandria
asdf-package-system
asdf-system-connections
cl_plus_ssl
cl-ppcre
quicklisp
quri
uiop
usocket
];
in {
options.fudo.slynk = {
enable = mkEnableOption "Enable Slynk emacs common lisp server.";
port = mkOption {
type = types.int;
description = "Port on which to open a Slynk server.";
default = 4005;
};
};
config = mkIf cfg.enable {
systemd.user.services.slynk = {
description = "Slynk Common Lisp server.";
serviceConfig = let
load-paths = (map (pkg: "${pkg}/lib/common-lisp/") lisp-libs);
in {
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
ExecStart = "${pkgs.sbcl}/bin/sbcl --load ${initScript cfg.port load-paths}";
Restart = "on-failure";
PIDFile = "/run/slynk.$USERNAME.pid";
};
path = with pkgs; [
gcc
glibc # for getent
file
];
environment = {
LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib";
};
};
};
}
+43
View File
@@ -0,0 +1,43 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.fudo.system;
in {
options.fudo.system = {
disableTransparentHugePages = mkOption {
type = types.bool;
description = ''
Disable transparent huge pages (recommended for database loads, in
particular for Redis.
'';
default = false;
};
postHugePageServices = mkOption {
type = with types; listOf str;
description = "List of systemd services that should wait until after THP are disabled.";
default = [];
example = ["redis.service"];
};
tmpOnTmpfs = mkOption {
type = types.bool;
description = "Put tmp filesystem on tmpfs (needs enough RAM).";
default = true;
};
};
config = mkIf cfg.disableTransparentHugePages {
systemd.services.disableHugePages = {
description = "Turn off Transparent Huge Pages (https://www.kernel.org/doc/Documentation/vm/transhuge.txt)";
after = [ "sysinit.target" "localfs-target" ];
before = cfg.postHugePageServices;
enable = true;
serviceConfig = {
ExecStart = "/bin/sh -c 'echo never | tee /sys/kernel/mm/transparent_hugepage/enabled > /dev/null";
Type = "oneshot";
};
};
};
}
+212
View File
@@ -0,0 +1,212 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.users;
systemUserOpts = { username, ... }: {
options = {
username = mkOption {
type = types.str;
description = "The system user's login name.";
default = username;
};
description = mkOption {
type = types.str;
description = "Description of this system user's purpose or role";
};
ldap-hashed-password = mkOption {
type = types.str;
description =
"LDAP-formatted hashed password for this user. Generate with slappasswd.";
};
};
};
userOpts = { username, ... }: {
options = {
username = mkOption {
type = types.str;
description = "The user's login name.";
default = username;
};
uidNumber = mkOption {
type = types.int;
description = "Unique UID number for the user.";
};
common-name = mkOption {
type = types.str;
description = "The user's common or given name.";
};
primary-group = mkOption {
type = types.str;
description = "Primary group to which the user belongs.";
};
login-shell = mkOption {
type = with types; nullOr shellPackage;
description = "The user's preferred shell.";
};
description = mkOption {
type = types.str;
default = "Fudo Member";
description = "A description of this user's role.";
};
ldap-hashed-passwd = mkOption {
type = with types; nullOr str;
description =
"LDAP-formatted hashed password, used for email and other services. Use slappasswd to generate the properly-formatted password.";
default = null;
};
login-hashed-passwd = mkOption {
type = with types; nullOr str;
description =
"Hashed password for shell, used for shell access to hosts. Use mkpasswd to generate the properly-formatted password.";
default = null;
};
ssh-authorized-keys = mkOption {
type = with types; listOf str;
description = "SSH public keys this user can use to log in.";
default = [ ];
};
home-manager-config = mkOption {
type = with types; nullOr attrs;
description = "Home Manager configuration for the given user.";
default = null;
};
home-directory = mkOption {
type = types.str;
description = "Default home directory for the given user.";
};
};
};
groupOpts = { group-name, ... }: {
options = {
group-name = mkOption {
type = with types; nullOr str;
default = group-name;
description = "Group name.";
};
description = mkOption {
type = types.str;
description = "Description of the group or it's purpose.";
};
members = mkOption {
type = with types; listOf str;
default = [ ];
description = "A list of users who are members of the current group.";
};
gidNumber = mkOption {
type = types.int;
description = "GID number of the group.";
};
};
};
in {
options.fudo = {
users = mkOption {
type = with types; attrsOf (submodule userOpts);
description = "Users";
default = { };
};
groups = mkOption {
type = with types; attrsOf (submodule groupOpts);
description = "Groups";
default = { };
};
system-users = mkOption {
type = with types; attrsOf (submodule systemUserOpts);
description = "System users (probably not what you're looking for!)";
default = { };
};
};
config = let
local-host = config.fudo.common.hostname;
local-domain = config.fudo.common.domain;
local-user-list = config.fudo.hosts."${local-host}".local-users;
domain-user-list = config.fudo.domains."${local-domain}".local-users;
local-users = getAttrs (local-user-list ++ domain-user-list) cfg.users;
local-group-list = config.fudo.hosts."${local-host}".local-groups;
domain-group-list = config.fudo.domains."${local-domain}".local-groups;
local-groups = getAttrs (local-group-list ++ domain-group-list) cfg.groups;
in {
fudo.auth.ldap = let
ldapUsers = (filterAttrs
(username: userOpts: userOpts.ldap-hashed-password != null)) cfg.users;
list-includes = list: el: isNull (findFirst (this: this == el) list null);
filterExistingUsers = users: group-members:
let user-list = attrNames users;
in filter (username: list-includes user-list username) users;
in {
users = mapAttrs (username: userOpts: {
uid = userOpts.uid;
group = userOpts.primary-group;
common-name = userOpts.common-name;
hashed-password = userOpts.ldap-hashed-password;
}) ldapUsers;
groups = mapAttrs (groupname: groupOpts: {
gid = groupOpts.gid-number;
description = groupOpts.description;
members = filterExistingUsers ldapUsers groupOpts.members;
}) cfg.groups;
system-users = mapAttrs (username: userOpts: {
description = userOpts.description;
hashed-password = userOpts.ldap-hashed-passwd;
}) cfg.system-users;
};
users = {
users = mapAttrs (username: userOpts: {
isNormalUser = true;
uid = userOpts.uidNumber;
createHome = true;
description = userOpts.common-name;
group = userOpts.primary-group;
home = userOpts.home;
hashedPassword = userOpts.login-hashed-passwd;
openssh.authorizedKeys.keys = userOpts.ssh-authorized-keys;
}) local-users;
groups = mapAttrs (groupname: groupOpts: {
gid = groupOpts.gidNumber;
description = groupOpts.description;
members = filterExistingUsers localUsers groupOpts.members;
}) local-groups;
};
home-manager.users = let
home-manager-users =
filterAttrs (username: userOpts: userOpts.home-manager-config != null)
local-users;
in mapAttrs (username: userOpts: userOpts.home-manager-config)
home-manager-users;
};
}
+126
View File
@@ -0,0 +1,126 @@
{ pkgs, lib, config, ... }:
with lib;
let
cfg = config.fudo.vpn;
generate-pubkey-pkg = name: privkey:
pkgs.runCommand "wireguard-${name}-pubkey" {
WIREGUARD_PRIVATE_KEY = privkey;
} ''
mkdir $out
PUBKEY=$(echo $WIREGUARD_PRIVATE_KEY | ${pkgs.wireguard-tools}/bin/wg pubkey)
echo $PUBKEY > $out/pubkey.key
'';
generate-client-config = privkey-file: server-pubkey: network: server-ip: listen-port: dns-servers: ''
[Interface]
Address = ${ip.networkMinIp network}
PrivateKey = ${fileContents privkey-file}
ListenPort = ${toString listen-port}
DNS = ${concatStringsSep ", " dns-servers}
[Peer]
PublicKey = ${server-pubkey}
Endpoint = ${server-ip}:${toString listen-port}
AllowedIps = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
'';
generate-peer-entry = peer-name: peer-privkey-path: peer-allowed-ips: let
peer-pkg = generate-pubkey-pkg "client-${peer-name}" (fileContents peer-privkey-path);
pubkey-path = "${peer-pkg}/pubkey.key";
in {
publicKey = fileContents pubkey-path;
allowedIPs = peer-allowed-ips;
};
in {
options.fudo.vpn = with types; {
enable = mkEnableOption "Enable Fudo VPN";
network = mkOption {
type = str;
description = "Network range to assign this interface.";
default = "10.100.0.0/16";
};
private-key-file = mkOption {
type = str;
description = "Path to the secret key (generated with wg [genkey/pubkey]).";
example = "/path/to/secret.key";
};
listen-port = mkOption {
type = port;
description = "Port on which to listen for incoming connections.";
default = 51820;
};
dns-servers = mkOption {
type = listOf str;
description = "A list of dns servers to pass to clients.";
default = ["1.1.1.1" "8.8.8.8"];
};
server-ip = mkOption {
type = str;
description = "IP of this WireGuard server.";
};
peers = mkOption {
type = loaOf str;
description = "A map of peers to shared private keys.";
default = {};
example = {
peer0 = "/path/to/priv.key";
};
};
};
config = mkIf cfg.enable {
environment.etc = let
peer-data = imap1 (i: peer:{
name = peer.name;
privkey-path = peer.privkey-path;
network-range = let
base = ip.intToIpv4
((ip.ipv4ToInt (ip.getNetworkBase cfg.network)) + (i * 256));
in "${base}/24";
}) (mapAttrsToList (name: privkey-path: {
name = name;
privkey-path = privkey-path;
}) cfg.peers);
server-pubkey-pkg = generate-pubkey-pkg "server-pubkey" (fileContents cfg.private-key-file);
server-pubkey = fileContents "${server-pubkey-pkg}/pubkey.key";
in listToAttrs
(map (peer: nameValuePair "wireguard/clients/${peer.name}.conf" {
mode = "0400";
user = "root";
group = "root";
text = generate-client-config
peer.privkey-path
server-pubkey
peer.network-range
cfg.server-ip
cfg.listen-port
cfg.dns-servers;
}) peer-data);
networking.wireguard = {
enable = true;
interfaces.wgtun0 = {
generatePrivateKeyFile = false;
ips = [ cfg.network ];
listenPort = cfg.listen-port;
peers = mapAttrsToList
(name: private-key: generate-peer-entry name private-key ["0.0.0.0/0" "::/0"])
cfg.peers;
privateKeyFile = cfg.private-key-file;
};
};
};
}
+352
View File
@@ -0,0 +1,352 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.fudo.webmail;
inherit (lib.strings) concatStringsSep;
webmail-user = "webmail-php";
webmail-group = "webmail-php";
base-data-path = "/var/rainloop";
fastcgi-conf = builtins.toFile "fastcgi.conf" ''
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
'';
site-packages = mapAttrs (site: site-cfg:
pkgs.rainloop-community.overrideAttrs (oldAttrs: {
# Not sure how to correctly specify this arg...
#dataPath = "${base-data-path}/${site}";
# Overwriting, to correctly create data dir
installPhase = ''
mkdir $out
cp -r rainloop/* $out
rm -rf $out/data
ln -s ${base-data-path}/${site} $out/data
ln -s ${site-cfg.favicon} $out/favicon.ico
'';
})) cfg.sites;
siteOpts = { site-host, ... }: {
options = {
title = mkOption {
type = types.str;
description = "Webmail site title";
example = "My Webmail";
};
debug = mkOption {
type = types.bool;
description = "Turn debug logs on.";
default = false;
};
mail-server = mkOption {
type = types.str;
description = "Mail server from which to send & recieve email.";
default = "mail.fudo.org";
};
favicon = mkOption {
type = types.str;
description = "URL of the site favicon";
example = "https://www.somepage.com/fav.ico";
};
messages-per-page = mkOption {
type = types.int;
description = "Default number of messages to show per page";
default = 30;
};
max-upload-size = mkOption {
type = types.int;
description = "Size limit in MB for uploaded files";
default = 30;
};
theme = mkOption {
type = types.str;
description = "Default theme to use for this webmail site.";
default = "Default";
};
# Ideally, don't even allow admin logins, since they'll just add state that can be clobbered
# admin-password = mkOption {
# type = types.str;
# description = "Password to use for the admin user";
# };
domain = mkOption {
type = types.str;
description = "Domain for which the server acts as webmail server";
};
edit-mode = mkOption {
type = types.enum [ "Plain" "Html" "PlainForced" "HtmlForced" ];
description = "Default text editing mode for email";
default = "Html";
};
layout-mode = mkOption {
type = types.enum [ "side" "bottom" ];
description = "Layout mode to use for email preview.";
default = "side";
};
enable-threading = mkOption {
type = types.bool;
description = "Whether to enable threading for email.";
default = true;
};
enable-mobile = mkOption {
type = types.bool;
description = "Whether to enable a mobile site view.";
default = true;
};
database = mkOption {
type = with types; nullOr (submodule databaseOpts);
description = "Database configuration for storing contact data.";
example = {
name = "my_db";
host = "db.domain.com";
user = "my_user";
password-file = /path/to/some/file.pw;
};
default = null;
};
admin-email = mkOption {
type = types.str;
description = "Email of administrator of this site.";
default = "admin@fudo.org";
};
};
};
databaseOpts = { ... }: {
options = {
type = mkOption {
type = types.enum [ "pgsql" "mysql" ];
description = "Driver to use when connecting to the database.";
default = "pgsql";
};
hostname = mkOption {
type = types.str;
description = "Name of host running the database.";
example = "my-db.domain.com";
};
port = mkOption {
type = types.int;
description = "Port on which the database server is listening.";
default = 5432;
};
name = mkOption {
type = types.str;
description =
"Name of the database containing contact info. <user> must have access.";
default = "rainloop_contacts";
};
user = mkOption {
type = types.str;
description = "User as which to connect to the database.";
};
password-file = mkOption {
type = types.str;
description = "Password to use when connecting to the database.";
};
};
};
in {
options.fudo.webmail = {
enable = mkEnableOption "Enable a RainLoop webmail server.";
sites = mkOption {
type = with types; (loaOf (submodule siteOpts));
description = "A map of webmail sites to site configurations.";
example = {
"webmail.domain.com" = {
title = "My Awesome Webmail";
layout-mode = "side";
favicon = "/path/to/favicon.ico";
admin-password = "shh-don't-tell";
};
};
};
};
config = mkIf cfg.enable {
users = {
users = {
${webmail-user} = {
isSystemUser = true;
description = "Webmail PHP FPM user";
group = webmail-group;
};
};
groups = {
${webmail-group} = {
members = [ webmail-user config.services.nginx.user ];
};
};
};
security.acme.certs = mapAttrs'
(site: site-cfg: nameValuePair site { email = site-cfg.admin-email; })
cfg.sites;
services = {
phpfpm = {
pools.webmail = {
settings = {
"pm" = "dynamic";
"pm.max_children" = 50;
"pm.start_servers" = 5;
"pm.min_spare_servers" = 1;
"pm.max_spare_servers" = 8;
};
phpOptions = ''
memory_limit = 500M
'';
# Not working....see chmod below
user = webmail-user;
group = webmail-group;
};
};
nginx = {
enable = true;
virtualHosts = mapAttrs (site: site-cfg: {
enableACME = true;
forceSSL = true;
root = "${site-packages.${site}}";
locations = {
"/" = { index = "index.php"; };
"/data" = {
extraConfig = ''
deny all;
return 403;
'';
};
};
extraConfig = ''
location ~ \.php$ {
expires -1;
include ${fastcgi-conf};
fastcgi_index index.php;
fastcgi_pass unix:${config.services.phpfpm.pools.webmail.socket};
}
'';
}) cfg.sites;
};
};
systemd.services = {
webmail-init = let
link-configs = concatStringsSep "\n" (mapAttrsToList (site: site-cfg:
let
cfg-file = builtins.toFile "${site}-rainloop.cfg"
(import ./include/rainloop.nix lib site site-cfg
site-packages.${site}.version);
domain-cfg = builtins.toFile "${site}-domain.cfg" ''
imap_host = "${site-cfg.mail-server}"
imap_port = 143
imap_secure = "TLS"
imap_short_login = On
sieve_use = Off
sieve_allow_raw = Off
sieve_host = ""
sieve_port = 4190
sieve_secure = "None"
smtp_host = "${site-cfg.mail-server}"
smtp_port = 587
smtp_secure = "TLS"
smtp_short_login = On
smtp_auth = On
smtp_php_mail = Off
white_list = ""
'';
in ''
${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/configs
${pkgs.coreutils}/bin/cp ${cfg-file} ${base-data-path}/${site}/_data_/_default_/configs/application.ini
${pkgs.coreutils}/bin/mkdir -p ${base-data-path}/${site}/_data_/_default_/domains/
${pkgs.coreutils}/bin/cp ${domain-cfg} ${base-data-path}/${site}/_data_/_default_/domains/${site-cfg.domain}.ini
'') cfg.sites);
scriptPkg = (pkgs.writeScriptBin "webmail-init.sh" ''
#!${pkgs.bash}/bin/bash -e
${link-configs}
${pkgs.coreutils}/bin/chown -R ${webmail-user}:${webmail-group} ${base-data-path}
${pkgs.coreutils}/bin/chmod -R ug+w ${base-data-path}
'');
in {
requiredBy = [ "nginx.service" ];
description =
"Initialize webmail service directories prior to starting nginx.";
script = "${scriptPkg}/bin/webmail-init.sh";
};
phpfpm-webmail-socket-perm = {
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}
'';
};
};
nginx = {
requires =
[ "webmail-init.service" "phpfpm-webmail-socket-perm.service" ];
};
};
};
}
+32
View File
@@ -0,0 +1,32 @@
{ config, lib, pkgs, ... }:
with lib;
let
networkOpts = { network, ... }: {
options = {
network = mkOption {
type = types.str;
description = "Name of wireless network.";
default = network;
};
key = mkOption {
type = types.str;
description = "Secret key for wireless network.";
};
};
};
in {
option.fudo.wireless-networks = mkOption {
type = with types; listOf (submodule networkOpts);
description = "A map of wireless networks to attributes (including key).";
default = { };
};
config = {
wireless.networks =
mapAttrs (network: networkOpts: { psk = networkOpts.key; })
config.fudo.wireless-networks;
};
}
+6
View File
@@ -0,0 +1,6 @@
{ lib, ... }:
{
ip = import ./fudolib/ip.nix { };
dns = import ./fudolib/dns.nix { };
}
+19 -12
View File
@@ -1,15 +1,17 @@
{ pkgs ? import <nixpkgs> {}, ... }:
{ lib, ... }:
with pkgs.lib;
with lib;
let
join-lines = concatStringsSep "\n";
makeSrvRecords = protocol: type: records:
join-lines (map (record:
"_${type}._${protocol} IN SRV ${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.")
records);
"_${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);
makeSrvProtocolRecords = protocol: types:
join-lines (mapAttrsToList (makeSrvRecords protocol) types);
srvRecordOpts = with types; {
options = {
@@ -39,20 +41,25 @@ let
};
srvRecordPair = domain: protocol: type: record: {
"_${type}._${protocol}.${domain}" = "${toString record.priority} ${toString record.weight} ${toString record.port} ${record.host}.";
"_${type}._${protocol}.${domain}" =
"${toString record.priority} ${toString record.weight} ${
toString record.port
} ${record.host}.";
};
in rec {
srvRecords = with types; attrsOf (attrsOf (listOf (submodule srvRecordOpts)));
srvRecordsToBindZone = srvRecords: join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords);
srvRecordsToBindZone = srvRecords:
join-lines (mapAttrsToList makeSrvProtocolRecords srvRecords);
concatMapAttrs = f: attrs: concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs);
concatMapAttrs = f: attrs:
concatMap (x: x) (mapAttrsToList (key: val: f key val) attrs);
srvRecordsToPairs = domain: srvRecords:
listToAttrs
(concatMapAttrs (protocol: types:
concatMapAttrs (type: records: map (srvRecordPair domain protocol type) records) types)
srvRecords);
listToAttrs (concatMapAttrs (protocol: types:
concatMapAttrs
(type: records: map (srvRecordPair domain protocol type) records) types)
srvRecords);
}
+80
View File
@@ -0,0 +1,80 @@
{ lib, ... }:
with lib;
let
joinString = concatStringsSep;
pow = x: e: if (e == 0) then 1 else x * (pow x (e - 1));
generateNBits = n:
let
helper = n: c:
if (c == n) then pow 2 c else (pow 2 c) + (helper n (c + 1));
in if (n <= 0) then
throw "Can't generate 0 or fewer bits"
else
helper (n - 1) 0;
rightPadBits = int: bits: bitOr int (generateNBits bits);
reverseIpv4 = ip: joinString "." (reverseList (splitString "." ip));
intToBinaryList = int:
let
helper = int: cur:
let curExp = pow 2 cur;
in if (curExp > int) then
[ ]
else
[ (if ((bitAnd curExp int) > 0) then 1 else 0) ]
++ (helper int (cur + 1));
in reverseList (helper int 0);
leftShift = int: n: int * (pow 2 n);
rightShift = int: n: int / (pow 2 n);
in rec {
ipv4ToInt = ip:
let els = map toInt (reverseList (splitString "." ip));
in foldr (a: b: a + b) 0 (imap0 (i: el: (leftShift el (i * 8))) els);
intToIpv4 = int:
joinString "."
(map (i: toString (bitAnd (rightShift int (i * 8)) 255)) [ 3 2 1 0 ]);
maskFromV32Network = network:
let
fullMask = ipv4ToInt "255.255.255.255";
insignificantBits = 32 - (getNetworkMask network);
in intToIpv4
(leftShift (rightShift fullMask insignificantBits) insignificantBits);
networkMinIp = network: intToIpv4 (1 + (ipv4ToInt (getNetworkBase network)));
networkMaxIp = network:
intToIpv4 (rightPadBits (ipv4ToInt (getNetworkBase network))
(32 - (getNetworkMask network)));
# To avoid broadcast IP...
networkMaxButOneIp = network:
intToIpv4 ((rightPadBits (ipv4ToInt (getNetworkBase network))
(32 - (getNetworkMask network))) - 1);
ipv4OnNetwork = ip: network:
let
ip-int = ipv4ToInt ip;
net-min = networkMinIp network;
net-max = networkMaxIp network;
in (ip-int >= networkMinIp) && (ip-int <= networkMaxIp);
getNetworkMask = network: toInt (elemAt (splitString "/" network) 1);
getNetworkBase = network:
let
ip = elemAt (splitString "/" network) 0;
insignificantBits = 32 - (getNetworkMask network);
in intToIpv4
(leftShift (rightShift (ipv4ToInt ip) insignificantBits) insignificantBits);
}
+212
View File
@@ -0,0 +1,212 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.informis.cl-gemini;
lisp-libs = with pkgs.lispPackages; [
asdf-package-system
asdf-system-connections
alexandria
asdf-package-system
asdf-system-connections
cl_plus_ssl
cl-ppcre
quicklisp
quri
uiop
usocket
];
launchServer = ip: port: root: public-dir: key: cert: slynk-port: feeds-string: textfiles-archive:
pkgs.writeText "launch-server.lisp" ''
(load (merge-pathnames "quicklisp/setup.lisp" (user-homedir-pathname)))
(ql:quickload :slynk)
(ql:quickload :cl-gemini)
${optionalString (slynk-port != null) "(slynk:create-server :port ${toString slynk-port} :dont-close t)"}
${feeds-string}
(cl-gemini:start-gemini-server "${ip}" "${key}" "${cert}"
:port ${toString port}
:document-root "${root}"
:textfiles-root "${textfiles-archive}"
:file-cmd "${pkgs.file}/bin/file"
:log-stream *standard-output*
:threaded t
:separate-thread t)
(loop (sleep 60))
'';
sbcl-with-ssl = pkgs.sbcl.overrideAttrs (oldAttrs: rec {
extraLibs = with pkgs; [
openssl_1_1.dev
];
});
feedOpts = with types; {
options = {
url = mkOption {
type = str;
description = "Base URI of the feed, i.e. the URI corresponding to the feed path.";
example = "gemini://my.server/path/to/feedfiles";
};
title = mkOption {
type = str;
description = "Title of given feed.";
example = "My Fancy Feed";
};
path = mkOption {
type = str;
description = "Path to Gemini files making up the feed.";
example = "/path/to/feed";
};
};
};
register-feed = name: opts: ''
(cl-gemini:register-feed :name "${name}" :title "${opts.title}" :path "${opts.path}" :base-uri "${opts.url}")'';
register-feeds = feeds:
concatStringsSep "\n"
(mapAttrsToList register-feed feeds);
in {
options.informis.cl-gemini = with types; {
enable = mkEnableOption "Enable the cl-gemini server.";
port = mkOption {
type = port;
description = "Port on which to serve Gemini traffic.";
default = 1965;
};
server-ip = mkOption {
type = str;
description = "IP on which to serve Gemini traffic.";
example = "1.2.3.4";
};
document-root = mkOption {
type = str;
description = "Root at which to look for gemini files.";
example = "/my/gemini/root";
};
user-public = mkOption {
type = str;
description = "Subdirectory of user homes to check for gemini files.";
default = "gemini-public";
};
ssl-private-key = mkOption {
type = path;
description = "Path to the pem-encoded server private key.";
example = /path/to/secret/key.pem;
};
ssl-certificate = mkOption {
type = path;
description = "Path to the pem-encoded server public certificate.";
example = /path/to/cert.pem;
};
slynk-port = mkOption {
type = nullOr port;
description = "Port on which to open a slynk server, if any.";
default = null;
};
feeds = mkOption {
type = loaOf (submodule feedOpts);
description = "Feeds to generate and make available (as eg. /feed/name.xml).";
example = {
diary = {
title = "My Diary";
path = "/path/to/my/gemfiles/";
url = "gemini://my.host/blog-path/";
};
};
default = {};
};
textfiles-archive = mkOption {
type = str;
description = "A path containing only gemini & text files.";
example = "/path/to/textfiles/";
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
cl-gemini
];
users.users = {
cl-gemini = {
isSystemUser = true;
group = "nogroup";
createHome = true;
home = "/var/lib/cl-gemini";
};
};
environment.etc = {
"cl-gemini/key.pem" = {
mode = "0400";
user = "cl-gemini";
source = cfg.ssl-private-key;
};
"cl-gemini/cert.pem" = {
mode = "0444";
user = "cl-gemini";
source = cfg.ssl-certificate;
};
};
systemd.services.cl-gemini = {
description = "cl-gemini Gemini server (https://gemini.circumlunar.space/)";
serviceConfig = let
feed-registrations = register-feeds cfg.feeds;
in {
ExecStartPre = "${pkgs.lispPackages.quicklisp}/bin/quicklisp init";
ExecStart = "${sbcl-with-ssl}/bin/sbcl --load ${
launchServer
cfg.server-ip
cfg.port
cfg.document-root
cfg.user-public
"/etc/cl-gemini/key.pem"
"/etc/cl-gemini/cert.pem"
cfg.slynk-port
feed-registrations
cfg.textfiles-archive
}";
Restart = "on-failure";
PIDFile = "/run/cl-gemini.$USERNAME.uid";
User = "cl-gemini";
};
environment = {
LD_LIBRARY_PATH = "${pkgs.openssl_1_1.out}/lib";
CL_SOURCE_REGISTRY = concatStringsSep ":"
(["${config.users.users.cl-gemini.home}/quicklisp/quicklisp"] ++
(map
(pkg: "${pkg}//")
(lisp-libs ++ [pkgs.cl-gemini])));
};
path = with pkgs; [
gcc
file
getent
];
wantedBy = [ "default.target" ];
};
};
}
+12
View File
@@ -0,0 +1,12 @@
{ config, lib, pkgs, ... }:
{
options.instance = {
hostname = mkOption {
type = types.str;
description = ''
Hostname of this specific host (without domain).
'';
};
};
}
-63
View File
@@ -1,63 +0,0 @@
{ pkgs ? import <nixpkgs> {}, ... }:
with pkgs.lib;
let
joinString = concatStringsSep;
pow = x: e: if (e == 0) then 1 else x * (pow x (e - 1));
generateNBits = n: let
helper = n: c: if (c == n) then pow 2 c else (pow 2 c) + (helper n (c + 1));
in if (n <= 0) then throw "Can't generate 0 or fewer bits" else helper (n - 1) 0;
rightPadBits = int: bits: bitOr int (generateNBits bits);
reverseIpv4 = ip: joinString "." (reverseList (splitString "." ip));
intToBinaryList = int: let
helper = int: cur: let
curExp = pow 2 cur;
in if (curExp > int) then
[]
else
[(if ((bitAnd curExp int) > 0) then 1 else 0)] ++ (helper int (cur + 1));
in reverseList (helper int 0);
leftShift = int: n: int * (pow 2 n);
rightShift = int: n: int / (pow 2 n);
in rec {
ipv4ToInt = ip: let
els = map toInt (reverseList (splitString "." ip));
in foldr (a: b: a + b) 0 (imap0 (i: el: (leftShift el (i * 8))) els);
intToIpv4 = int: joinString "." (map (i: toString (bitAnd (rightShift int (i * 8)) 255)) [ 3 2 1 0 ]);
maskFromV32Network = network: let
fullMask = ipv4ToInt "255.255.255.255";
insignificantBits = 32 - (getNetworkMask network);
in intToIpv4 (leftShift (rightShift fullMask insignificantBits) insignificantBits);
networkMinIp = network: intToIpv4 (1 + (ipv4ToInt (getNetworkBase network)));
networkMaxIp = network: intToIpv4 (rightPadBits (ipv4ToInt (getNetworkBase network)) (32 - (getNetworkMask network)));
# To avoid broadcast IP...
networkMaxButOneIp = network: intToIpv4 ((rightPadBits (ipv4ToInt (getNetworkBase network)) (32 - (getNetworkMask network))) - 1);
ipv4OnNetwork = ip: network: let
ip-int = ipv4ToInt ip;
net-min = networkMinIp network;
net-max = networkMaxIp network;
in
(ip-int >= networkMinIp) && (ip-int <= networkMaxIp);
getNetworkMask = network: toInt (elemAt (splitString "/" network) 1);
getNetworkBase = network: let
ip = elemAt (splitString "/" network) 0;
insignificantBits = 32 - (getNetworkMask network);
in intToIpv4 (leftShift (rightShift (ipv4ToInt ip) insignificantBits) insignificantBits);
}
-14
View File
@@ -1,14 +0,0 @@
{ lib }:
with lib;
{
recursiveMergeAttrs = a: b: let
commonAttrs = intersectLists (attrNames a) (attrNames b);
aAttrs = subtractLists (attrNames a) commonAttrs;
bAttrs = subtractLists (attrNames b) commonAttrs;
aSide = (filterAttrs (k: v: elem k aAttrs) a);
bSide = (filterAttrs (k: v: elem k bAttrs) b);
common = (foldr (a: b: a // b) {}
(map (k: { ${k} = a.${k} // b.${k}; }) commonAttrs));
in aSide // bSide // common;
}