Compare commits
2 commits
cc1ab841c2
...
fbe957b046
Author | SHA1 | Date | |
---|---|---|---|
fbe957b046 | |||
02325a7017 |
20 changed files with 436 additions and 295 deletions
|
@ -1,9 +0,0 @@
|
||||||
{ lib, config, ... }:
|
|
||||||
let
|
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
khscodes.services.nginx.virtualHosts."${config.khscodes.networking.fqdn}" = { };
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
{ lib, config, ... }:
|
|
||||||
let
|
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
|
||||||
secretFile = "/run/secret/dovecot/ldap";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.khscodes.infrastructure.mailserver = {
|
|
||||||
ldap = {
|
|
||||||
mount = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
mailserver.ldap = {
|
|
||||||
enable = true;
|
|
||||||
uris = [ "ldaps://login.kaareskovgaard.net" ];
|
|
||||||
searchBase = "dc=login,dc=kaareskovgaard,dc=net";
|
|
||||||
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 = {
|
|
||||||
dovecot2 = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = [ secretFile ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
postfix = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = [ secretFile ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
postfix-setup = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = [ secretFile ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
khscodes.services.vault-agent.templates = [
|
|
||||||
{
|
|
||||||
contents = ''
|
|
||||||
{{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}}
|
|
||||||
{{ .Data.data.apiToken }}
|
|
||||||
{{- end -}}
|
|
||||||
'';
|
|
||||||
destination = secretFile;
|
|
||||||
owner = "dovecot";
|
|
||||||
group = "dovecot";
|
|
||||||
restartUnits = [
|
|
||||||
"dovecot2.service"
|
|
||||||
"postfix.service"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
# TODO: Include a similiar rule for openstack
|
|
||||||
khscodes.infrastructure.hetzner-instance.extraFirewallRules = [
|
|
||||||
{
|
|
||||||
direction = "out";
|
|
||||||
protocol = "tcp";
|
|
||||||
port = 636;
|
|
||||||
destination_ips = [
|
|
||||||
"0.0.0.0/0"
|
|
||||||
"::/0"
|
|
||||||
];
|
|
||||||
description = "ldaps";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
|
||||||
"${cfg.ldap.mount}/data/${cfg.ldap.path}" = {
|
|
||||||
capabilities = [ "read" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
{ config, lib, ... }:
|
|
||||||
let
|
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.dovecot2.protocols = [ "sieve" ];
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,84 +1,46 @@
|
||||||
{
|
{
|
||||||
inputs,
|
inputs,
|
||||||
config,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
"${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
|
"${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
|
||||||
# ./testuser.nix
|
./mailserver
|
||||||
];
|
];
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
khscodes = {
|
||||||
(
|
infrastructure = {
|
||||||
{ ... }:
|
hetzner-instance = {
|
||||||
{
|
enable = true;
|
||||||
khscodes.vault = {
|
mapRdns = true;
|
||||||
enable = true;
|
server_type = "cax11";
|
||||||
mount."mx.kaareskovgaard.net" = {
|
};
|
||||||
path = "mx.kaareskovgaard.net";
|
provisioning.pre.modules = [
|
||||||
type = "kv";
|
(
|
||||||
options = {
|
{ ... }:
|
||||||
version = "2";
|
{
|
||||||
|
khscodes.vault = {
|
||||||
|
enable = true;
|
||||||
|
mount."mx.kaareskovgaard.net" = {
|
||||||
|
path = "mx.kaareskovgaard.net";
|
||||||
|
type = "kv";
|
||||||
|
options = {
|
||||||
|
version = "2";
|
||||||
|
};
|
||||||
|
description = "Secrets used for mx.kaareskovgaard.net";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
description = "Secrets used for mx.kaareskovgaard.net";
|
}
|
||||||
};
|
)
|
||||||
};
|
];
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
khscodes.infrastructure = {
|
|
||||||
kanidm-client-application = {
|
|
||||||
enable = true;
|
|
||||||
appName = "dovecot";
|
|
||||||
secretOwner = "dovecot2";
|
|
||||||
perms = "0644";
|
|
||||||
};
|
};
|
||||||
hetzner-instance = {
|
"mx.kaareskovgaard.net" = {
|
||||||
enable = true;
|
|
||||||
mapRdns = true;
|
|
||||||
server_type = "cax11";
|
|
||||||
};
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
domains = [
|
domains = [
|
||||||
"agerlin-skovgaard.dk"
|
"agerlin-skovgaard.dk"
|
||||||
"agerlinskovgaard.dk"
|
"agerlinskovgaard.dk"
|
||||||
];
|
];
|
||||||
dkim = {
|
accounts = import ./users.nix;
|
||||||
vault = {
|
|
||||||
mount = "mx.kaareskovgaard.net";
|
|
||||||
prefixPath = "dkim";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
ldap = {
|
|
||||||
mount = "kanidm";
|
|
||||||
path = "ldap/dovecot";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
services.roundcube = {
|
|
||||||
enable = true;
|
|
||||||
hostName = "mail.kaareskovgaard.net";
|
|
||||||
configureNginx = true;
|
|
||||||
extraConfig = ''
|
|
||||||
# starttls needed for authentication, so the fqdn required to match
|
|
||||||
# the certificate
|
|
||||||
$config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
|
||||||
$config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
|
||||||
$config['oauth_provider'] = 'generic';
|
|
||||||
$config['oauth_provider_name'] = 'Kanidm';
|
|
||||||
$config['oauth_client_id'] = 'dovecot';
|
|
||||||
$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/dovecot/userinfo';
|
|
||||||
$config['oauth_identity_fields'] = ['preferred_username'];
|
|
||||||
$config['oauth_scope'] = 'email openid profile';
|
|
||||||
$config['plugins'] = [
|
|
||||||
'managesieve',
|
|
||||||
];
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
khscodes.services.nginx = {
|
khscodes.services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts."mail.kaareskovgaard.net" = { };
|
virtualHosts."mail.kaareskovgaard.net" = { };
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
|
passDbFile = "/run/secret/dovecot/passwd";
|
||||||
|
bogusPasswdFile = pkgs.writeTextFile {
|
||||||
|
name = "bogus-passwd";
|
||||||
|
text = "$6$1234";
|
||||||
|
};
|
||||||
|
accountPrimaryEmail =
|
||||||
|
name: account: if account.isLdapAccount then lib.lists.head account.aliases else name;
|
||||||
|
accountAlternativeEmails =
|
||||||
|
name: account:
|
||||||
|
if account.isLdapAccount then
|
||||||
|
lib.lists.ifilter0 (idx: _: idx > 0) account.aliases
|
||||||
|
else
|
||||||
|
account.aliases;
|
||||||
|
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.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.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";
|
||||||
|
|
||||||
|
# I would like to not use emails as usernames, but this is not something postfix is particularly happy with,
|
||||||
|
# as far as I can see. I *think* I can do this by randomly selecting an address, ie. the first, as the "primary"
|
||||||
|
# address, and then map all other addresses to that one. Then create a virtual mailbox map that maps that back
|
||||||
|
# to the username before delivering it to lmtp. All of this just worked when using ldap, but that meant
|
||||||
|
# not using code configured accounts set up with opentofu as well, and would also make the email delivery
|
||||||
|
# take a hard dependency on ldap (and not just the login process).
|
||||||
|
|
||||||
|
# Create extra virtual aliases for all accounts (as they are not emails, that maps them to their primary email).
|
||||||
|
# Then we map those emails back into the account name in the mailbox maps.
|
||||||
|
extraVirtualAliases = lib.mapAttrs (name: account: accountPrimaryEmail name account) cfg.accounts;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf accountOption;
|
||||||
|
default = { };
|
||||||
|
};
|
||||||
|
config = {
|
||||||
|
mailserver = {
|
||||||
|
inherit extraVirtualAliases;
|
||||||
|
loginAccounts = lib.attrsets.mapAttrs (name: value: {
|
||||||
|
inherit (value)
|
||||||
|
name
|
||||||
|
aliasesRegexp
|
||||||
|
catchAll
|
||||||
|
quota
|
||||||
|
sieveScript
|
||||||
|
sendOnly
|
||||||
|
sendOnlyRejectMessage
|
||||||
|
;
|
||||||
|
aliases = accountAlternativeEmails name value;
|
||||||
|
hashedPasswordFile = bogusPasswdFile;
|
||||||
|
}) cfg.accounts;
|
||||||
|
};
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# This prevents local usernames without domain names to get rewritten.
|
||||||
|
# services.postfix.submissionOptions = submissionOptions;
|
||||||
|
# services.postfix.submissionsOptions = submissionOptions;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
khscodes.services.nginx.virtualHosts."${config.khscodes.networking.fqdn}" = { };
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,12 +5,11 @@
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
fqdn = config.khscodes.networking.fqdn;
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.khscodes.infrastructure.mailserver = {
|
options.khscodes."mx.kaareskovgaard.net" = {
|
||||||
enable = lib.mkEnableOption "Enables setting up stuff for a mail server";
|
|
||||||
domains = lib.mkOption {
|
domains = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
@ -18,6 +17,7 @@ in
|
||||||
};
|
};
|
||||||
imports = [
|
imports = [
|
||||||
inputs.simple-nixos-mailserver.nixosModules.mailserver
|
inputs.simple-nixos-mailserver.nixosModules.mailserver
|
||||||
|
./accounts.nix
|
||||||
./acme.nix
|
./acme.nix
|
||||||
./dmarc.nix
|
./dmarc.nix
|
||||||
./dane.nix
|
./dane.nix
|
||||||
|
@ -29,9 +29,9 @@ in
|
||||||
./prometheus.nix
|
./prometheus.nix
|
||||||
./openid-connect.nix
|
./openid-connect.nix
|
||||||
./ldap.nix
|
./ldap.nix
|
||||||
|
./roundcube.nix
|
||||||
];
|
];
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
# TODO: Include a similiar rule for openstack
|
|
||||||
khscodes.infrastructure.hetzner-instance.extraFirewallRules = [
|
khscodes.infrastructure.hetzner-instance.extraFirewallRules = [
|
||||||
{
|
{
|
||||||
direction = "out";
|
direction = "out";
|
||||||
|
@ -80,6 +80,7 @@ in
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
mailserver = {
|
mailserver = {
|
||||||
|
inherit (cfg) domains;
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = false;
|
enableImap = false;
|
||||||
enableImapSsl = true;
|
enableImapSsl = true;
|
||||||
|
@ -87,7 +88,6 @@ in
|
||||||
enableSubmissionSsl = true;
|
enableSubmissionSsl = true;
|
||||||
fqdn = config.khscodes.networking.fqdn;
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
useUTF8FolderNames = true;
|
useUTF8FolderNames = true;
|
||||||
domains = cfg.domains;
|
|
||||||
certificateScheme = "acme";
|
certificateScheme = "acme";
|
||||||
};
|
};
|
||||||
services.fail2ban.jails = {
|
services.fail2ban.jails = {
|
|
@ -5,7 +5,10 @@
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
|
mount = "mx.kaareskovgaard.net";
|
||||||
|
mountExpr = "\${ vault_mount.${lib.khscodes.sanitize-terraform-name mount}.path }";
|
||||||
|
prefixPath = "dkim";
|
||||||
opensslStripAsn1 = pkgs.writeTextFile {
|
opensslStripAsn1 = pkgs.writeTextFile {
|
||||||
name = "openssl-strip-asn1";
|
name = "openssl-strip-asn1";
|
||||||
executable = true;
|
executable = true;
|
||||||
|
@ -48,23 +51,9 @@ let
|
||||||
''''${ replace(trimprefix(trimsuffix(${tls_key}.public_key_pem, ${publicKeyEnd}), ${publicKeyBegin}), "\n", "") }'';
|
''''${ replace(trimprefix(trimsuffix(${tls_key}.public_key_pem, ${publicKeyEnd}), ${publicKeyBegin}), "\n", "") }'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.khscodes.infrastructure.mailserver.dkim = {
|
config = {
|
||||||
vault = {
|
|
||||||
mount = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
mountExpr = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "\${ vault_mount.${lib.khscodes.sanitize-terraform-name cfg.dkim.vault.mount}.path }";
|
|
||||||
};
|
|
||||||
prefixPath = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
config = lib.mkIf (cfg.enable) {
|
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
khscodes.infrastructure.vault-server-approle.policy = {
|
||||||
"${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/*" = {
|
"${mount}/data/${prefixPath}/*" = {
|
||||||
capabilities = [ "read" ];
|
capabilities = [ "read" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -142,8 +131,8 @@ in
|
||||||
lib.lists.map (domain: {
|
lib.lists.map (domain: {
|
||||||
name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa";
|
name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa";
|
||||||
value = {
|
value = {
|
||||||
mount = cfg.dkim.vault.mountExpr;
|
mount = mountExpr;
|
||||||
name = cfg.dkim.vault.prefixPath + "/${domain}/rsa";
|
name = prefixPath + "/${domain}/rsa";
|
||||||
data_json = ''
|
data_json = ''
|
||||||
{ "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa.private_key_pem) } }
|
{ "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_rsa.private_key_pem) } }
|
||||||
'';
|
'';
|
||||||
|
@ -154,8 +143,8 @@ in
|
||||||
lib.lists.map (domain: {
|
lib.lists.map (domain: {
|
||||||
name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519";
|
name = "${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519";
|
||||||
value = {
|
value = {
|
||||||
mount = cfg.dkim.vault.mountExpr;
|
mount = mountExpr;
|
||||||
name = cfg.dkim.vault.prefixPath + "/${domain}/ed25519";
|
name = prefixPath + "/${domain}/ed25519";
|
||||||
data_json = ''
|
data_json = ''
|
||||||
{ "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519.private_key_pem) } }
|
{ "dkim_private_key": ''${ jsonencode(resource.tls_private_key.${lib.khscodes.sanitize-terraform-name domain}_dkim_ed25519.private_key_pem) } }
|
||||||
'';
|
'';
|
||||||
|
@ -169,7 +158,7 @@ in
|
||||||
lib.lists.map (domain: [
|
lib.lists.map (domain: [
|
||||||
{
|
{
|
||||||
contents = ''
|
contents = ''
|
||||||
{{- with secret "${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/${domain}/rsa" -}}
|
{{- with secret "${mount}/data/${prefixPath}/${domain}/rsa" -}}
|
||||||
{{ .Data.data.dkim_private_key }}
|
{{ .Data.data.dkim_private_key }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
'';
|
'';
|
||||||
|
@ -185,7 +174,7 @@ in
|
||||||
# rspamd does not like reading ed25519 keys with begin/end pairs, as they get parsed as
|
# rspamd does not like reading ed25519 keys with begin/end pairs, as they get parsed as
|
||||||
# rsa keys
|
# rsa keys
|
||||||
contents = ''
|
contents = ''
|
||||||
{{- with secret "${cfg.dkim.vault.mount}/data/${cfg.dkim.vault.prefixPath}/${domain}/ed25519" -}}
|
{{- with secret "${mount}/data/${prefixPath}/${domain}/ed25519" -}}
|
||||||
{{ .Data.data.dkim_private_key }}
|
{{ .Data.data.dkim_private_key }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
'';
|
'';
|
|
@ -1,9 +1,9 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
{
|
{
|
||||||
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
ldapMount = "kanidm";
|
||||||
|
ldapPath = "ldap/dovecot";
|
||||||
|
secretFile = "/run/secret/dovecot/dovecot-ldap.conf.ext";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
services.dovecot2.extraConfig = ''
|
||||||
|
passdb {
|
||||||
|
driver = ldap
|
||||||
|
args = ${secretFile}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
systemd.services = {
|
||||||
|
dovecot2 = {
|
||||||
|
unitConfig = {
|
||||||
|
ConditionPathExists = [ secretFile ];
|
||||||
|
};
|
||||||
|
serviceConfig.ReadOnlyPaths = [
|
||||||
|
secretFile
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
khscodes.services.vault-agent.templates = [
|
||||||
|
{
|
||||||
|
contents = ''
|
||||||
|
{{- with secret "${ldapMount}/data/${ldapPath}" -}}
|
||||||
|
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 -}}
|
||||||
|
'';
|
||||||
|
destination = secretFile;
|
||||||
|
owner = "root";
|
||||||
|
group = "root";
|
||||||
|
restartUnits = [
|
||||||
|
"dovecot2.service"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
|
# TODO: Include a similiar rule for openstack
|
||||||
|
khscodes.infrastructure.hetzner-instance.extraFirewallRules = [
|
||||||
|
{
|
||||||
|
direction = "out";
|
||||||
|
protocol = "tcp";
|
||||||
|
port = 636;
|
||||||
|
destination_ips = [
|
||||||
|
"0.0.0.0/0"
|
||||||
|
"::/0"
|
||||||
|
];
|
||||||
|
description = "ldaps";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
khscodes.infrastructure.vault-server-approle.policy = {
|
||||||
|
"${ldapMount}/data/${ldapPath}" = {
|
||||||
|
capabilities = [ "read" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
config = {
|
||||||
|
services.dovecot2.protocols = [ "sieve" ];
|
||||||
|
services.roundcube.extraConfig = ''
|
||||||
|
$config['plugins'] = [
|
||||||
|
'managesieve',
|
||||||
|
];
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
fqdn = config.khscodes.networking.fqdn;
|
fqdn = config.khscodes.networking.fqdn;
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
# Increment this if ever changing mta-sts settings.
|
# Increment this if ever changing mta-sts settings.
|
||||||
policyVersion = 2;
|
policyVersion = 2;
|
||||||
mtaStsWellKnown = pkgs.writeTextFile {
|
mtaStsWellKnown = pkgs.writeTextFile {
|
||||||
|
@ -20,7 +20,7 @@ let
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
khscodes.services.nginx.virtualHosts = (
|
khscodes.services.nginx.virtualHosts = (
|
||||||
lib.listToAttrs (
|
lib.listToAttrs (
|
||||||
lib.lists.map (domain: {
|
lib.lists.map (domain: {
|
|
@ -1,10 +1,15 @@
|
||||||
{ config, lib, ... }:
|
{ config, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
|
||||||
oauthConfigFile = "/run/secret/dovecot/dovecot-oauth2.conf.ext";
|
oauthConfigFile = "/run/secret/dovecot/dovecot-oauth2.conf.ext";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
|
khscodes.infrastructure.kanidm-client-application = {
|
||||||
|
enable = true;
|
||||||
|
appName = "dovecot";
|
||||||
|
secretOwner = "root";
|
||||||
|
perms = "0644";
|
||||||
|
};
|
||||||
khscodes.services.vault-agent.templates = [
|
khscodes.services.vault-agent.templates = [
|
||||||
{
|
{
|
||||||
contents = ''
|
contents = ''
|
||||||
|
@ -23,6 +28,17 @@ in
|
||||||
restartUnits = [ "dovecot2.service" ];
|
restartUnits = [ "dovecot2.service" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
services.roundcube.extraConfig = ''
|
||||||
|
$config['oauth_provider'] = 'generic';
|
||||||
|
$config['oauth_provider_name'] = 'Kanidm';
|
||||||
|
$config['oauth_client_id'] = 'dovecot';
|
||||||
|
$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/dovecot/userinfo';
|
||||||
|
$config['oauth_identity_fields'] = ['preferred_username'];
|
||||||
|
$config['oauth_scope'] = 'email openid profile';
|
||||||
|
'';
|
||||||
services.dovecot2.extraConfig = ''
|
services.dovecot2.extraConfig = ''
|
||||||
auth_mechanisms = $auth_mechanisms oauthbearer xoauth2
|
auth_mechanisms = $auth_mechanisms oauthbearer xoauth2
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
{ config, lib, ... }:
|
|
||||||
let
|
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
|
|
||||||
services.prometheus.exporters.postfix = {
|
services.prometheus.exporters.postfix = {
|
||||||
enable = true;
|
enable = true;
|
|
@ -0,0 +1,18 @@
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
|
services.roundcube = {
|
||||||
|
enable = true;
|
||||||
|
hostName = "mail.kaareskovgaard.net";
|
||||||
|
configureNginx = true;
|
||||||
|
extraConfig = ''
|
||||||
|
# starttls needed for authentication, so the fqdn required to match
|
||||||
|
# the certificate
|
||||||
|
$config['smtp_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
||||||
|
$config['imap_host'] = "ssl://${config.khscodes.networking.fqdn}";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
khscodes.services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
virtualHosts."mail.kaareskovgaard.net" = { };
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
{
|
{
|
||||||
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
|
@ -1,9 +1,9 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.khscodes.infrastructure.mailserver;
|
cfg = config.khscodes."mx.kaareskovgaard.net";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
khscodes.infrastructure.provisioning.pre.modules = [
|
||||||
{
|
{
|
||||||
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
khscodes.cloudflare.dns.txtRecords = lib.lists.map (domain: {
|
|
@ -1,71 +0,0 @@
|
||||||
{
|
|
||||||
khscodes.infrastructure.vault-server-approle.policy = {
|
|
||||||
"mail.kaareskovgaard.net/data/users/*" = {
|
|
||||||
capabilities = [ "read" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
khscodes.services.vault-agent.templates = [
|
|
||||||
{
|
|
||||||
contents = ''
|
|
||||||
{{- with secret "mail.kaareskovgaard.net/data/users/test" -}}
|
|
||||||
{{ .Data.data.hashed_password }}
|
|
||||||
{{- end -}}
|
|
||||||
'';
|
|
||||||
destination = "/run/secret/mailserver/users/test.passwd.hash";
|
|
||||||
perms = "0600";
|
|
||||||
owner = "rspamd";
|
|
||||||
group = "rspamd";
|
|
||||||
restartUnits = [
|
|
||||||
"rspamd.service"
|
|
||||||
"postfix.service"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
systemd.services.postfix = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = [ "/run/secret/mailserver/users/test.passwd.hash" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
|
||||||
unitConfig = {
|
|
||||||
ConditionPathExists = [ "/run/secret/mailserver/users/test.passwd.hash" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
khscodes.infrastructure.provisioning.pre.modules = [
|
|
||||||
(
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
terraform.required_providers.random = {
|
|
||||||
source = "hashicorp/random";
|
|
||||||
version = "3.7.2";
|
|
||||||
};
|
|
||||||
provider.random = { };
|
|
||||||
|
|
||||||
resource.random_password.test = {
|
|
||||||
length = 48;
|
|
||||||
numeric = true;
|
|
||||||
lower = true;
|
|
||||||
upper = true;
|
|
||||||
special = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
resource.vault_kv_secret_v2.test = {
|
|
||||||
mount = config.khscodes.vault.output.mount."mail.kaareskovgaard.net".path;
|
|
||||||
name = "users/test";
|
|
||||||
data_json = ''
|
|
||||||
{
|
|
||||||
"hashed_password": ''${ jsonencode(resource.random_password.test.bcrypt_hash) },
|
|
||||||
"password": ''${ jsonencode(resource.random_password.test.result) }
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
mailserver.loginAccounts = {
|
|
||||||
"test@agerlin-skovgaard.dk" = {
|
|
||||||
hashedPasswordFile = "/run/secret/mailserver/users/test.passwd.hash";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
11
nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix
Normal file
11
nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"khs" = {
|
||||||
|
isLdapAccount = true;
|
||||||
|
name = "Kaare Agerlin Skovgaard";
|
||||||
|
aliases = [
|
||||||
|
"kaare@agerlin-skovgaard.dk"
|
||||||
|
"kaare@agerlinskovgaard.dk"
|
||||||
|
];
|
||||||
|
quota = "10G";
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue