Begin preparing to move LDAP accounts into passdb only

This should allow LDAP accounts to have password
set in LDAP, as well as provisioning service accounts
statically in nix.

This will also move alias configuration of all accounts
into nix as well.
This commit is contained in:
Kaare Hoff Skovgaard 2025-07-30 21:36:48 +02:00
parent cc1ab841c2
commit 02325a7017
Signed by: khs
GPG key ID: C7D890804F01E9F0
2 changed files with 158 additions and 40 deletions

View file

@ -0,0 +1,128 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.khscodes.infrastructure.mailserver;
accountOption = lib.khscodes.mkSubmodule {
description = "mail account";
options = {
name = lib.mkOption {
type = lib.types.str;
example = "user1@example.com";
description = "Username";
};
aliases = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [
"abuse@example.com"
"postmaster@example.com"
];
default = [ ];
description = ''
A list of aliases of this login account.
Note: Use list entries like "@example.com" to create a catchAll
that allows sending from all email addresses in these domain.
'';
};
aliasesRegexp = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ ''/^tom\..*@domain\.com$/'' ];
default = [ ];
description = ''
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
'';
};
catchAll = lib.typs.mkOption {
type = lib.types.listOf (lib.types.enum config.mailserver.domains);
example = [
"example.com"
"example2.com"
];
default = [ ];
description = ''
For which domains should this account act as a catch all?
Note: Does not allow sending from all addresses of 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.types.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;
in
{
options.khscodes.infrastructure.mailserver.accounts = lib.mkOption {
type = lib.types.attrsOf accountOption;
default = { };
};
config = lib.mkIf cfg.enable {
};
}

View file

@ -1,7 +1,12 @@
{ lib, config, ... }: {
lib,
config,
pkgs,
...
}:
let let
cfg = config.khscodes.infrastructure.mailserver; cfg = config.khscodes.infrastructure.mailserver;
secretFile = "/run/secret/dovecot/ldap"; secretFile = "/run/secret/dovecot/dovecot-ldap.conf.ext";
in in
{ {
options.khscodes.infrastructure.mailserver = { options.khscodes.infrastructure.mailserver = {
@ -16,45 +21,20 @@ in
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
mailserver.ldap = { services.dovecot2.extraConfig = ''
enable = true; passdb {
uris = [ "ldaps://login.kaareskovgaard.net" ]; driver = ldap
searchBase = "dc=login,dc=kaareskovgaard,dc=net"; args = ${secretFile}
searchScope = "sub"; }
bind = { '';
dn = "dn=token";
passwordFile = secretFile;
};
dovecot = {
# Map LDAP uid to dovecot user, and ldap userPassword to dovecot password
passAttrs = "uid=user";
passFilter = "(&(class=account)(memberOf=mail_user)(uid=%u))";
# This filter is used both when receiving mail (thus needing to lookup by mail address, and when authenticating, requriing the lookup by uid.)
# Note that the pass filter only allows looking up by uid, so should still only be able to authenticate using that.
userFilter = "(&(class=account)(memberOf=mail_user)(|(mail=%u)(uid=%u)))";
userAttrs = "uid=user";
};
postfix = {
filter = "(&(class=account)(memberOf=mail_user)(mail=%s))";
mailAttribute = "uid";
uidAttribute = "uid";
};
};
systemd.services = { systemd.services = {
dovecot2 = { dovecot2 = {
unitConfig = { unitConfig = {
ConditionPathExists = [ secretFile ]; ConditionPathExists = [ secretFile ];
}; };
}; serviceConfig.ReadOnlyPaths = [
postfix = { secretFile
unitConfig = { ];
ConditionPathExists = [ secretFile ];
};
};
postfix-setup = {
unitConfig = {
ConditionPathExists = [ secretFile ];
};
}; };
}; };
@ -62,15 +42,25 @@ in
{ {
contents = '' contents = ''
{{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}} {{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}}
{{ .Data.data.apiToken }} ldap_version = 3
uris = ldaps://login.kaareskovgaard.net
tls_require_cert = hard
tls_ca_cert_file = ${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
dn = dn=token
dnpass = {{ .Data.data.apiToken }}
auth_bind = yes
base = dc=login,dc=kaareskovgaard,dc=net
scope = subtree
pass_attrs = uid=user
pass_filter = (&(class=account)(memberOf=mail_user)(uid=%u)
{{- end -}} {{- end -}}
''; '';
destination = secretFile; destination = secretFile;
owner = "dovecot"; owner = "root";
group = "dovecot"; group = "root";
restartUnits = [ restartUnits = [
"dovecot2.service" "dovecot2.service"
"postfix.service"
]; ];
} }
]; ];