Still very broken...moving to start testing
This commit is contained in:
@@ -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
|
||||
];
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{ ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./dns.nix
|
||||
];
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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.";
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 = { };
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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}/";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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}"
|
||||
''
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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} - -"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
|
||||
@@ -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");
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 ];
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.fudo.netinfo-email;
|
||||
|
||||
make-script = server: port: target: pkgs.writeText "netinfo-script.rb" ''
|
||||
#!${pkgs.ruby}/bin/ruby
|
||||
|
||||
require 'net/smtp'
|
||||
|
||||
raise RuntimeError.new("NETINFO_SMTP_USERNAME not set!") if not ENV['NETINFO_SMTP_USERNAME']
|
||||
user = ENV['NETINFO_SMTP_USERNAME']
|
||||
|
||||
raise RuntimeError.new("NETINFO_SMTP_PASSWD not set!") if not ENV['NETINFO_SMTP_PASSWD']
|
||||
passwd = ENV['NETINFO_SMTP_PASSWD']
|
||||
|
||||
hostname = `${pkgs.inetutils}/bin/hostname -f`.strip
|
||||
date = `${pkgs.coreutils}/bin/date +%Y-%m-%d`.strip
|
||||
email_date = `${pkgs.coreutils}/bin/date`
|
||||
ipinfo = `${pkgs.iproute}/bin/ip addr`
|
||||
|
||||
message = <<EOM
|
||||
From: #{user}@fudo.org
|
||||
To: ${target}
|
||||
Subject: #{hostname} network info for #{date}
|
||||
Date: #{email_date}
|
||||
|
||||
#{ipinfo}
|
||||
EOM
|
||||
|
||||
smtp = Net::SMTP.new("${server}", ${toString port})
|
||||
smtp.enable_starttls
|
||||
|
||||
smtp.start('localhost', user, passwd) do |server|
|
||||
server.send_message(message, "#{user}@fudo.org", ["${target}"])
|
||||
end
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
options.fudo.netinfo-email = {
|
||||
enable = mkEnableOption "Enable netinfo email (hacky way to keep track of a host's IP";
|
||||
|
||||
smtp-server = mkOption {
|
||||
type = types.str;
|
||||
default = "mail.fudo.org";
|
||||
};
|
||||
|
||||
smtp-port = mkOption {
|
||||
type = types.port;
|
||||
default = 587;
|
||||
};
|
||||
|
||||
env-file = mkOption {
|
||||
type = types.str;
|
||||
description = "Path to file containing NETINFO_SMTP_USERNAME and NETINFO_SMTP_PASSWD";
|
||||
};
|
||||
|
||||
target-email = mkOption {
|
||||
type = types.str;
|
||||
default = "network-info@fudo.link";
|
||||
description = "Email to which to send network info report.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd = {
|
||||
timers.netinfo = {
|
||||
enable = true;
|
||||
description = "Send network info to ${cfg.target-email}";
|
||||
partOf = ["netinfo.service"];
|
||||
wantedBy = [ "timers.target" ];
|
||||
requires = [ "network-online.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
};
|
||||
};
|
||||
|
||||
services.netinfo = {
|
||||
enable = true;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
StandardOutput = "journal";
|
||||
EnvironmentFile = cfg.env-file;
|
||||
};
|
||||
script = ''
|
||||
${pkgs.ruby}/bin/ruby ${make-script cfg.smtp-server cfg.smtp-port cfg.target-email}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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*
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 = { };
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
ip = import ./fudolib/ip.nix { };
|
||||
dns = import ./fudolib/dns.nix { };
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
options.instance = {
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
description = ''
|
||||
Hostname of this specific host (without domain).
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
-63
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user