266 lines
8.5 KiB
Nix
266 lines
8.5 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
|
passDbFile = "/run/secret/dovecot/passwd";
|
|
isCatchAllAddress = addr: lib.strings.hasPrefix "@" addr;
|
|
bogusPasswdFile = pkgs.writeTextFile {
|
|
name = "bogus-passwd";
|
|
text = "$6$1234";
|
|
};
|
|
userDbAccountOpts =
|
|
name: account:
|
|
lib.strings.concatStringsSep " " (
|
|
(lib.lists.optional (account.quota != null) "userdb_quota_rule=*:storage=${account.quota}")
|
|
++ [ "userdb_user=${name}" ]
|
|
);
|
|
userDbLine =
|
|
addr: name: account:
|
|
"${addr}:::::::${userDbAccountOpts name account}";
|
|
userDbFile = pkgs.writeTextFile {
|
|
name = "userdb";
|
|
text = lib.strings.concatStringsSep "\n" (
|
|
lib.lists.flatten (
|
|
lib.mapAttrsToList (
|
|
name: account:
|
|
[
|
|
(userDbLine name name account)
|
|
]
|
|
++ (lib.lists.map (addr: userDbLine addr name account) (
|
|
# Don't include catch all addresses, they become aliases in postfix
|
|
lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses
|
|
))
|
|
) cfg.accounts
|
|
)
|
|
);
|
|
};
|
|
firstNonCatchAllAddress =
|
|
account: lib.lists.head (lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses);
|
|
accountOption = lib.khscodes.mkSubmodule {
|
|
description = "mail account";
|
|
options = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "user1@example.com";
|
|
description = "Username";
|
|
};
|
|
|
|
addresses = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
example = [
|
|
"abuse@example.com"
|
|
"postmaster@example.com"
|
|
];
|
|
default = [ ];
|
|
description = ''
|
|
A list of addresses for the account.
|
|
Note: Use list entires like "@example.com" to create a catchAll
|
|
that allows sending from an email address in these domains.
|
|
'';
|
|
};
|
|
|
|
quota = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "2G";
|
|
description = ''
|
|
Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
|
|
obvious meaning. Leave blank for the standard quota `100G`.
|
|
'';
|
|
};
|
|
|
|
sieveScript = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.lines;
|
|
default = null;
|
|
example = ''
|
|
require ["fileinto", "mailbox"];
|
|
|
|
if address :is "from" "gitlab@mg.gitlab.com" {
|
|
fileinto :create "GitLab";
|
|
stop;
|
|
}
|
|
|
|
# This must be the last rule, it will check if list-id is set, and
|
|
# file the message into the Lists folder for further investigation
|
|
elsif header :matches "list-id" "<?*>" {
|
|
fileinto :create "Lists";
|
|
stop;
|
|
}
|
|
'';
|
|
description = ''
|
|
Per-user sieve script.
|
|
'';
|
|
};
|
|
|
|
sendOnly = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Specifies if the account should be a send-only account.
|
|
Emails sent to send-only accounts will be rejected from
|
|
unauthorized senders with the `sendOnlyRejectMessage`
|
|
stating the reason.
|
|
'';
|
|
};
|
|
|
|
sendOnlyRejectMessage = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "This account cannot receive emails.";
|
|
description = ''
|
|
The message that will be returned to the sender when an email is
|
|
sent to a send-only account. Only used if the account is marked
|
|
as send-only.
|
|
'';
|
|
};
|
|
|
|
isLdapAccount = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Marks the account as an LDAP account. Non LDAP accounts gets passwords provisioned, such that other services can use them";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemAccounts = lib.attrsets.foldlAttrs (
|
|
acc: name: value:
|
|
if value.isLdapAccount then acc else acc ++ [ name ]
|
|
) [ ] cfg.accounts;
|
|
|
|
systemAccountsPassDbTemplateContents =
|
|
lib.concatStringsSep "\n" (
|
|
lib.lists.map (account: ''
|
|
{{- with secret "mx.kaareskovgaard.net/data/users/${account}" -}}
|
|
${account}:{{ .Data.data.hashed_password }}::::::
|
|
{{- end -}}
|
|
'') systemAccounts
|
|
)
|
|
# Just make sure the file is not empty
|
|
+ "\n";
|
|
|
|
catchAllAliases = lib.strings.concatStringsSep "\n" (
|
|
lib.lists.flatten (
|
|
lib.attrsets.mapAttrsToList (
|
|
name: account:
|
|
let
|
|
regularEmailAddr = firstNonCatchAllAddress account;
|
|
catchAlls = lib.lists.filter isCatchAllAddress account.addresses;
|
|
in
|
|
lib.lists.map (addr: "${addr} ${regularEmailAddr}") catchAlls
|
|
) cfg.accounts
|
|
)
|
|
);
|
|
postmasterAliases = lib.strings.concatStringsSep "\n" (
|
|
lib.lists.map (domain: "postmaster@${domain} ${cfg.postmaster}") cfg.domains
|
|
);
|
|
|
|
abuseAliases = lib.strings.concatStringsSep "\n" (
|
|
lib.lists.map (domain: "abuse@${domain} ${cfg.abuse}") cfg.domains
|
|
);
|
|
extraVirtualAliases = lib.strings.concatStringsSep "\n" [
|
|
catchAllAliases
|
|
postmasterAliases
|
|
abuseAliases
|
|
];
|
|
in
|
|
{
|
|
options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption {
|
|
type = lib.types.attrsOf accountOption;
|
|
default = { };
|
|
};
|
|
config = {
|
|
mailserver = {
|
|
loginAccounts = lib.attrsets.mapAttrs (name: value: {
|
|
inherit (value)
|
|
name
|
|
quota
|
|
sieveScript
|
|
sendOnly
|
|
sendOnlyRejectMessage
|
|
;
|
|
aliases = value.addresses;
|
|
hashedPasswordFile = bogusPasswdFile;
|
|
}) cfg.accounts;
|
|
extraVirtualAliases = { };
|
|
};
|
|
khscodes.infrastructure.vault-server-approle.policy = {
|
|
"mx.kaareskovgaard.net/data/users/*" = {
|
|
capabilities = [ "read" ];
|
|
};
|
|
};
|
|
khscodes.infrastructure.provisioning.pre.modules = [
|
|
{
|
|
terraform.required_providers.random = {
|
|
source = "hashicorp/random";
|
|
version = "3.7.2";
|
|
};
|
|
provider.random = { };
|
|
}
|
|
]
|
|
++ (lib.lists.map (
|
|
account:
|
|
let
|
|
tfName = lib.khscodes.sanitize-terraform-name account;
|
|
in
|
|
{ config, ... }:
|
|
{
|
|
resource.random_password."${tfName}" = {
|
|
length = 48;
|
|
numeric = true;
|
|
lower = true;
|
|
upper = true;
|
|
special = false;
|
|
};
|
|
|
|
resource.vault_kv_secret_v2."${tfName}" = {
|
|
mount = config.khscodes.vault.output.mount."mx.kaareskovgaard.net".path;
|
|
name = "users/${account}";
|
|
data_json = ''
|
|
{
|
|
"hashed_password": ''${ jsonencode(resource.random_password.${tfName}.bcrypt_hash) },
|
|
"password": ''${ jsonencode(resource.random_password.${tfName}.result) }
|
|
}
|
|
'';
|
|
};
|
|
}
|
|
) systemAccounts);
|
|
khscodes.services.vault-agent.templates = [
|
|
{
|
|
contents = systemAccountsPassDbTemplateContents;
|
|
destination = passDbFile;
|
|
perms = "0600";
|
|
owner = "dovecot2";
|
|
group = "dovecot2";
|
|
restartUnits = [
|
|
"dovecot2.service"
|
|
];
|
|
}
|
|
];
|
|
systemd.services.dovecot2 = {
|
|
unitConfig.ConditionPathExists = [ passDbFile ];
|
|
serviceConfig.ReadOnlyPaths = [ passDbFile ];
|
|
# simple-nixos-mailserver creates its own passwd file,
|
|
# but the passwords in that file are all bogus, so replace them
|
|
# with our own.
|
|
preStart = lib.mkAfter ''
|
|
cp ${passDbFile} /run/dovecot2/passwd
|
|
cp ${userDbFile} /run/dovecot2/userdb
|
|
'';
|
|
};
|
|
# The stuff simple nixos-mailserver sets up here creates aliases to the accounts listed,
|
|
# but I use usernames for the accounts, which ends up creating forwards to stuff like khs@mx.kaareskovgaard.net, which causes issues.
|
|
# Instead I just want the entries in virtual_mailbox_maps for all regular addresses, which will leave the addresses alone (ie. not rewrite them)
|
|
# and send them off via lmtp to dovecot2. Dovecot2 has a fixed userdb which accepts all regular email addresses for every login and maps it to that
|
|
# users mailbox. The only thing not handled this way is catchAll addresses (and regexes, which I do not support at the moment, and hopefully won't need).
|
|
services.postfix = {
|
|
virtual = lib.mkForce extraVirtualAliases;
|
|
config.virtual_alias_maps = lib.mkForce [ "hash:/etc/postfix/virtual" ];
|
|
};
|
|
# This prevents local usernames without domain names to get rewritten.
|
|
# services.postfix.submissionOptions = submissionOptions;
|
|
# services.postfix.submissionsOptions = submissionOptions;
|
|
};
|
|
}
|