Begin reverting back to simple-nixos-mailserver
It appears I can get app passwords with kanidm and ldap so just going to a more stable, probably supported setup, should be good.
This commit is contained in:
parent
c97b19c495
commit
cd8a0db1b6
21 changed files with 814 additions and 250 deletions
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
khscodes.khs.enable = true;
|
||||||
|
khscodes.khs.shell.oh-my-posh.enable = true;
|
||||||
|
}
|
|
@ -97,10 +97,15 @@ in
|
||||||
{
|
{
|
||||||
options.khscodes.infrastructure.hetzner-instance = {
|
options.khscodes.infrastructure.hetzner-instance = {
|
||||||
enable = lib.mkEnableOption "enables generating a opentofu config";
|
enable = lib.mkEnableOption "enables generating a opentofu config";
|
||||||
dnsNames = lib.mkOption {
|
dnsName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = fqdn;
|
||||||
|
};
|
||||||
|
dnsAliases = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
description = "DNS names for the server";
|
default = lib.lists.unique (
|
||||||
default = lib.lists.unique ([ fqdn ] ++ config.khscodes.networking.aliases);
|
lib.lists.filter (alias: alias != cfg.dnsName) config.khscodes.networking.aliases
|
||||||
|
);
|
||||||
};
|
};
|
||||||
bucket = {
|
bucket = {
|
||||||
key = lib.mkOption {
|
key = lib.mkOption {
|
||||||
|
@ -206,14 +211,22 @@ in
|
||||||
enable = true;
|
enable = true;
|
||||||
dns = {
|
dns = {
|
||||||
enable = true;
|
enable = true;
|
||||||
aRecords = lib.lists.map (d: {
|
aRecords = [
|
||||||
fqdn = d;
|
{
|
||||||
content = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
fqdn = cfg.dnsName;
|
||||||
}) cfg.dnsNames;
|
content = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
||||||
aaaaRecords = lib.lists.map (d: {
|
}
|
||||||
fqdn = d;
|
];
|
||||||
content = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
aaaaRecords = [
|
||||||
}) cfg.dnsNames;
|
{
|
||||||
|
fqdn = cfg.dnsName;
|
||||||
|
content = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
cnameRecords = lib.lists.map (domain: {
|
||||||
|
fqdn = domain;
|
||||||
|
content = cfg.dnsName;
|
||||||
|
}) cfg.dnsAliases;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
resource.hcloud_firewall.fw = lib.mkIf firewallEnable {
|
resource.hcloud_firewall.fw = lib.mkIf firewallEnable {
|
||||||
|
|
|
@ -19,6 +19,10 @@ in
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = cfg.secretOwner;
|
default = cfg.secretOwner;
|
||||||
};
|
};
|
||||||
|
perms = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "0600";
|
||||||
|
};
|
||||||
reloadOrRestartUnits = lib.mkOption {
|
reloadOrRestartUnits = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
@ -41,7 +45,7 @@ in
|
||||||
destination = cfg.secretFile;
|
destination = cfg.secretFile;
|
||||||
owner = cfg.secretOwner;
|
owner = cfg.secretOwner;
|
||||||
group = cfg.secretGroup;
|
group = cfg.secretGroup;
|
||||||
perms = "0600";
|
perms = cfg.perms;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
khscodes.infrastructure.vault-server-approle.policy = {
|
||||||
|
|
|
@ -63,11 +63,14 @@ in
|
||||||
{
|
{
|
||||||
options.khscodes.infrastructure.khs-openstack-instance = {
|
options.khscodes.infrastructure.khs-openstack-instance = {
|
||||||
enable = lib.mkEnableOption "enables generating a opentofu config for khs openstack instance";
|
enable = lib.mkEnableOption "enables generating a opentofu config for khs openstack instance";
|
||||||
dnsNames = lib.mkOption {
|
dnsName = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = config.khscodes.networking.fqdn;
|
||||||
|
};
|
||||||
|
dnsAliases = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
description = "DNS names for the instance";
|
|
||||||
default = lib.lists.unique (
|
default = lib.lists.unique (
|
||||||
[ config.khscodes.networking.fqdn ] ++ config.khscodes.networking.aliases
|
lib.lists.filter (alias: alias != cfg.dnsName) config.khscodes.networking.aliases
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
bucket = {
|
bucket = {
|
||||||
|
@ -153,16 +156,20 @@ in
|
||||||
enable = true;
|
enable = true;
|
||||||
dns = {
|
dns = {
|
||||||
enable = true;
|
enable = true;
|
||||||
aRecords = lib.mkIf cfg.dns.mapIpv4Address (
|
aRecords = lib.mkIf cfg.dns.mapIpv4Address [
|
||||||
lib.lists.map (d: {
|
{
|
||||||
fqdn = d;
|
fqdn = cfg.dnsName;
|
||||||
content = config.khscodes.openstack.output.compute_instance.compute.ipv4_address;
|
content = config.khscodes.openstack.output.compute_instance.compute.ipv4_address;
|
||||||
}) cfg.dnsNames
|
}
|
||||||
);
|
];
|
||||||
aaaaRecords = lib.lists.map (d: {
|
aaaaRecords = lib.lists.map (d: {
|
||||||
fqdn = d;
|
fqdn = d;
|
||||||
content = config.khscodes.openstack.output.compute_instance.compute.ipv6_address;
|
content = config.khscodes.openstack.output.compute_instance.compute.ipv6_address;
|
||||||
}) cfg.dnsNames;
|
}) cfg.dnsAliases;
|
||||||
|
cnameRecords = lib.lists.map (domain: {
|
||||||
|
fqdn = domain;
|
||||||
|
content = cfg.dnsName;
|
||||||
|
}) cfg.dnsAliases;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
output.ipv4_address = {
|
output.ipv4_address = {
|
||||||
|
|
6
nix/modules/nixos/infrastructure/mailserver/dane.nix
Normal file
6
nix/modules/nixos/infrastructure/mailserver/dane.nix
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# I cannot right now figure out the best way forward with implementing DANE.
|
||||||
|
# It seems to me that the server itself needs access to cloudflare to update its
|
||||||
|
# DNS records, then I need to coordinate with the ACME setup to not rotate the key (reuse_key)
|
||||||
|
# before the DNS records are updated.
|
||||||
|
# This all seems like a lot of hassle, and for now, I am foregoing this.
|
||||||
|
{ }
|
|
@ -1,84 +1,12 @@
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
fqdn = config.khscodes.networking.fqdn;
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
adminCredentialsFile = "/etc/stalwart/admin-pw";
|
|
||||||
stalwart-spam-filter = pkgs.callPackage ./stalwart-spam-filter.nix { };
|
|
||||||
ifthen = condition: expr: {
|
|
||||||
"if" = condition;
|
|
||||||
"then" = expr;
|
|
||||||
};
|
|
||||||
otherwise = expr: { "else" = expr; };
|
|
||||||
authDkimForDomain = domain: [
|
|
||||||
(ifthen "sender_domain = '${domain}'" "['${domain}_rsa', '${domain}_ed25519']")
|
|
||||||
];
|
|
||||||
authDkim = lib.lists.flatten (lib.lists.map authDkimForDomain cfg.domains);
|
|
||||||
signatureForDomain = domain: [
|
|
||||||
{
|
|
||||||
name = "${domain}_rsa";
|
|
||||||
value = {
|
|
||||||
inherit domain;
|
|
||||||
private-key = "%{file:/run/secret/dkim/${domain}.snm_rsa.key}";
|
|
||||||
selector = "snm_rsa";
|
|
||||||
headers = [
|
|
||||||
"From"
|
|
||||||
"To"
|
|
||||||
"Cc"
|
|
||||||
"Date"
|
|
||||||
"Subject"
|
|
||||||
"Message-ID"
|
|
||||||
"Organization"
|
|
||||||
"MIME-Version"
|
|
||||||
"Content-Type"
|
|
||||||
"In-Reply-To"
|
|
||||||
"References"
|
|
||||||
"List-Id"
|
|
||||||
"User-Agent"
|
|
||||||
"Thread-Topic"
|
|
||||||
"Thread-Index"
|
|
||||||
];
|
|
||||||
algorithm = "rsa-sha256";
|
|
||||||
canonicalization = "relaxed/relaxed";
|
|
||||||
report = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "${domain}_ed25519";
|
|
||||||
value = {
|
|
||||||
inherit domain;
|
|
||||||
private-key = "%{file:/run/secret/dkim/${domain}.snm_ed25519.key}";
|
|
||||||
selector = "snm_ed25519";
|
|
||||||
headers = [
|
|
||||||
"From"
|
|
||||||
"To"
|
|
||||||
"Cc"
|
|
||||||
"Date"
|
|
||||||
"Subject"
|
|
||||||
"Message-ID"
|
|
||||||
"Organization"
|
|
||||||
"MIME-Version"
|
|
||||||
"Content-Type"
|
|
||||||
"In-Reply-To"
|
|
||||||
"References"
|
|
||||||
"List-Id"
|
|
||||||
"User-Agent"
|
|
||||||
"Thread-Topic"
|
|
||||||
"Thread-Index"
|
|
||||||
];
|
|
||||||
algorithm = "ed25519-sha256";
|
|
||||||
canonicalization = "relaxed/relaxed";
|
|
||||||
report = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
dkimSignatures = {
|
|
||||||
signature = lib.listToAttrs (lib.lists.flatten (lib.lists.map signatureForDomain cfg.domains));
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.khscodes.infrastructure.mailserver = {
|
options.khscodes.infrastructure.mailserver = {
|
||||||
|
@ -89,7 +17,16 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
imports = [
|
imports = [
|
||||||
|
inputs.simple-nixos-mailserver.nixosModules.mailserver
|
||||||
|
./dmarc.nix
|
||||||
|
./dane.nix
|
||||||
./dkim.nix
|
./dkim.nix
|
||||||
|
./mta-sts.nix
|
||||||
|
./spf.nix
|
||||||
|
./tls-rpt.nix
|
||||||
|
./prometheus.nix
|
||||||
|
./openid-connect.nix
|
||||||
|
./package/nixos-module.nix
|
||||||
];
|
];
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# TODO: Include a similiar rule for openstack
|
# TODO: Include a similiar rule for openstack
|
||||||
|
@ -117,90 +54,83 @@ in
|
||||||
ttl = 600;
|
ttl = 600;
|
||||||
}) cfg.domains
|
}) cfg.domains
|
||||||
);
|
);
|
||||||
|
khscodes.cloudflare.dns.srvRecords = lib.lists.flatten (
|
||||||
|
lib.lists.map (domain: [
|
||||||
|
{
|
||||||
|
fqdn = "_imaps._tcp.${domain}";
|
||||||
|
content = fqdn;
|
||||||
|
priority = 0;
|
||||||
|
weight = 1;
|
||||||
|
port = 993;
|
||||||
|
ttl = 600;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
fqdn = "_submissions._tcp.${domain}";
|
||||||
|
content = fqdn;
|
||||||
|
priority = 0;
|
||||||
|
weight = 1;
|
||||||
|
port = 465;
|
||||||
|
ttl = 600;
|
||||||
|
}
|
||||||
|
]) cfg.domains
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
environment.etc."stalwart/admin-pw".text = "test";
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
enableImap = false;
|
||||||
|
enableImapSsl = true;
|
||||||
|
enableSubmission = false;
|
||||||
|
enableSubmissionSsl = true;
|
||||||
|
stateVersion = 3;
|
||||||
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
|
systemDomain = config.khscodes.networking.fqdn;
|
||||||
|
useUTF8FolderNames = true;
|
||||||
|
domains = cfg.domains;
|
||||||
|
certificateScheme = "acme";
|
||||||
|
};
|
||||||
|
services.dovecot2.extraConfig = ''
|
||||||
|
oauth2 {
|
||||||
|
openid_configuration_url = https://login.kaareskovgaard.net/oauth2/openid/stalwart/.well-known/openid-configuration
|
||||||
|
scope = email openid profile
|
||||||
|
username_attribute = preferred_username
|
||||||
|
client_id = stalwart
|
||||||
|
client_secret = <${config.khscodes.infrastructure.kanidm-client-application.secretFile}
|
||||||
|
tokeninfo_url = https://login.kaareskovgaard.net/oauth2/token
|
||||||
|
introspection_url = https://login.kaareskovgaard.net/oauth2/token/introspect
|
||||||
|
introspection_mode = post
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
services.prometheus.exporters.postfix = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
khscodes.infrastructure.vault-prometheus-sender.exporters.enabled = [ "postfix" ];
|
||||||
|
services.fail2ban.jails = {
|
||||||
|
postfix = {
|
||||||
|
settings = {
|
||||||
|
enabled = true;
|
||||||
|
mode = "aggressive";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = "1d";
|
||||||
|
maxretry = 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
dovecot = {
|
||||||
|
settings = {
|
||||||
|
enabled = true;
|
||||||
|
mode = "aggressive";
|
||||||
|
findtime = 600;
|
||||||
|
bantime = "1d";
|
||||||
|
maxretry = 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [
|
networking.firewall.allowedTCPPorts = [
|
||||||
25
|
25
|
||||||
465
|
465
|
||||||
993
|
993
|
||||||
];
|
];
|
||||||
security.acme.certs."${fqdn}" = {
|
|
||||||
# Not sure if this does an actual reload (which is not supported apparently),
|
|
||||||
# or if it does a full restart (which would be needed).
|
|
||||||
# If it doesn't work, then I should look into using post run to query the API.
|
|
||||||
reloadServices = [ "stalwart-mail.service" ];
|
|
||||||
};
|
|
||||||
users.users.stalwart-mail.extraGroups = [ config.security.acme.certs.${fqdn}.group ];
|
|
||||||
systemd.services.stalwart-mail = {
|
|
||||||
serviceConfig = {
|
|
||||||
ReadOnlyPaths = [ config.security.acme.certs."${fqdn}".directory ];
|
|
||||||
};
|
|
||||||
wants = [ "acme-${fqdn}.service" ];
|
|
||||||
after = [ "acme-${fqdn}.service" ];
|
|
||||||
};
|
|
||||||
services.stalwart-mail = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.stalwart-mail;
|
|
||||||
openFirewall = false;
|
|
||||||
settings = {
|
|
||||||
http = {
|
|
||||||
url = "https://${fqdn}";
|
|
||||||
use-x-forwarded = true;
|
|
||||||
};
|
|
||||||
server = {
|
|
||||||
hostname = fqdn;
|
|
||||||
tls = {
|
|
||||||
enable = true;
|
|
||||||
certificate = "default";
|
|
||||||
implicit = true;
|
|
||||||
ignore-client-order = true;
|
|
||||||
};
|
|
||||||
listener = {
|
|
||||||
smtp = {
|
|
||||||
protocol = "smtp";
|
|
||||||
bind = "[::]:25";
|
|
||||||
};
|
|
||||||
submissions = {
|
|
||||||
bind = "[::]:465";
|
|
||||||
protocol = "smtp";
|
|
||||||
};
|
|
||||||
imaps = {
|
|
||||||
bind = "[::]:993";
|
|
||||||
protocol = "imap";
|
|
||||||
};
|
|
||||||
management = {
|
|
||||||
bind = [ "127.0.0.1:8080" ];
|
|
||||||
protocol = "http";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# There's a bug in 25.05 that references the wrong file. Fixed in master/unstable.
|
|
||||||
spam-filter.resource = "file://${stalwart-spam-filter}/spam-filter.toml";
|
|
||||||
authentication.fallback-admin = {
|
|
||||||
user = "admin";
|
|
||||||
secret = "%{file:/etc/stalwart/admin-pw}%";
|
|
||||||
};
|
|
||||||
certificate.default = {
|
|
||||||
cert = "%{file://${config.security.acme.certs."${fqdn}".directory}/fullchain.pem}";
|
|
||||||
private-key = "%{file://${config.security.acme.certs."${fqdn}".directory}/key.pem}";
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
auth.dkim = {
|
|
||||||
sign = authDkim ++ [
|
|
||||||
(otherwise false)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
directory.memory = {
|
|
||||||
type = "memory";
|
|
||||||
domains = [
|
|
||||||
"agerlinskovgaard.dk"
|
|
||||||
"agerlin-skovgaard.dk"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// dkimSignatures;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
pkgs,
|
|
||||||
lib,
|
lib,
|
||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
|
@ -52,37 +51,6 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config = lib.mkIf (cfg.enable) {
|
config = lib.mkIf (cfg.enable) {
|
||||||
# mailserver = {
|
|
||||||
# dkimSigning = false;
|
|
||||||
# };
|
|
||||||
# services.rspamd.locals."dkim_signing.conf" = lib.mkForce {
|
|
||||||
# text = ''
|
|
||||||
# enabled = true;
|
|
||||||
# domain {
|
|
||||||
# ${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)}
|
|
||||||
# }
|
|
||||||
# '';
|
|
||||||
# };
|
|
||||||
# systemd.services.rspamd = {
|
|
||||||
# unitConfig = {
|
|
||||||
# ConditionPathExists = domainKeyPaths;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# systemd.services.postfix = {
|
|
||||||
# unitConfig = {
|
|
||||||
# ConditionPathExists = domainKeyPaths;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
systemd.services.stalwart-mail = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = domainKeyPaths;
|
|
||||||
};
|
|
||||||
serviceConfig = {
|
|
||||||
ReadOnlyPaths = [
|
|
||||||
"/run/secret/dkim"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
khscodes.infrastructure.vault-server-approle.policy = {
|
||||||
"${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/*" = {
|
"${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/*" = {
|
||||||
capabilities = [ "read" ];
|
capabilities = [ "read" ];
|
||||||
|
@ -90,7 +58,7 @@ in
|
||||||
};
|
};
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
(
|
(
|
||||||
{ config, ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
terraform.required_providers.tls = {
|
terraform.required_providers.tls = {
|
||||||
source = "hashicorp/tls";
|
source = "hashicorp/tls";
|
||||||
|
@ -128,16 +96,6 @@ in
|
||||||
fqdn = "snm_ed25519._domainkey.${domain}";
|
fqdn = "snm_ed25519._domainkey.${domain}";
|
||||||
content = ''"''${ join("\" \"", regexall(".{1,255}", "v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519"}" )) }"'';
|
content = ''"''${ join("\" \"", regexall(".{1,255}", "v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519"}" )) }"'';
|
||||||
ttl = 600;
|
ttl = 600;
|
||||||
}) cfg.domains)
|
|
||||||
++ (lib.lists.map (domain: {
|
|
||||||
fqdn = domain;
|
|
||||||
content = ''"v=spf1 mx -all"'';
|
|
||||||
ttl = 600;
|
|
||||||
}) cfg.domains)
|
|
||||||
++ (lib.lists.map (domain: {
|
|
||||||
fqdn = "_dmarc.${domain}";
|
|
||||||
content = ''"v=DMARC1; p=reject; adkim=s; aspf=s;"'';
|
|
||||||
ttl = 600;
|
|
||||||
}) cfg.domains);
|
}) cfg.domains);
|
||||||
|
|
||||||
resource.vault_kv_secret_v2 =
|
resource.vault_kv_secret_v2 =
|
||||||
|
@ -168,11 +126,6 @@ in
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
systemd.services.stalwart-mail.serviceConfig = {
|
|
||||||
ReadWritePaths = [
|
|
||||||
"/run/secret/dkim"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
khscodes.services.vault-agent.templates = lib.lists.flatten (
|
khscodes.services.vault-agent.templates = lib.lists.flatten (
|
||||||
lib.lists.map (domain: [
|
lib.lists.map (domain: [
|
||||||
{
|
{
|
||||||
|
@ -183,10 +136,10 @@ in
|
||||||
'';
|
'';
|
||||||
destination = rsaKeyPath domain;
|
destination = rsaKeyPath domain;
|
||||||
perms = "0600";
|
perms = "0600";
|
||||||
owner = "stalwart-mail";
|
owner = "rspamd";
|
||||||
group = "stalwart-mail";
|
group = "rspamd";
|
||||||
restartUnits = [
|
restartUnits = [
|
||||||
"stalwart-mail.service"
|
"rspamd.service"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -197,13 +150,34 @@ in
|
||||||
'';
|
'';
|
||||||
destination = ed25519KeyPath domain;
|
destination = ed25519KeyPath domain;
|
||||||
perms = "0600";
|
perms = "0600";
|
||||||
owner = "stalwart-mail";
|
owner = "rspamd";
|
||||||
group = "stalwart-mail";
|
group = "rspamd";
|
||||||
restartUnits = [
|
restartUnits = [
|
||||||
"stalwart-mail.service"
|
"rspamd.service"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
]) cfg.domains
|
]) cfg.domains
|
||||||
);
|
);
|
||||||
|
mailserver = {
|
||||||
|
dkimSigning = false;
|
||||||
|
};
|
||||||
|
services.rspamd.locals."dkim_signing.conf" = lib.mkForce {
|
||||||
|
text = ''
|
||||||
|
enabled = true;
|
||||||
|
domain {
|
||||||
|
${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
systemd.services.rspamd = {
|
||||||
|
unitConfig = {
|
||||||
|
ConditionPathExists = domainKeyPaths;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
systemd.services.postfix = {
|
||||||
|
unitConfig = {
|
||||||
|
ConditionPathExists = domainKeyPaths;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
17
nix/modules/nixos/infrastructure/mailserver/dmarc.nix
Normal file
17
nix/modules/nixos/infrastructure/mailserver/dmarc.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
|
{
|
||||||
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
||||||
|
fqdn = "_dmarc.${domain}";
|
||||||
|
content = ''"v=DMARC1; p=reject; rua=mailto:postmaster@${domain}; ruf=mailto:postmaster@${domain}; adkim=s; aspf=s;"'';
|
||||||
|
ttl = 600;
|
||||||
|
}) cfg.domains;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
54
nix/modules/nixos/infrastructure/mailserver/mta-sts.nix
Normal file
54
nix/modules/nixos/infrastructure/mailserver/mta-sts.nix
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
# Increment this if ever changing mta-sts settings.
|
||||||
|
policyVersion = 2;
|
||||||
|
mtaStsWellKnown = pkgs.writeTextFile "mta-sts.txt" ''
|
||||||
|
version: STSv1
|
||||||
|
mode: enforce
|
||||||
|
max_age: 600
|
||||||
|
mx: ${fqdn}
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
khscodes.services.nginx.virtualHosts = (
|
||||||
|
lib.listToAttrs (
|
||||||
|
lib.lists.map (domain: {
|
||||||
|
name = "mta-sts.${domain}";
|
||||||
|
value = {
|
||||||
|
locations."=/.well-known/mta-sts.txt" = {
|
||||||
|
tryFiles = "${mtaStsWellKnown} =404";
|
||||||
|
};
|
||||||
|
locations."/" = {
|
||||||
|
return = 404;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}) cfg.domains
|
||||||
|
)
|
||||||
|
);
|
||||||
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
|
{
|
||||||
|
khscodes.cloudflare.dns.txtRecords = (
|
||||||
|
lib.lists.map (domain: {
|
||||||
|
fqdn = "_mta-sts.${domain}";
|
||||||
|
content = ''"v=STSv1; id=${builtins.toString policyVersion}"'';
|
||||||
|
}) cfg.domains
|
||||||
|
);
|
||||||
|
khscodes.cloudflare.dns.cnameRecords = (
|
||||||
|
lib.lists.map (domain: {
|
||||||
|
fqdn = "mta-sts.${domain}";
|
||||||
|
content = fqdn;
|
||||||
|
ttl = 600;
|
||||||
|
}) cfg.domains
|
||||||
|
);
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable { };
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# This file contains patches for Nixos 25.05 to be compatible with new stalwart mail
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
configFormat = pkgs.formats.toml { };
|
||||||
|
configFile = configFormat.generate "stalwart-mail.toml" config.services.stalwart-mail.settings;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
systemd.services.stalwart-mail = lib.mkIf config.services.stalwart-mail.enable {
|
||||||
|
serviceConfig = {
|
||||||
|
User = "stalwart-mail";
|
||||||
|
Group = "stalwart-mail";
|
||||||
|
ExecStart = lib.mkForce [
|
||||||
|
""
|
||||||
|
"${lib.getExe config.services.stalwart-mail.package} --config=${configFile}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
194
nix/modules/nixos/infrastructure/mailserver/package/package.nix
Normal file
194
nix/modules/nixos/infrastructure/mailserver/package/package.nix
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
rustPlatform,
|
||||||
|
fetchFromGitHub,
|
||||||
|
pkg-config,
|
||||||
|
protobuf,
|
||||||
|
bzip2,
|
||||||
|
openssl,
|
||||||
|
sqlite,
|
||||||
|
foundationdb,
|
||||||
|
zstd,
|
||||||
|
stdenv,
|
||||||
|
nix-update-script,
|
||||||
|
nixosTests,
|
||||||
|
rocksdb,
|
||||||
|
callPackage,
|
||||||
|
withFoundationdb ? false,
|
||||||
|
stalwartEnterprise ? false,
|
||||||
|
}:
|
||||||
|
|
||||||
|
rustPlatform.buildRustPackage (finalAttrs: {
|
||||||
|
pname = "stalwart-mail" + (lib.optionalString stalwartEnterprise "-enterprise");
|
||||||
|
version = "0.13.2";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "stalwartlabs";
|
||||||
|
repo = "stalwart";
|
||||||
|
rev = "51a0a1445d74a8cfb880e9d88f5be390fa0e9365";
|
||||||
|
hash = "sha256-VdeHb1HVGXA5RPenhhK4r/kkQiLG8/4qhdxoJ3xIqR4=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-Wu6skjs3Stux5nCX++yoQPeA33Qln67GoKcob++Ldng=";
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkg-config
|
||||||
|
protobuf
|
||||||
|
rustPlatform.bindgenHook
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
bzip2
|
||||||
|
openssl
|
||||||
|
sqlite
|
||||||
|
zstd
|
||||||
|
]
|
||||||
|
++ lib.optionals (stdenv.hostPlatform.isLinux && withFoundationdb) [ foundationdb ];
|
||||||
|
|
||||||
|
# Issue: https://github.com/stalwartlabs/stalwart/issues/1104
|
||||||
|
buildNoDefaultFeatures = true;
|
||||||
|
buildFeatures = [
|
||||||
|
"postgres"
|
||||||
|
"rocks"
|
||||||
|
"elastic"
|
||||||
|
"redis"
|
||||||
|
]
|
||||||
|
++ lib.optionals withFoundationdb [ "foundationdb" ]
|
||||||
|
++ lib.optionals stalwartEnterprise [ "enterprise" ];
|
||||||
|
|
||||||
|
env = {
|
||||||
|
OPENSSL_NO_VENDOR = true;
|
||||||
|
ZSTD_SYS_USE_PKG_CONFIG = true;
|
||||||
|
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
|
||||||
|
ROCKSDB_LIB_DIR = "${rocksdb}/lib";
|
||||||
|
};
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/etc/stalwart
|
||||||
|
|
||||||
|
mkdir -p $out/lib/systemd/system
|
||||||
|
|
||||||
|
substitute resources/systemd/stalwart-mail.service $out/lib/systemd/system/stalwart-mail.service \
|
||||||
|
--replace "__PATH__" "$out"
|
||||||
|
'';
|
||||||
|
|
||||||
|
checkFlags = lib.forEach [
|
||||||
|
# Require running mysql, postgresql daemon
|
||||||
|
"directory::imap::imap_directory"
|
||||||
|
"directory::internal::internal_directory"
|
||||||
|
"directory::ldap::ldap_directory"
|
||||||
|
"directory::sql::sql_directory"
|
||||||
|
"directory::oidc::oidc_directory"
|
||||||
|
"store::blob::blob_tests"
|
||||||
|
"store::lookup::lookup_tests"
|
||||||
|
"smtp::lookup::sql::lookup_sql"
|
||||||
|
# thread 'directory::smtp::lmtp_directory' panicked at tests/src/store/mod.rs:122:44:
|
||||||
|
# called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
|
||||||
|
"directory::smtp::lmtp_directory"
|
||||||
|
# thread 'imap::imap_tests' panicked at tests/src/imap/mod.rs:436:14:
|
||||||
|
# Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
|
||||||
|
"imap::imap_tests"
|
||||||
|
# thread 'jmap::jmap_tests' panicked at tests/src/jmap/mod.rs:303:14:
|
||||||
|
# Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
|
||||||
|
"jmap::jmap_tests"
|
||||||
|
# Failed to read system DNS config: io error: No such file or directory (os error 2)
|
||||||
|
"smtp::inbound::data::data"
|
||||||
|
# Expected "X-My-Header: true" but got Received: from foobar.net (unknown [10.0.0.123])
|
||||||
|
"smtp::inbound::scripts::sieve_scripts"
|
||||||
|
# thread 'smtp::outbound::lmtp::lmtp_delivery' panicked at tests/src/smtp/session.rs:313:13:
|
||||||
|
# Expected "<invalid@domain.org> (failed to lookup" but got From: "Mail Delivery Subsystem" <MAILER-DAEMON@localhost>
|
||||||
|
"smtp::outbound::lmtp::lmtp_delivery"
|
||||||
|
# thread 'smtp::outbound::extensions::extensions' panicked at tests/src/smtp/inbound/mod.rs:45:23:
|
||||||
|
# No queue event received.
|
||||||
|
"smtp::outbound::extensions::extensions"
|
||||||
|
# panicked at tests/src/smtp/outbound/smtp.rs:173:5:
|
||||||
|
"smtp::outbound::smtp::smtp_delivery"
|
||||||
|
# panicked at tests/src/smtp/outbound/lmtp.rs
|
||||||
|
"smtp::outbound::lmtp::lmtp_delivery"
|
||||||
|
# thread 'smtp::queue::retry::queue_retry' panicked at tests/src/smtp/queue/retry.rs:119:5:
|
||||||
|
# assertion `left == right` failed
|
||||||
|
# left: [1, 2, 2]
|
||||||
|
# right: [1, 2, 3]
|
||||||
|
"smtp::queue::retry::queue_retry"
|
||||||
|
# thread 'smtp::queue::virtualq::virtual_queue' panicked at /build/source/crates/store/src/dispatch/store.rs:548:14:
|
||||||
|
# called `Result::unwrap()` on an `Err` value: Error(Event { inner: Store(MysqlError), keys: [(Reason, String("Input/output error: Input/output error: Connection refused (os error 111)")), (CausedBy, String("crates/store/src/dispatch/store.rs:301"))] })
|
||||||
|
"smtp::queue::virtualq::virtual_queue"
|
||||||
|
# Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
|
||||||
|
"store::store_tests"
|
||||||
|
# Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
|
||||||
|
"cluster::cluster_tests"
|
||||||
|
# Missing store type. Try running `STORE=<store_type> cargo test`: NotPresent
|
||||||
|
"webdav::webdav_tests"
|
||||||
|
# thread 'config::parser::tests::toml_parse' panicked at crates/utils/src/config/parser.rs:463:58:
|
||||||
|
# called `Result::unwrap()` on an `Err` value: "Expected ['\\n'] but found '!' in value at line 70."
|
||||||
|
"config::parser::tests::toml_parse"
|
||||||
|
# error[E0432]: unresolved import `r2d2_sqlite`
|
||||||
|
# use of undeclared crate or module `r2d2_sqlite`
|
||||||
|
"backend::sqlite::pool::SqliteConnectionManager::with_init"
|
||||||
|
# thread 'smtp::reporting::analyze::report_analyze' panicked at tests/src/smtp/reporting/analyze.rs:88:5:
|
||||||
|
# assertion `left == right` failed
|
||||||
|
# left: 0
|
||||||
|
# right: 12
|
||||||
|
"smtp::reporting::analyze::report_analyze"
|
||||||
|
# thread 'smtp::inbound::dmarc::dmarc' panicked at tests/src/smtp/inbound/mod.rs:59:26:
|
||||||
|
# Expected empty queue but got Reload
|
||||||
|
"smtp::inbound::dmarc::dmarc"
|
||||||
|
# thread 'smtp::queue::concurrent::concurrent_queue' panicked at tests/src/smtp/inbound/mod.rs:65:9:
|
||||||
|
# assertion `left == right` failed
|
||||||
|
"smtp::queue::concurrent::concurrent_queue"
|
||||||
|
# Failed to read system DNS config: io error: No such file or directory (os error 2)
|
||||||
|
"smtp::inbound::auth::auth"
|
||||||
|
# Failed to read system DNS config: io error: No such file or directory (os error 2)
|
||||||
|
"smtp::inbound::antispam::antispam"
|
||||||
|
# Failed to read system DNS config: io error: No such file or directory (os error 2)
|
||||||
|
"smtp::inbound::vrfy::vrfy_expn"
|
||||||
|
# thread 'smtp::management::queue::manage_queue' panicked at tests/src/smtp/inbound/mod.rs:45:23:
|
||||||
|
# No queue event received.
|
||||||
|
# NOTE: Test unreliable on high load systems
|
||||||
|
"smtp::management::queue::manage_queue"
|
||||||
|
# thread 'responses::tests::parse_responses' panicked at crates/dav-proto/src/responses/mod.rs:671:17:
|
||||||
|
# assertion `left == right` failed: failed for 008.xml
|
||||||
|
# left: ElementEnd
|
||||||
|
# right: Bytes([...])
|
||||||
|
"responses::tests::parse_responses"
|
||||||
|
] (test: "--skip=${test}");
|
||||||
|
|
||||||
|
doCheck = !(stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isAarch64);
|
||||||
|
|
||||||
|
# Allow network access during tests on Darwin/macOS
|
||||||
|
__darwinAllowLocalNetworking = true;
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
inherit rocksdb; # make used rocksdb version available (e.g., for backup scripts)
|
||||||
|
webadmin = callPackage ./webadmin.nix { };
|
||||||
|
spam-filter = callPackage ./spam-filter.nix { };
|
||||||
|
updateScript = nix-update-script { };
|
||||||
|
tests.stalwart-mail = nixosTests.stalwart-mail;
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Secure & Modern All-in-One Mail Server (IMAP, JMAP, SMTP)";
|
||||||
|
homepage = "https://github.com/stalwartlabs/mail-server";
|
||||||
|
changelog = "https://github.com/stalwartlabs/mail-server/blob/main/CHANGELOG.md";
|
||||||
|
license = [
|
||||||
|
lib.licenses.agpl3Only
|
||||||
|
]
|
||||||
|
++ lib.optionals stalwartEnterprise [
|
||||||
|
{
|
||||||
|
fullName = "Stalwart Enterprise License 1.0 (SELv1) Agreement";
|
||||||
|
url = "https://github.com/stalwartlabs/mail-server/blob/main/LICENSES/LicenseRef-SEL.txt";
|
||||||
|
free = false;
|
||||||
|
redistributable = false;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
mainProgram = "stalwart";
|
||||||
|
maintainers = with lib.maintainers; [
|
||||||
|
happysalada
|
||||||
|
onny
|
||||||
|
oddlama
|
||||||
|
pandapip1
|
||||||
|
norpol
|
||||||
|
];
|
||||||
|
};
|
||||||
|
})
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
rustPlatform,
|
||||||
|
stalwart-mail,
|
||||||
|
fetchFromGitHub,
|
||||||
|
trunk,
|
||||||
|
tailwindcss_3,
|
||||||
|
fetchNpmDeps,
|
||||||
|
nix-update-script,
|
||||||
|
nodejs,
|
||||||
|
npmHooks,
|
||||||
|
llvmPackages,
|
||||||
|
wasm-bindgen-cli_0_2_93,
|
||||||
|
binaryen,
|
||||||
|
zip,
|
||||||
|
}:
|
||||||
|
|
||||||
|
rustPlatform.buildRustPackage (finalAttrs: {
|
||||||
|
pname = "webadmin";
|
||||||
|
version = "0.1.31";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "stalwartlabs";
|
||||||
|
repo = "webadmin";
|
||||||
|
rev = "6f1368b8a1160341b385980accea489ee0e45440";
|
||||||
|
hash = "sha256-/EWn/wiY6zFNhObfo11OkoGufcUODMYs18P3vTBbB8s=";
|
||||||
|
};
|
||||||
|
|
||||||
|
npmDeps = fetchNpmDeps {
|
||||||
|
name = "${finalAttrs.pname}-npm-deps";
|
||||||
|
hash = "sha256-na1HEueX8w7kuDp8LEtJ0nD1Yv39cyk6sEMpS1zix2s=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-Q05+wH9+NfkfmEDJFLuWVQ7wuDeEu9h1XmOMN6SYdyU=";
|
||||||
|
|
||||||
|
postPatch = ''
|
||||||
|
# Using local tailwindcss for compilation
|
||||||
|
substituteInPlace Trunk.toml --replace-fail "npx tailwindcss" "tailwindcss"
|
||||||
|
'';
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
binaryen
|
||||||
|
llvmPackages.bintools-unwrapped
|
||||||
|
nodejs
|
||||||
|
npmHooks.npmConfigHook
|
||||||
|
tailwindcss_3
|
||||||
|
trunk
|
||||||
|
# needs to match with wasm-bindgen version in upstreams Cargo.lock
|
||||||
|
wasm-bindgen-cli_0_2_93
|
||||||
|
|
||||||
|
zip
|
||||||
|
];
|
||||||
|
|
||||||
|
NODE_PATH = "$npmDeps";
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
trunk build --offline --frozen --release
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
cd dist
|
||||||
|
mkdir -p $out
|
||||||
|
zip -r $out/webadmin.zip *
|
||||||
|
'';
|
||||||
|
|
||||||
|
passthru = {
|
||||||
|
updateScript = nix-update-script { };
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Secure & modern all-in-one mail server Stalwart (webadmin module)";
|
||||||
|
homepage = "https://github.com/stalwartlabs/webadmin";
|
||||||
|
changelog = "https://github.com/stalwartlabs/webadmin/blob/${finalAttrs.src.tag}/CHANGELOG.md";
|
||||||
|
license = lib.licenses.agpl3Only;
|
||||||
|
inherit (stalwart-mail.meta) maintainers;
|
||||||
|
};
|
||||||
|
})
|
15
nix/modules/nixos/infrastructure/mailserver/prometheus.nix
Normal file
15
nix/modules/nixos/infrastructure/mailserver/prometheus.nix
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
services.stalwart-mail.settings.metrics.prometheus = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
khscodes.services.nginx.virtualHosts."${fqdn}".locations."=/metrics/prometheus" = {
|
||||||
|
return = 404;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
17
nix/modules/nixos/infrastructure/mailserver/spf.nix
Normal file
17
nix/modules/nixos/infrastructure/mailserver/spf.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
|
{
|
||||||
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
||||||
|
fqdn = domain;
|
||||||
|
content = ''"v=spf1 mx ra=postmaster -all"'';
|
||||||
|
ttl = 600;
|
||||||
|
}) cfg.domains;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
17
nix/modules/nixos/infrastructure/mailserver/tls-rpt.nix
Normal file
17
nix/modules/nixos/infrastructure/mailserver/tls-rpt.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
cfg = config.khscodes.infrastructure.mailserver;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
|
{
|
||||||
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
||||||
|
fqdn = "_smtp._tls.${domain}";
|
||||||
|
content = ''"v=TLSRPTv1; rua=mailto:postmaster@${domain}"'';
|
||||||
|
ttl = 600;
|
||||||
|
}) cfg.domains;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
|
@ -19,6 +19,35 @@ let
|
||||||
top = lib.lists.takeEnd 2 split;
|
top = lib.lists.takeEnd 2 split;
|
||||||
in
|
in
|
||||||
lib.strings.concatStringsSep "." top;
|
lib.strings.concatStringsSep "." top;
|
||||||
|
serviceFromFqdn =
|
||||||
|
fqdn:
|
||||||
|
let
|
||||||
|
split = lib.strings.splitString "." fqdn;
|
||||||
|
in
|
||||||
|
assert
|
||||||
|
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
||||||
|
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
||||||
|
builtins.elemAt split 0;
|
||||||
|
protocolFromFqdn =
|
||||||
|
fqdn:
|
||||||
|
let
|
||||||
|
split = lib.strings.splitString "." fqdn;
|
||||||
|
in
|
||||||
|
assert
|
||||||
|
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
||||||
|
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
||||||
|
builtins.elemAt split 1;
|
||||||
|
nameFromFqdn =
|
||||||
|
fqdn:
|
||||||
|
let
|
||||||
|
split = lib.strings.splitString "." fqdn;
|
||||||
|
in
|
||||||
|
assert
|
||||||
|
lib.strings.hasPrefix "_" (builtins.elemAt split 0)
|
||||||
|
&& lib.strings.hasPrefix "_" (builtins.elemAt split 1);
|
||||||
|
lib.strings.concatStringsSep "." (
|
||||||
|
lib.lists.removePrefix [ (builtins.elemAt split 0) (builtins.elemAt split 1) ] split
|
||||||
|
);
|
||||||
dnsARecordModule = lib.khscodes.mkSubmodule {
|
dnsARecordModule = lib.khscodes.mkSubmodule {
|
||||||
description = "Module for defining dns A/AAAA record";
|
description = "Module for defining dns A/AAAA record";
|
||||||
options = {
|
options = {
|
||||||
|
@ -42,6 +71,29 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
dnsCnameRecordModule = lib.khscodes.mkSubmodule {
|
||||||
|
description = "Module for defining dns CNAME record";
|
||||||
|
options = {
|
||||||
|
fqdn = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The FQDN of the CNAME record to create";
|
||||||
|
};
|
||||||
|
content = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The content of the CNAME record (canonical name)";
|
||||||
|
};
|
||||||
|
proxied = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = "Creates a proxied record in cloudflare";
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
ttl = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Time to Live for the CNAME record";
|
||||||
|
default = 600;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
dnsTxtRecordModule = lib.khscodes.mkSubmodule {
|
dnsTxtRecordModule = lib.khscodes.mkSubmodule {
|
||||||
description = "Module for defining dns TXT record";
|
description = "Module for defining dns TXT record";
|
||||||
options = {
|
options = {
|
||||||
|
@ -60,6 +112,54 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
dnsSrvRecordModule = lib.khscodes.mkSubmodule {
|
||||||
|
description = "Module for defining dns SRV record";
|
||||||
|
options = {
|
||||||
|
fqdn = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The FQDN of the SRV record to create";
|
||||||
|
};
|
||||||
|
content = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The content of the SRV record";
|
||||||
|
};
|
||||||
|
priority = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Priority for the SRV record";
|
||||||
|
};
|
||||||
|
weight = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Weight for the SRV record";
|
||||||
|
};
|
||||||
|
port = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Port for the SRV record";
|
||||||
|
};
|
||||||
|
ttl = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Time to Live for the SRV record";
|
||||||
|
default = 600;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
dnsTlsaRecordModule = lib.khscodes.mkSubmodule {
|
||||||
|
description = "Module for defining dns TLSA record";
|
||||||
|
options = {
|
||||||
|
fqdn = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The FQDN of the TLSA record to create";
|
||||||
|
};
|
||||||
|
content = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "The content of the TLSA record";
|
||||||
|
};
|
||||||
|
ttl = lib.mkOption {
|
||||||
|
type = lib.types.int;
|
||||||
|
description = "Time to Live for the TLSA record";
|
||||||
|
default = 600;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
dnsMxRecordModule = lib.khscodes.mkSubmodule {
|
dnsMxRecordModule = lib.khscodes.mkSubmodule {
|
||||||
description = "Module for defining dns MX record";
|
description = "Module for defining dns MX record";
|
||||||
options = {
|
options = {
|
||||||
|
@ -99,11 +199,26 @@ in
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "AAAA records to create in the zone";
|
description = "AAAA records to create in the zone";
|
||||||
};
|
};
|
||||||
|
cnameRecords = lib.mkOption {
|
||||||
|
type = lib.types.listOf dnsCnameRecordModule;
|
||||||
|
default = [ ];
|
||||||
|
description = "CNAME records to create in the zone";
|
||||||
|
};
|
||||||
txtRecords = lib.mkOption {
|
txtRecords = lib.mkOption {
|
||||||
type = lib.types.listOf dnsTxtRecordModule;
|
type = lib.types.listOf dnsTxtRecordModule;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "TXT Records to create";
|
description = "TXT Records to create";
|
||||||
};
|
};
|
||||||
|
srvRecords = lib.mkOption {
|
||||||
|
type = lib.types.listOf dnsSrvRecordModule;
|
||||||
|
default = [ ];
|
||||||
|
description = "SRV Records to create";
|
||||||
|
};
|
||||||
|
tlsaRecords = lib.mkOption {
|
||||||
|
type = lib.types.listOf dnsTlsaRecordModule;
|
||||||
|
default = [ ];
|
||||||
|
description = "TLSA Records to create";
|
||||||
|
};
|
||||||
mxRecords = lib.mkOption {
|
mxRecords = lib.mkOption {
|
||||||
type = lib.types.listOf dnsMxRecordModule;
|
type = lib.types.listOf dnsMxRecordModule;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
@ -125,7 +240,14 @@ in
|
||||||
|
|
||||||
khscodes.cloudflare.data.dns_zones = lib.lists.unique (
|
khscodes.cloudflare.data.dns_zones = lib.lists.unique (
|
||||||
lib.lists.map (a: tldFromFqdn (a.fqdn)) (
|
lib.lists.map (a: tldFromFqdn (a.fqdn)) (
|
||||||
cfg.dns.aRecords ++ cfg.dns.aaaaRecords ++ cfg.dns.txtRecords ++ cfg.dns.mxRecords
|
cfg.dns.aRecords
|
||||||
|
++ cfg.dns.aaaaRecords
|
||||||
|
++ cfg.dns.txtRecords
|
||||||
|
++ cfg.dns.mxRecords
|
||||||
|
++ cfg.dns.cnameRecords
|
||||||
|
++ cfg.dns.srvRecords
|
||||||
|
++ cfg.dns.tlsaRecords
|
||||||
|
++ cfg.dns.cnameRecords
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
resource.cloudflare_record = lib.attrsets.optionalAttrs cfg.dns.enable (
|
resource.cloudflare_record = lib.attrsets.optionalAttrs cfg.dns.enable (
|
||||||
|
@ -162,6 +284,22 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
) cfg.dns.aaaaRecords)
|
) cfg.dns.aaaaRecords)
|
||||||
|
++ (lib.lists.map (
|
||||||
|
record:
|
||||||
|
let
|
||||||
|
zoneName = tldFromFqdn record.fqdn;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_cname";
|
||||||
|
value = {
|
||||||
|
inherit (record) content ttl proxied;
|
||||||
|
name = nameFromFQDNAndZone record.fqdn zoneName;
|
||||||
|
type = "CNAME";
|
||||||
|
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
|
||||||
|
comment = "app=${zoneName}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) cfg.dns.cnameRecords)
|
||||||
++ (lib.lists.map (
|
++ (lib.lists.map (
|
||||||
record:
|
record:
|
||||||
let
|
let
|
||||||
|
@ -194,6 +332,41 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
) cfg.dns.mxRecords)
|
) cfg.dns.mxRecords)
|
||||||
|
++ (lib.lists.map (
|
||||||
|
record:
|
||||||
|
let
|
||||||
|
zoneName = tldFromFqdn record.fqdn;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_srv";
|
||||||
|
value = {
|
||||||
|
name = nameFromFQDNAndZone record.fqdn zoneName;
|
||||||
|
type = "SRV";
|
||||||
|
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
|
||||||
|
comment = "app=${zoneName}";
|
||||||
|
data = {
|
||||||
|
inherit (record) priority weight port;
|
||||||
|
target = record.content;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) cfg.dns.srvRecords)
|
||||||
|
++ (lib.lists.map (
|
||||||
|
record:
|
||||||
|
let
|
||||||
|
zoneName = tldFromFqdn record.fqdn;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
name = "${lib.khscodes.sanitize-terraform-name record.fqdn}_tlsa";
|
||||||
|
value = {
|
||||||
|
inherit (record) content;
|
||||||
|
name = nameFromFQDNAndZone record.fqdn zoneName;
|
||||||
|
type = "TLSA";
|
||||||
|
zone_id = "\${ data.cloudflare_zone.${lib.khscodes.sanitize-terraform-name zoneName}.id }";
|
||||||
|
comment = "app=${zoneName}";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) cfg.dns.tlsaRecords)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,12 @@
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
khscodes.infrastructure = {
|
khscodes.infrastructure = {
|
||||||
|
kanidm-client-application = {
|
||||||
|
enable = true;
|
||||||
|
appName = "stalwart";
|
||||||
|
secretOwner = "dovecot2";
|
||||||
|
perms = "0600";
|
||||||
|
};
|
||||||
hetzner-instance = {
|
hetzner-instance = {
|
||||||
enable = true;
|
enable = true;
|
||||||
mapRdns = true;
|
mapRdns = true;
|
||||||
|
@ -46,6 +52,20 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
services.stalwart-mail.settings.directory.memory = {
|
||||||
|
type = "memory";
|
||||||
|
principals = [
|
||||||
|
{
|
||||||
|
class = "individual";
|
||||||
|
name = "khs";
|
||||||
|
fullName = "Kaare Agerlin Skovgaard";
|
||||||
|
email = [
|
||||||
|
"kaare@agerlinskovgaard.dk"
|
||||||
|
"kaare@agerlin-skovgaard.dk"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
services.roundcube = {
|
services.roundcube = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostName = "mail.kaareskovgaard.net";
|
hostName = "mail.kaareskovgaard.net";
|
||||||
|
@ -53,34 +73,22 @@
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
# starttls needed for authentication, so the fqdn required to match
|
# starttls needed for authentication, so the fqdn required to match
|
||||||
# the certificate
|
# the certificate
|
||||||
$config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}:465";
|
$config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
||||||
$config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}:993";
|
$config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
||||||
|
$config['oauth_provider'] = 'generic';
|
||||||
|
$config['oauth_provider_name'] = 'Kanidm';
|
||||||
|
$config['oauth_client_id'] = 'stalwart';
|
||||||
|
$config['oauth_client_secret'] = file_get_contents("${config.khscodes.infrastructure.kanidm-client-application.secretFile}");
|
||||||
|
$config['oauth_auth_uri'] = 'https://login.kaareskovgaard.net/ui/oauth2';
|
||||||
|
$config['oauth_token_uri'] = 'https://login.kaareskovgaard.net/oauth2/token';
|
||||||
|
$config['oauth_identity_uri'] = 'https://login.kaareskovgaard.net/oauth2/openid/stalwart/userinfo';
|
||||||
|
$config['oauth_identity_fields'] = ['preferred_username'];
|
||||||
|
$config['oauth_scope'] = 'email openid profile';
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
khscodes.services.nginx = {
|
khscodes.services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts."mx.kaareskovgaard.net" = {
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:8080";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
recommendedProxySettings = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
virtualHosts."mail.kaareskovgaard.net" = { };
|
virtualHosts."mail.kaareskovgaard.net" = { };
|
||||||
virtualHosts."autoconfig.kaareskovgaard.net" = {
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:8080";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
recommendedProxySettings = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
virtualHosts."autodiscover.kaareskovgaard.net" = {
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:8080";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
recommendedProxySettings = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
khscodes.networking.fqdn = "mx.kaareskovgaard.net";
|
khscodes.networking.fqdn = "mx.kaareskovgaard.net";
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
|
|
|
@ -120,6 +120,19 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
stalwart = {
|
||||||
|
allowedRedirectUris = [ "https://mail.kaareskovgaard.net/index.php/login/oauth" ];
|
||||||
|
landingUri = "https://mail.kaareskovgaard.net";
|
||||||
|
displayName = "Mail";
|
||||||
|
allowInsecureClientDisablePkce = true;
|
||||||
|
scopeMaps = {
|
||||||
|
"mail_user" = [
|
||||||
|
"profile"
|
||||||
|
"email"
|
||||||
|
"openid"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
services.kanidm = {
|
services.kanidm = {
|
||||||
enableServer = true;
|
enableServer = true;
|
||||||
|
@ -163,6 +176,10 @@ in
|
||||||
present = true;
|
present = true;
|
||||||
members = [ "khs" ];
|
members = [ "khs" ];
|
||||||
};
|
};
|
||||||
|
groups.mail_user = {
|
||||||
|
present = true;
|
||||||
|
members = [ "khs" ];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ let
|
||||||
}
|
}
|
||||||
) cfg.applications;
|
) cfg.applications;
|
||||||
systemsOauth2 = lib.attrsets.mapAttrs (key: value: {
|
systemsOauth2 = lib.attrsets.mapAttrs (key: value: {
|
||||||
inherit (value) scopeMaps claimMaps;
|
inherit (value) scopeMaps claimMaps allowInsecureClientDisablePkce;
|
||||||
present = true;
|
present = true;
|
||||||
public = false;
|
public = false;
|
||||||
preferShortUsername = true;
|
preferShortUsername = true;
|
||||||
|
@ -72,6 +72,10 @@ let
|
||||||
allowedRedirectUris = lib.mkOption {
|
allowedRedirectUris = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
};
|
};
|
||||||
|
allowInsecureClientDisablePkce = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
landingUri = lib.mkOption {
|
landingUri = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue