Partway to getting France back in the fold. Stuck on ACME certs.
This commit is contained in:
@@ -6,6 +6,7 @@ with lib; {
|
||||
|
||||
./instance.nix
|
||||
|
||||
./fudo/acme-certs.nix
|
||||
./fudo/acme-for-hostname.nix
|
||||
./fudo/authentication.nix
|
||||
./fudo/backplane
|
||||
@@ -23,6 +24,7 @@ with lib; {
|
||||
./fudo/host-filesystems.nix
|
||||
./fudo/initrd-network.nix
|
||||
./fudo/ipfs.nix
|
||||
./fudo/jabber.nix
|
||||
./fudo/kdc.nix
|
||||
./fudo/ldap.nix
|
||||
./fudo/local-network.nix
|
||||
|
||||
+2
-1
@@ -3,13 +3,14 @@
|
||||
let
|
||||
ip = import ./ip.nix { inherit lib; };
|
||||
dns = import ./dns.nix { inherit lib; };
|
||||
passwd = import ./passwd.nix { inherit lib; };
|
||||
in
|
||||
{
|
||||
lib.overlays = [
|
||||
(final: prev:
|
||||
prev.lib // {
|
||||
fudo = {
|
||||
inherit ip dns;
|
||||
inherit ip dns passwd;
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
localCopyOpts = { copy, ... }: let
|
||||
in {
|
||||
options = with types; {
|
||||
inherit domain;
|
||||
user = mkOption {
|
||||
type = str;
|
||||
description = "User to which this copy belongs.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = nullOr str;
|
||||
description = "Group to which this copy belongs.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = str;
|
||||
description = "Path at which to store the local copy.";
|
||||
#default = "/var/run/${toplevel.config.domain}/${copy}";
|
||||
};
|
||||
|
||||
service = mkOption {
|
||||
type = str;
|
||||
description = "systemd job to copy certs.";
|
||||
default = "fudo-${toplevel.config.domain}-${copy}-certs.service";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
domainOpts = { domain, ... }: {
|
||||
options = with types; {
|
||||
email = mkOption {
|
||||
type = str;
|
||||
description = "Domain administrator email.";
|
||||
default = "admin@${domain}";
|
||||
};
|
||||
|
||||
extra-domains = mkOption {
|
||||
type = listOf str;
|
||||
description = "List of domains to add to this certificate.";
|
||||
default = [];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
head-or-null = lst: if (lst == []) then null else head lst;
|
||||
rm-service-ext = filename:
|
||||
head-or-null (builtins.match "^(.+)\.service$" filename);
|
||||
|
||||
concatMapAttrs = f: attrs:
|
||||
foldr (a: b: a // b) {} (mapAttrsToList f attrs);
|
||||
|
||||
hostname = config.instance.hostname;
|
||||
cfg = config.fudo.acme;
|
||||
localDomains = if (hasAttr hostname cfg.host-domains) then
|
||||
cfg.host-domains.${hostname} else {};
|
||||
|
||||
optionalStringOr = str: default:
|
||||
if cond then str else default;
|
||||
|
||||
in {
|
||||
options.fudo.acme = with types; {
|
||||
host-domains = mkOption {
|
||||
type = attrsOf (attrsOf (submodule domainOpts));
|
||||
description = "Map of host to domains to domain options.";
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
security.acme.certs = mapAttrs (domain: domainOpts: {
|
||||
email = domainOpts.email;
|
||||
extraDomainNames = domainOpts.extra-domains;
|
||||
}) localDomains;
|
||||
|
||||
systemd = {
|
||||
tmpfiles.rules = let
|
||||
copies = concatMapAttrs (domain: domainOpts:
|
||||
domainOpts.local-copies) localDomains;
|
||||
copy-paths = mapAttrsToList (copy: copyOpts:
|
||||
"D '${path}' 0550 ${copyOpts.user} ${optionalStringOr copyOpts.group "-"} - -")
|
||||
copies;
|
||||
in copy-paths;
|
||||
|
||||
# TODO: Make this a Fudo service?
|
||||
services = concatMapAttrs (domain: domainOpts:
|
||||
mapAttrs' (copy: copyOpts: let
|
||||
source = config.security.acme.certs.${domain}.directory;
|
||||
target = copyOpts.path;
|
||||
install-certs = pkgs.writeShellScript "fudo-install-${domain}-${site}-certs.sh" ''
|
||||
for cert in cert chain fullchain full key; do
|
||||
cp ${source}/$cert.pem ${target}/$cert.pem
|
||||
chmod 0440 ${source}/$cert.pem
|
||||
done
|
||||
'';
|
||||
remove-certs = pkgs.writeShellScript "fudo-remove-${domain}-${site}-certs.sh" ''
|
||||
for cert in cert chain fullchain full key; do
|
||||
rm -rf ${target}/$cert.pem
|
||||
done
|
||||
'';
|
||||
in nameValuePair
|
||||
(rm-service-ext copyOpts.service) {
|
||||
description = "Copy ${domain} ACME certs for ${copy}.";
|
||||
after = [ "acme-${domain}.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = install-certs;
|
||||
ExecStop = remove-certs;
|
||||
RemainAfterExit = true;
|
||||
StandardOutput = "journal";
|
||||
};
|
||||
}) domainOpts.local-copies) localDomains;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@ with lib;
|
||||
let
|
||||
hostname = config.instance.hostname;
|
||||
host-filesystems = config.fudo.hosts.${hostname}.encrypted-filesystems;
|
||||
|
||||
|
||||
optionalOrDefault = str: default: if (str != null) then str else default;
|
||||
|
||||
filesystemsToMountpointLists = mapAttrsToList
|
||||
@@ -12,21 +12,24 @@ let
|
||||
|
||||
concatMapAttrs = f: as: concatMap (i: i) (mapAttrsToList f as);
|
||||
|
||||
concatMapAttrsToList = f: attrs:
|
||||
concatMap (i: i) (mapAttrsToList f attrs);
|
||||
|
||||
in {
|
||||
config = {
|
||||
users.groups = let
|
||||
mountpointToGroups = mp: mpOpts:
|
||||
optional (mpOpts.group != null)
|
||||
(nameValuePair mpOpts.group {
|
||||
members = mpOpts.users;
|
||||
});
|
||||
mountpointListToGroups =
|
||||
concatMapAttrs mountpointToGroups;
|
||||
mountpointListsToGroups =
|
||||
concatMap mountpointListToGroups;
|
||||
site-name = config.instance.local-site;
|
||||
site-hosts = filterAttrs
|
||||
(hostname: hostOpts: hostOpts.site == site-name)
|
||||
config.fudo.hosts;
|
||||
site-mountpoints = concatMapAttrsToList
|
||||
(host: hostOpts: concatMapAttrsToList
|
||||
(fs: fsOpts: attrValues fsOpts.mountpoints)
|
||||
hostOpts.encrypted-filesystems)
|
||||
site-hosts;
|
||||
in listToAttrs
|
||||
(mountpointListsToGroups
|
||||
(filesystemsToMountpointLists host-filesystems));
|
||||
(map (mp: nameValuePair mp.group { members = mp.users; })
|
||||
site-mountpoints);
|
||||
|
||||
systemd = {
|
||||
# Ensure the mountpoints exist
|
||||
|
||||
+36
-26
@@ -42,36 +42,41 @@ in {
|
||||
(substring 0 8 host-cfg.machine-id);
|
||||
};
|
||||
|
||||
# NixOS generates a stupid hosts file, just force it
|
||||
environment.etc = {
|
||||
hosts = let
|
||||
host-entries = mapAttrsToList
|
||||
(ip: hostnames: "${ip} ${concatStringsSep " " hostnames}")
|
||||
config.fudo.system.hostfile-entries;
|
||||
in mkForce {
|
||||
text = ''
|
||||
environment = {
|
||||
etc = {
|
||||
# NixOS generates a stupid hosts file, just force it
|
||||
hosts = let
|
||||
host-entries = mapAttrsToList
|
||||
(ip: hostnames: "${ip} ${concatStringsSep " " hostnames}")
|
||||
config.fudo.system.hostfile-entries;
|
||||
in mkForce {
|
||||
text = ''
|
||||
127.0.0.1 ${hostname}.${domain-name} ${hostname} localhost
|
||||
127.0.0.2 ${hostname} localhost
|
||||
::1 ${hostname}.${domain-name} ${hostname} localhost
|
||||
${concatStringsSep "\n" host-entries}
|
||||
'';
|
||||
user = "root";
|
||||
group = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
user = "root";
|
||||
group = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
machine-id = mkIf (host-cfg.machine-id != null) {
|
||||
text = host-cfg.machine-id;
|
||||
user = "root";
|
||||
group = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
machine-id = mkIf (host-cfg.machine-id != null) {
|
||||
text = host-cfg.machine-id;
|
||||
user = "root";
|
||||
group = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
current-system-packages.text = with builtins; let
|
||||
packages = map (p: "${p.name}")
|
||||
config.environment.systemPackages;
|
||||
sorted-unique = sort lessThan (unique packages);
|
||||
in concatStringsSep "\n" sorted-unique;
|
||||
current-system-packages.text = with builtins; let
|
||||
packages = map (p: "${p.name}")
|
||||
config.environment.systemPackages;
|
||||
sorted-unique = sort lessThan (unique packages);
|
||||
in concatStringsSep "\n" sorted-unique;
|
||||
};
|
||||
|
||||
systemPackages = with pkgs;
|
||||
mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ];
|
||||
};
|
||||
|
||||
time.timeZone = site.timezone;
|
||||
@@ -80,9 +85,6 @@ in {
|
||||
|
||||
services.cron.mailto = domain.admin-email;
|
||||
|
||||
environment.systemPackages = with pkgs;
|
||||
mkIf (host-cfg.docker-server) [ docker nix-prefetch-docker ];
|
||||
|
||||
virtualisation.docker = mkIf (host-cfg.docker-server) {
|
||||
enable = true;
|
||||
enableOnBoot = true;
|
||||
@@ -136,5 +138,13 @@ in {
|
||||
};
|
||||
|
||||
boot.tmpOnTmpfs = host-cfg.tmp-on-tmpfs;
|
||||
|
||||
home-manager.users.root.home.file = {
|
||||
".k5login".text = let
|
||||
realm = domain.gssapi-realm;
|
||||
entries =
|
||||
map (admin: "${admin}/root@${realm}") config.instance.local-admins;
|
||||
in concatStringsSep "\n" entries;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
lib: site: config: version:
|
||||
with lib;
|
||||
let
|
||||
db-config = if (config.database != null) then
|
||||
db-config = optionalString (config.database != null)
|
||||
''
|
||||
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}"
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
hostname = config.instance.hostname;
|
||||
|
||||
siteOpts = { ... }: with types; {
|
||||
options = {
|
||||
enableACME = mkOption {
|
||||
type = bool;
|
||||
description = "Use ACME to get SSL certificates for this site.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
site-config = mkOption {
|
||||
type = attrs;
|
||||
description = "Site-specific configuration.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
site-copy = site: "ejabberd-${site}";
|
||||
|
||||
concatMapAttrs = f: attrs:
|
||||
foldr (a: b: a // b) {} (mapAttrs f attrs);
|
||||
|
||||
concatMapAttrsToList = f: attr:
|
||||
attrValues (concatMapAttrs f attr);
|
||||
|
||||
host-domains = config.fudo.acme.host-domains.${hostname};
|
||||
|
||||
siteCerts = site: let
|
||||
certPath = config.fudo.acme.local-copies.${site-copy site}.path;
|
||||
in [
|
||||
"${certPath}/fullchain.pem"
|
||||
"${certPath}/privkey.pem"
|
||||
"${certPath}/chain.pem"
|
||||
];
|
||||
|
||||
siteCertService = site:
|
||||
config.fudo.acme.local-copies.${site-copy site}.service;
|
||||
|
||||
config-file-template = let
|
||||
jabber-config = {
|
||||
loglevel = cfg.log-level;
|
||||
|
||||
access_rules = {
|
||||
c2s = { allow = "all"; };
|
||||
announce = { allow = "admin"; };
|
||||
configure = { allow = "admin"; };
|
||||
pubsub_createnode = { allow = "local"; };
|
||||
};
|
||||
|
||||
acl = {
|
||||
admin = {
|
||||
user = concatMap
|
||||
(admin: map (site: "${admin}@${site}")
|
||||
(attrNames cfg.sites))
|
||||
cfg.admins;
|
||||
};
|
||||
};
|
||||
|
||||
hosts = attrNames cfg.sites;
|
||||
|
||||
listen = [{
|
||||
port = cfg.port;
|
||||
module = "ejabberd_c2s";
|
||||
ip = cfg.listen-ip;
|
||||
starttls = true;
|
||||
starttls_required = true;
|
||||
}];
|
||||
|
||||
certfiles = concatMapAttrsToList
|
||||
(site: siteOpts:
|
||||
if (siteOpts.enableACME) then
|
||||
(siteCerts site)
|
||||
else [])
|
||||
cfg.sites;
|
||||
|
||||
host_config =
|
||||
mapAttrs (site: siteOpts: siteOpts.site-config)
|
||||
cfg.sites;
|
||||
};
|
||||
|
||||
config-file = builtins.toJSON jabber-config;
|
||||
in pkgs.writeText "ejabberd.config.yml.template" config-file;
|
||||
|
||||
enter-secrets = template: secrets: target: let
|
||||
secret-readers = concatStringsSep "\n"
|
||||
(mapAttrsToList
|
||||
(secret: file: "${secret}=$(cat ${file})")
|
||||
secrets);
|
||||
secret-swappers = map
|
||||
(secret: "sed s/${secret}/\$${secret}/g")
|
||||
(attrNames secrets);
|
||||
swapper = concatStringsSep " | " secret-swappers;
|
||||
in pkgs.writeShellScript "ejabberd-generate-config.sh" ''
|
||||
cat ${template} | ${swapper} > ${target}
|
||||
chown ${cfg.user}:${cfg.group} ${target}
|
||||
'';
|
||||
|
||||
cfg = config.fudo.jabber;
|
||||
|
||||
in {
|
||||
options.fudo.jabber = with types; {
|
||||
enable = mkEnableOption "Enable ejabberd server.";
|
||||
|
||||
port = mkOption {
|
||||
type = port;
|
||||
description = "Port on which to listen for Jabber connections.";
|
||||
default = 5222;
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = str;
|
||||
description = "User as which to run the ejabberd server.";
|
||||
default = "ejabberd";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = str;
|
||||
description = "Group as which to run the ejabberd server.";
|
||||
default = "ejabberd";
|
||||
};
|
||||
|
||||
admins = mkOption {
|
||||
type = str;
|
||||
description = "List of admin users for the server.";
|
||||
default = [];
|
||||
};
|
||||
|
||||
sites = mkOption {
|
||||
type = attrsOf (submodule siteOpts);
|
||||
description = "List of sites on which to listen for Jabber connections.";
|
||||
};
|
||||
|
||||
secret-files = mkOption {
|
||||
type = attrsOf str;
|
||||
description = "Map of secret-name to file. File contents will be subbed for the name in the config.";
|
||||
default = {};
|
||||
};
|
||||
|
||||
config-file = mkOption {
|
||||
type = str;
|
||||
description = "Location at which to generate the configuration file.";
|
||||
default = "/var/run/ejabberd/ejabberd.yaml";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users = {
|
||||
users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
groups.${cfg.group} = {
|
||||
members = [ cfg.user ];
|
||||
};
|
||||
};
|
||||
|
||||
fudo.acme.local-copies = mapAttrs' (site: siteCfg:
|
||||
nameValuePair (site-copy site)
|
||||
mkif siteCfg.enableACME {
|
||||
domain = site;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
}) cfg.sites;
|
||||
|
||||
systemd = {
|
||||
tmpfiles.rules = [
|
||||
"D '${dirOf cfg.config-file}' 0550 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
services.ejabberd = let
|
||||
config-generator = enter-secrets config-file-template cfg.secret-files cfg.config-file;
|
||||
in {
|
||||
wants = map (site: siteCertService site) (attrNames cfg.sites);
|
||||
environment = cfg.secret-files;
|
||||
serviceConfig = {
|
||||
ExecStartPre = mkAfter "${config-generator}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.ejabberd = {
|
||||
enable = true;
|
||||
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
|
||||
configFile = cfg.config-file;
|
||||
};
|
||||
};
|
||||
}
|
||||
+12
-17
@@ -63,8 +63,8 @@ let
|
||||
}
|
||||
|
||||
[logging]
|
||||
kdc = FILE:/var/kerberos/kerberos.log
|
||||
default = FILE:/var/kerberos/kerberos.log
|
||||
kdc = FILE:${cfg.state-directory}/kerberos.log
|
||||
default = FILE:${cfg.state-directory}/kerberos.log
|
||||
'';
|
||||
|
||||
aclEntry = { principal, ... }: {
|
||||
@@ -111,7 +111,9 @@ let
|
||||
|
||||
in {
|
||||
|
||||
options.fudo.auth.kdc = with types; {
|
||||
options.fudo.auth.kdc = with types; let
|
||||
default-state-dir = "/var/kerberos";
|
||||
in {
|
||||
enable = mkEnableOption "Fudo KDC";
|
||||
|
||||
realm = mkOption {
|
||||
@@ -150,31 +152,31 @@ in {
|
||||
state-directory = mkOption {
|
||||
type = str;
|
||||
description = "Path at which to store kerberos database.";
|
||||
default = "/var/kerberos";
|
||||
default = default-state-dir;
|
||||
};
|
||||
|
||||
master-key-file = mkOption {
|
||||
type = str;
|
||||
description = "File containing the master key for the realm.";
|
||||
default = "/var/kerberos/master.key";
|
||||
default = "${default-state-dir}/master.key";
|
||||
};
|
||||
|
||||
primary-keytab = mkOption {
|
||||
type = str;
|
||||
description = "Location of keytab for kadmind.";
|
||||
default = "/var/kerberos/host.keytab";
|
||||
default = "${default-state-dir}/host.keytab";
|
||||
};
|
||||
|
||||
kadmin-keytab = mkOption {
|
||||
type = str;
|
||||
description = "Location of keytab for kadmind.";
|
||||
default = "/var/kerberos/kadmind.keytab";
|
||||
default = "${default-state-dir}/kadmind.keytab";
|
||||
};
|
||||
|
||||
kpasswdd-keytab = mkOption {
|
||||
type = str;
|
||||
description = "Location of keytab for kpasswdd.";
|
||||
default = "/var/kerberos/kpasswdd.keytab";
|
||||
default = "${default-state-dir}/kpasswdd.keytab";
|
||||
};
|
||||
|
||||
kdc-internal-port = mkOption {
|
||||
@@ -184,13 +186,6 @@ in {
|
||||
default = 4088;
|
||||
};
|
||||
|
||||
# k5login-directory = mkOption {
|
||||
# type = str;
|
||||
# description =
|
||||
# "Directory in which k5login files are stored for local users (equivalent to ~/.k5login).";
|
||||
# default = "/var/kerberos/k5login";
|
||||
# };
|
||||
|
||||
max-ticket-lifetime = mkOption {
|
||||
type = str;
|
||||
description = "Maximum lifetime of a single ticket in this realm.";
|
||||
@@ -208,7 +203,7 @@ in {
|
||||
users = {
|
||||
users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
home = "/var/kerberos";
|
||||
home = cfg.state-directory;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
@@ -224,7 +219,7 @@ in {
|
||||
};
|
||||
realms = { ${cfg.realm} = { enable-http = false; }; };
|
||||
extraConfig = ''
|
||||
default = FILE:/var/kerberos/kerberos.log
|
||||
default = FILE:${cfg.state-directory}/kerberos.log
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
+21
-21
@@ -169,41 +169,41 @@ let
|
||||
|
||||
in {
|
||||
|
||||
options = {
|
||||
options = with types; {
|
||||
fudo = {
|
||||
auth = {
|
||||
ldap-server = {
|
||||
enable = mkEnableOption "Fudo Authentication";
|
||||
|
||||
kerberos-host = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = ''
|
||||
The name of the host to use for Kerberos authentication.
|
||||
'';
|
||||
};
|
||||
|
||||
kerberos-keytab = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = ''
|
||||
The path to a keytab for the LDAP server, containing a principal for ldap/<hostname>.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCert = mkOption {
|
||||
type = types.str;
|
||||
ssl-certificate = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
The path to the SSL certificate to use for the server.
|
||||
'';
|
||||
};
|
||||
|
||||
sslKey = mkOption {
|
||||
type = types.str;
|
||||
ssl-private-key = mkOption {
|
||||
type = str;
|
||||
description = ''
|
||||
The path to the SSL key to use for the server.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCACert = mkOption {
|
||||
ssl-ca-certificate = mkOption {
|
||||
type = with types; nullOr str;
|
||||
description = ''
|
||||
The path to the SSL CA cert used to sign the certificate.
|
||||
@@ -212,14 +212,14 @@ in {
|
||||
};
|
||||
|
||||
organization = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = ''
|
||||
The name to use for the organization.
|
||||
'';
|
||||
};
|
||||
|
||||
base = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = ''
|
||||
The base dn of the LDAP server (eg. "dc=fudo,dc=org").
|
||||
'';
|
||||
@@ -227,24 +227,23 @@ in {
|
||||
|
||||
rootpw-file = mkOption {
|
||||
default = "";
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = ''
|
||||
The path to a file containing the root password for this database.
|
||||
'';
|
||||
};
|
||||
|
||||
listen-uris = mkOption {
|
||||
default = [ ];
|
||||
type = with types; listOf str;
|
||||
type = listOf str;
|
||||
description = ''
|
||||
A list of URIs on which the ldap server should listen.
|
||||
'';
|
||||
example = [ "ldap://auth.fudo.org" "ldaps://auth.fudo.org" ];
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
users = mkOption {
|
||||
default = { };
|
||||
type = with types; attrsOf (submodule ldapUserOpts);
|
||||
type = attrsOf (submodule ldapUserOpts);
|
||||
example = {
|
||||
tester = {
|
||||
uid = 10099;
|
||||
@@ -255,11 +254,12 @@ in {
|
||||
description = ''
|
||||
Users to be added to the Fudo LDAP database.
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
|
||||
groups = mkOption {
|
||||
default = { };
|
||||
type = with types; attrsOf (submodule ldapGroupOpts);
|
||||
type = attrsOf (submodule ldapGroupOpts);
|
||||
example = {
|
||||
admin = {
|
||||
gid = 1099;
|
||||
@@ -273,7 +273,7 @@ in {
|
||||
|
||||
system-users = mkOption {
|
||||
default = { };
|
||||
type = with types; attrsOf (submodule ldapSystemUserOpts);
|
||||
type = attrsOf (submodule ldapSystemUserOpts);
|
||||
example = {
|
||||
replicator = {
|
||||
description = "System user for database sync";
|
||||
@@ -346,10 +346,10 @@ in {
|
||||
|
||||
extraConfig = ''
|
||||
|
||||
TLSCertificateFile ${cfg.sslCert}
|
||||
TLSCertificateKeyFile ${cfg.sslKey}
|
||||
${optionalString (cfg.sslCACert != null)
|
||||
"TLSCACertificateFile ${cfg.sslCACert}"}
|
||||
TLSCertificateFile ${cfg.ssl-certificate}
|
||||
TLSCertificateKeyFile ${cfg.ssl-private-key}
|
||||
${optionalString (cfg.ssl-ca-certificate != null)
|
||||
"TLSCACertificateFile ${cfg.ssl-ca-certificate}"}
|
||||
|
||||
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"
|
||||
|
||||
+26
-4
@@ -52,11 +52,12 @@ let
|
||||
path = [ pkgs.age ];
|
||||
};
|
||||
|
||||
secretOpts = { ... }: {
|
||||
secretOpts = { name, ... }: {
|
||||
options = with types; {
|
||||
source-file = mkOption {
|
||||
type = path; # CAREFUL: this will copy the file to nixstore...keep on deploy host
|
||||
description = "File from which to load the secret.";
|
||||
description = "File from which to load the secret. If unspecified, a random new password will be generated.";
|
||||
default = "${generate-secret name}/passwd";
|
||||
};
|
||||
|
||||
target-file = mkOption {
|
||||
@@ -86,7 +87,26 @@ let
|
||||
|
||||
nix-build-users = let usernames = attrNames config.users.users;
|
||||
in filter (user: (builtins.match "^nixbld[0-9]{1,2}$" user) != null)
|
||||
usernames;
|
||||
usernames;
|
||||
|
||||
generate-secret = name: pkgs.stdenv.mkDerivation {
|
||||
name = "${name}-generated-passwd";
|
||||
|
||||
phases = [ "installPhase" ];
|
||||
|
||||
buildInputs = with pkgs; [ pwgen ];
|
||||
|
||||
buildPhase = ''
|
||||
echo "${name}-${config.instance.build-timestamp}" >> file.txt
|
||||
pwgen --secure --symbols --num-passwords=1 --sha1=file.txt 40 > passwd
|
||||
rm -f file.txt
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
mv passwd $out/passwd
|
||||
'';
|
||||
};
|
||||
|
||||
in {
|
||||
options.fudo.secrets = with types; {
|
||||
@@ -139,7 +159,9 @@ in {
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.groups = {
|
||||
${cfg.secret-group} = { members = cfg.secret-users ++ nix-build-users; };
|
||||
${cfg.secret-group} = {
|
||||
members = cfg.secret-users ++ nix-build-users;
|
||||
};
|
||||
};
|
||||
|
||||
systemd = let
|
||||
|
||||
+3
-24
@@ -40,12 +40,6 @@ let
|
||||
default = null;
|
||||
};
|
||||
|
||||
gateway-host = mkOption {
|
||||
type = nullOr str;
|
||||
description = "Identity of the host to act as a gateway.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
local-groups = mkOption {
|
||||
type = listOf str;
|
||||
description = "List of groups which should exist at this site.";
|
||||
@@ -135,24 +129,9 @@ let
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
keytab-path = mkOption {
|
||||
type = nullOr str;
|
||||
description = ''
|
||||
Directory containing site keytabs (files named $hostname.keytab).
|
||||
|
||||
Should exist only on build host.
|
||||
'';
|
||||
default = null;
|
||||
};
|
||||
|
||||
build-key-path = mkOption {
|
||||
type = nullOr str;
|
||||
description = ''
|
||||
Directory containing host build keys (files named $hostname.key).
|
||||
|
||||
Should exist only on build host.
|
||||
'';
|
||||
default = null;
|
||||
mail-server = mkOption {
|
||||
type = str;
|
||||
description = "Hostname of the mail server to use for this site.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
+93
-63
@@ -2,14 +2,14 @@
|
||||
|
||||
with lib;
|
||||
let
|
||||
hostname = config.instance.hostname;
|
||||
|
||||
cfg = config.fudo.webmail;
|
||||
|
||||
inherit (lib.strings) concatStringsSep;
|
||||
webmail-user = cfg.user;
|
||||
webmail-group = cfg.group;
|
||||
|
||||
webmail-user = "webmail-php";
|
||||
webmail-group = "webmail-php";
|
||||
|
||||
base-data-path = "/var/rainloop";
|
||||
base-data-path = "/var/run/rainloop";
|
||||
|
||||
fastcgi-conf = builtins.toFile "fastcgi.conf" ''
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
@@ -54,87 +54,81 @@ let
|
||||
'';
|
||||
})) cfg.sites;
|
||||
|
||||
siteOpts = { site-host, ... }: {
|
||||
siteOpts = { site-host, ... }: with types; {
|
||||
options = {
|
||||
title = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "Webmail site title";
|
||||
example = "My Webmail";
|
||||
};
|
||||
|
||||
debug = mkOption {
|
||||
type = types.bool;
|
||||
type = bool;
|
||||
description = "Turn debug logs on.";
|
||||
default = false;
|
||||
};
|
||||
|
||||
mail-server = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "Mail server from which to send & recieve email.";
|
||||
default = "mail.fudo.org";
|
||||
};
|
||||
|
||||
favicon = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "URL of the site favicon";
|
||||
example = "https://www.somepage.com/fav.ico";
|
||||
};
|
||||
|
||||
messages-per-page = mkOption {
|
||||
type = types.int;
|
||||
type = int;
|
||||
description = "Default number of messages to show per page";
|
||||
default = 30;
|
||||
};
|
||||
|
||||
max-upload-size = mkOption {
|
||||
type = types.int;
|
||||
type = int;
|
||||
description = "Size limit in MB for uploaded files";
|
||||
default = 30;
|
||||
};
|
||||
|
||||
theme = mkOption {
|
||||
type = types.str;
|
||||
type = 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;
|
||||
type = str;
|
||||
description = "Domain for which the server acts as webmail server";
|
||||
};
|
||||
|
||||
edit-mode = mkOption {
|
||||
type = types.enum [ "Plain" "Html" "PlainForced" "HtmlForced" ];
|
||||
type = enum [ "Plain" "Html" "PlainForced" "HtmlForced" ];
|
||||
description = "Default text editing mode for email";
|
||||
default = "Html";
|
||||
};
|
||||
|
||||
layout-mode = mkOption {
|
||||
type = types.enum [ "side" "bottom" ];
|
||||
type = enum [ "side" "bottom" ];
|
||||
description = "Layout mode to use for email preview.";
|
||||
default = "side";
|
||||
};
|
||||
|
||||
enable-threading = mkOption {
|
||||
type = types.bool;
|
||||
type = bool;
|
||||
description = "Whether to enable threading for email.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
enable-mobile = mkOption {
|
||||
type = types.bool;
|
||||
type = bool;
|
||||
description = "Whether to enable a mobile site view.";
|
||||
default = true;
|
||||
};
|
||||
|
||||
database = mkOption {
|
||||
type = with types; nullOr (submodule databaseOpts);
|
||||
type = nullOr (submodule databaseOpts);
|
||||
description = "Database configuration for storing contact data.";
|
||||
example = {
|
||||
name = "my_db";
|
||||
@@ -146,58 +140,63 @@ let
|
||||
};
|
||||
|
||||
admin-email = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "Email of administrator of this site.";
|
||||
default = "admin@fudo.org";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
databaseOpts = { ... }: {
|
||||
databaseOpts = { ... }: with types; {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "pgsql" "mysql" ];
|
||||
type = enum [ "pgsql" "mysql" ];
|
||||
description = "Driver to use when connecting to the database.";
|
||||
default = "pgsql";
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "Name of host running the database.";
|
||||
example = "my-db.domain.com";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.int;
|
||||
type = int;
|
||||
description = "Port on which the database server is listening.";
|
||||
default = 5432;
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description =
|
||||
"Name of the database containing contact info. <user> must have access.";
|
||||
default = "rainloop_contacts";
|
||||
default = "rainloop_webmail";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
type = str;
|
||||
description = "User as which to connect to the database.";
|
||||
default = "webmail";
|
||||
};
|
||||
|
||||
password-file = mkOption {
|
||||
type = types.str;
|
||||
description = "Password to use when connecting to the database.";
|
||||
type = nullOr str;
|
||||
description = ''
|
||||
Password to use when connecting to the database.
|
||||
|
||||
If unset, a random password will be generated.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
options.fudo.webmail = {
|
||||
options.fudo.webmail = with types; {
|
||||
enable = mkEnableOption "Enable a RainLoop webmail server.";
|
||||
|
||||
sites = mkOption {
|
||||
type = with types; (attrsOf (submodule siteOpts));
|
||||
type = attrsOf (submodule siteOpts);
|
||||
description = "A map of webmail sites to site configurations.";
|
||||
example = {
|
||||
"webmail.domain.com" = {
|
||||
@@ -208,6 +207,18 @@ in {
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = str;
|
||||
description = "User as which webmail will run.";
|
||||
default = "webmail-php";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = str;
|
||||
description = "Group as which webmail will run.";
|
||||
default = "webmail-php";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
@@ -226,13 +237,12 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
security.acme.certs = mapAttrs'
|
||||
(site: site-cfg: nameValuePair site { email = site-cfg.admin-email; })
|
||||
security.acme.certs = mapAttrs
|
||||
(site: site-cfg: { email = site-cfg.admin-email; })
|
||||
cfg.sites;
|
||||
|
||||
services = {
|
||||
phpfpm = {
|
||||
|
||||
pools.webmail = {
|
||||
settings = {
|
||||
"pm" = "dynamic";
|
||||
@@ -285,43 +295,63 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
fudo.secrets.host-secrets.${hostname} = concatMapAttrs
|
||||
(site: site-cfg: let
|
||||
|
||||
site-config-file = builtins.toFile "${site}-rainloop.cfg"
|
||||
(import ./include/rainloop.nix lib site site-cfg site-pkgs.${site}.version);
|
||||
|
||||
domain-cfg-file = 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 {
|
||||
"${site}-site-config" = {
|
||||
source-file = site-config-file;
|
||||
target-file = "/var/run/webmail/rainloop/site-${site}-rainloop.cfg";
|
||||
user = cfg.user;
|
||||
};
|
||||
|
||||
"${site}-domain-config" = {
|
||||
source-file = domain-config-file;
|
||||
target-file = "/var/run/webmail/rainloop/domain-${site}-rainloop.cfg";
|
||||
user = cfg.user;
|
||||
};
|
||||
}) cfg.sites;
|
||||
|
||||
# TODO: make this a fudo service
|
||||
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 = ""
|
||||
'';
|
||||
cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-site-config".target-file;
|
||||
domain-cfg-file = config.fudo.secrets.host-secrets.${hostname}."${site}-doomain-config".target-file;
|
||||
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
|
||||
${pkgs.coreutils}/bin/cp ${domain-cfg-file} ${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}
|
||||
${pkgs.coreutils}/bin/chmod -R u+w ${base-data-path}
|
||||
'');
|
||||
in {
|
||||
requiredBy = [ "nginx.service" ];
|
||||
|
||||
@@ -80,6 +80,12 @@ in {
|
||||
|
||||
local-hosts =
|
||||
filterAttrs (host: hostOpts: hostOpts.site == local-site) config.fudo.hosts;
|
||||
|
||||
local-networks =
|
||||
host.local-networks //
|
||||
config.fudo.domains.${local-domain}.local-networks //
|
||||
config.fudo.sites.${local-site}.local-networks;
|
||||
|
||||
in {
|
||||
instance = {
|
||||
local-domain = local-domain;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{ lib, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
hash-ldap-passwd-pkg = name: passwd-file: pkgs.stdenv.mkDerivation {
|
||||
name = "${name}-ldap-passwd";
|
||||
|
||||
phases = [ "buildPhase" "installPhase" ];
|
||||
|
||||
buildInputs = with pkgs; [ openldap ];
|
||||
|
||||
buildPhase = ''
|
||||
slappasswd -T ${passwd-file} > ldap-passwd
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
mv ldap-passwd $out
|
||||
'';
|
||||
};
|
||||
|
||||
hash-ldap-passwd = name: passwd-file: let
|
||||
passwd-pkgs = hash-ldap-passwd-pkg name passwd-file;
|
||||
in builtins.readFile "${passwd-pkgs}/ldap-passwd";
|
||||
|
||||
generate-random-passwd = name: length: pkgs.stdenv.mkDerivation {
|
||||
name = "${name}-random-passwd";
|
||||
|
||||
phases = [ "buildPhase" "installPhase" ];
|
||||
|
||||
buildInputs = with pkgs; [ pwgen ];
|
||||
|
||||
buildPhase = ''
|
||||
pwgen --symbols --num-passwords=1 ${length} > passwd
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir $out
|
||||
mv passwd $out
|
||||
'';
|
||||
};
|
||||
|
||||
in {
|
||||
hash-ldap-passwd = hash-ldap-passwd;
|
||||
|
||||
random-passwd-file = name: length:
|
||||
toPath "${generate-random-passwd name length}/passwd";
|
||||
}
|
||||
Reference in New Issue
Block a user