roundcube & mailserver: fix oauth: mailserver is an OAuth secret donor

Both of them use the same client ID and client secret, but Roundcube
depends on mailserver generally, so mailserver is the one to share OAuth
client id and secret.
This commit is contained in:
Alexander Tomokhov
2025-01-31 14:31:09 +04:00
parent 89e7145a01
commit 4c6228d694
7 changed files with 86 additions and 52 deletions

View File

@@ -7,5 +7,7 @@
[ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ],
[ "selfprivacy", "domain" ], [ "selfprivacy", "domain" ],
[ "selfprivacy", "modules", "auth" ], [ "selfprivacy", "modules", "auth" ],
[ "selfprivacy", "modules", "roundcube" ] [ "selfprivacy", "modules", "roundcube" ],
[ "selfprivacy", "passthru", "mailserver", "oauth-client-id" ],
[ "selfprivacy", "passthru", "mailserver", "oauth-client-secret-fp" ]
] ]

View File

@@ -5,24 +5,21 @@ let
is-auth-enabled = config.selfprivacy.modules.auth.enable or false; is-auth-enabled = config.selfprivacy.modules.auth.enable or false;
auth-passthru = config.passthru.selfprivacy.auth; auth-passthru = config.passthru.selfprivacy.auth;
auth-fqdn = auth-passthru.auth-fqdn; auth-fqdn = auth-passthru.auth-fqdn;
oauth-client-id = "roundcube"; sp-module-name = "roundcube";
roundcube-user = "roundcube"; user = "roundcube";
roundcube-group = "roundcube"; group = "roundcube";
kanidmExecStartPreScriptRoot = pkgs.writeShellScript oauth-donor = config.selfprivacy.passthru.mailserver;
"${oauth-client-id}-kanidm-ExecStartPre-root-script.sh"
''
# set-group-ID bit allows for kanidm user to create files,
mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id}
chown kanidm:${roundcube-group} /run/keys/${oauth-client-id}
'';
kanidm-oauth-client-secret-fp = kanidm-oauth-client-secret-fp =
"/run/keys/${oauth-client-id}/kanidm-oauth-client-secret"; "/run/keys/${group}/kanidm-oauth-client-secret";
kanidmExecStartPreScript = pkgs.writeShellScript kanidmExecStartPreScriptRoot = pkgs.writeShellScript
"${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' "${sp-module-name}-kanidm-ExecStartPre-root-script.sh"
set -o xtrace ''
[ -f "${kanidm-oauth-client-secret-fp}" ] || \ # set-group-ID bit allows for kanidm user to create files inheriting group
"${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32 mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group}
''; chown kanidm:${group} /run/keys/${group}
install -v -m640 -o kanidm -g ${group} ${oauth-donor.oauth-client-secret-fp} ${kanidm-oauth-client-secret-fp}
'';
in in
{ {
options.selfprivacy.modules.roundcube = { options.selfprivacy.modules.roundcube = {
@@ -72,21 +69,23 @@ in
}; };
systemd.slices.roundcube.description = "Roundcube service slice"; systemd.slices.roundcube.description = "Roundcube service slice";
# Roundcube depends on Dovecot and its OAuth2 client secret.
systemd.services.roundcube.after = [ "dovecot2.service" ];
} }
# the following part is active only when "auth" module is enabled # the following part is active only when "auth" module is enabled
(lib.attrsets.optionalAttrs (lib.attrsets.optionalAttrs
(options.selfprivacy.modules ? "auth") (options.selfprivacy.modules ? "auth")
(lib.mkIf is-auth-enabled { (lib.mkIf is-auth-enabled {
# for phpfpm-roundcube to have access to get through /run/keys directory # for phpfpm-roundcube to have access to get through /run/keys directory
users.groups.keys.members = [ roundcube-user ]; users.groups.keys.members = [ user ];
services.roundcube.extraConfig = lib.mkAfter '' services.roundcube.extraConfig = lib.mkAfter ''
$config['oauth_provider'] = 'generic'; $config['oauth_provider'] = 'generic';
$config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}';
$config['oauth_client_id'] = '${oauth-client-id}'; $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}';
$config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}'); $config['oauth_client_secret'] = file_get_contents('${kanidm-oauth-client-secret-fp}');
$config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
$config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token';
$config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-client-id}/userinfo'; $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo';
$config['oauth_scope'] = 'email profile openid'; # FIXME $config['oauth_scope'] = 'email profile openid'; # FIXME
$config['oauth_auth_parameters'] = []; $config['oauth_auth_parameters'] = [];
$config['oauth_identity_fields'] = ['email']; $config['oauth_identity_fields'] = ['email'];
@@ -96,11 +95,9 @@ in
# $config['oauth_pkce'] = 'S256'; # FIXME # $config['oauth_pkce'] = 'S256'; # FIXME
''; '';
systemd.services.kanidm = { systemd.services.kanidm = {
serviceConfig.ExecStartPre = lib.mkAfter [ serviceConfig.ExecStartPre = lib.mkBefore [
("-+" + kanidmExecStartPreScriptRoot) ("-+" + kanidmExecStartPreScriptRoot)
("-" + kanidmExecStartPreScript)
]; ];
requires = [ auth-passthru.oauth2-systemd-service ];
}; };
services.kanidm.provision = { services.kanidm.provision = {
groups = { groups = {
@@ -108,7 +105,7 @@ in
"sp.roundcube.users".members = "sp.roundcube.users".members =
[ "sp.roundcube.admins" auth-passthru.full-users-group ]; [ "sp.roundcube.admins" auth-passthru.full-users-group ];
}; };
systems.oauth2.roundcube = { systems.oauth2.${oauth-donor.oauth-client-id} = {
displayName = "Roundcube"; displayName = "Roundcube";
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
originLanding = "https://${cfg.subdomain}.${domain}/"; originLanding = "https://${cfg.subdomain}.${domain}/";

View File

@@ -1,14 +1,15 @@
{ config, lib, pkgs, ... }@nixos-args: { config, lib, pkgs, ... }@nixos-args:
let let
inherit (import ./common.nix nixos-args) inherit (import ./common.nix nixos-args)
appendLdapBindPwd appendSetting
auth-passthru auth-passthru
cfg cfg
domain domain
group
is-auth-enabled is-auth-enabled
; ;
runtime-directory = "dovecot2"; runtime-directory = group;
ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext"; ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext";
mkLdapSearchScope = scope: ( mkLdapSearchScope = scope: (
@@ -37,7 +38,7 @@ let
user_filter = ${config.mailserver.ldap.dovecot.userFilter} user_filter = ${config.mailserver.ldap.dovecot.userFilter}
''; '';
}; };
setPwdInLdapConfFile = appendLdapBindPwd { setPwdInLdapConfFile = appendSetting {
name = "ldap-conf-file"; name = "ldap-conf-file";
file = dovecot-ldap-config; file = dovecot-ldap-config;
prefix = ''dnpass = "''; prefix = ''dnpass = "'';
@@ -45,24 +46,39 @@ let
passwordFile = config.mailserver.ldap.bind.passwordFile; passwordFile = config.mailserver.ldap.bind.passwordFile;
destination = ldapConfFile; destination = ldapConfFile;
}; };
dovecot-oauth2-conf-file = pkgs.writeTextFile { oauth-client-id = "mailserver";
name = "dovecot-oauth2.conf.ext"; oauth-client-secret-fp =
text = '' "/run/keys/${group}/kanidm-oauth-client-secret";
oauth-secret-ExecStartPreScript = pkgs.writeShellScript
"${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
set -o xtrace
[ -f "${oauth-client-secret-fp}" ] || \
"${lib.getExe pkgs.openssl}" rand -base64 32 | tr -d "\n" > "${oauth-client-secret-fp}"
'';
dovecot-oauth2-conf-fp = "/run/${runtime-directory}/dovecot-oauth2.conf.ext";
write-dovecot-oauth2-conf = appendSetting {
name = "oauth2-conf-file";
file = builtins.toFile "dovecot-oauth2.conf.ext.template" ''
introspection_mode = post introspection_mode = post
introspection_url = ${auth-passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"}
client_id = roundcube
client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME
username_attribute = username username_attribute = username
scope = email profile openid scope = email profile openid
tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt
active_attribute = active active_attribute = active
active_value = true active_value = true
openid_configuration_url = ${auth-passthru.oauth2-discovery-url "roundcube"} openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id}
debug = "no" debug = "no"
''; '';
prefix = ''introspection_url = "'' +
(auth-passthru.oauth2-introspection-url-prefix oauth-client-id);
suffix = auth-passthru.oauth2-introspection-url-postfix + ''"'';
passwordFile = oauth-client-secret-fp;
destination = dovecot-oauth2-conf-fp;
}; };
in in
{ {
# for dovecot2 to have access to get through /run/keys directory
users.groups.keys.members = [ group ];
mailserver.ldap = { mailserver.ldap = {
# note: in `ldapsearch` first comes filter, then attributes # note: in `ldapsearch` first comes filter, then attributes
dovecot.userAttrs = "+"; # all operational attributes dovecot.userAttrs = "+"; # all operational attributes
@@ -76,7 +92,7 @@ in
passdb { passdb {
driver = oauth2 driver = oauth2
mechanisms = xoauth2 oauthbearer mechanisms = xoauth2 oauthbearer
args = ${dovecot-oauth2-conf-file} args = ${dovecot-oauth2-conf-fp}
} }
userdb { userdb {
@@ -114,13 +130,22 @@ in
services.dovecot2.enablePAM = false; services.dovecot2.enablePAM = false;
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
# TODO does it merge with existing preStart? # TODO does it merge with existing preStart?
preStart = setPwdInLdapConfFile + "\n"; preStart = setPwdInLdapConfFile + "\n" + write-dovecot-oauth2-conf + "\n";
# FIXME pass dependant services to auth module option instead? # FIXME pass dependant services to auth module option instead?
wants = [ auth-passthru.oauth2-systemd-service ]; wants = [ auth-passthru.oauth2-systemd-service ];
after = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ];
serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ]; serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ];
}; };
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
("-" + oauth-secret-ExecStartPreScript)
];
# does it merge with existing restartTriggers? # does it merge with existing restartTriggers?
systemd.services.postfix.restartTriggers = [ setPwdInLdapConfFile ]; systemd.services.postfix.restartTriggers = [
setPwdInLdapConfFile
write-dovecot-oauth2-conf
];
selfprivacy.passthru.mailserver = {
inherit oauth-client-id oauth-client-secret-fp;
};
} }

View File

@@ -1,7 +1,7 @@
{ config, lib, pkgs, ... }@nixos-args: { config, lib, pkgs, ... }@nixos-args:
let let
inherit (import ./common.nix nixos-args) inherit (import ./common.nix nixos-args)
appendLdapBindPwd appendSetting
auth-passthru auth-passthru
is-auth-enabled is-auth-enabled
; ;
@@ -29,7 +29,7 @@ let
query_filter = ${cfg.ldap.postfix.filter} query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute} result_attribute = ${cfg.ldap.postfix.mailAttribute}
''; '';
appendPwdInSenderLoginMap = appendLdapBindPwd { appendPwdInSenderLoginMap = appendSetting {
name = "ldap-sender-login-map"; name = "ldap-sender-login-map";
file = ldapSenderLoginMap; file = ldapSenderLoginMap;
prefix = "bind_pw = "; prefix = "bind_pw = ";
@@ -43,7 +43,7 @@ let
result_attribute = ${cfg.ldap.postfix.uidAttribute} result_attribute = ${cfg.ldap.postfix.uidAttribute}
''; '';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd { appendPwdInVirtualMailboxMap = appendSetting {
name = "ldap-virtual-mailbox-map"; name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap; file = ldapVirtualMailboxMap;
prefix = "bind_pw = "; prefix = "bind_pw = ";

View File

@@ -3,8 +3,9 @@ rec {
auth-passthru = config.passthru.selfprivacy.auth; auth-passthru = config.passthru.selfprivacy.auth;
domain = config.selfprivacy.domain; domain = config.selfprivacy.domain;
is-auth-enabled = config.selfprivacy.modules.auth.enable or false; is-auth-enabled = config.selfprivacy.modules.auth.enable or false;
group = "dovecot2";
appendLdapBindPwd = appendSetting =
{ name, file, prefix, suffix ? "", passwordFile, destination }: { name, file, prefix, suffix ? "", passwordFile, destination }:
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}

View File

@@ -1,10 +1,15 @@
[ [
[ "mailserver" ], [ "mailserver" ],
[ "passthru", "selfprivacy", "auth", "admins-group" ],
[ "passthru", "selfprivacy", "auth", "full-users-group" ],
[ "passthru", "selfprivacy", "auth", "ldap-base-dn" ], [ "passthru", "selfprivacy", "auth", "ldap-base-dn" ],
[ "passthru", "selfprivacy", "auth", "ldap-port" ], [ "passthru", "selfprivacy", "auth", "ldap-port" ],
[ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ], [ "passthru", "selfprivacy", "auth", "oauth2-discovery-url" ],
[ "passthru", "selfprivacy", "auth", "oauth2-introspection-url" ], [ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-postfix" ],
[ "passthru", "selfprivacy", "auth", "oauth2-introspection-url-prefix" ],
[ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ], [ "passthru", "selfprivacy", "auth", "oauth2-systemd-service" ],
[ "passthru", "selfprivacy", "roundcube", "oauth-client-id" ],
[ "passthru", "selfprivacy", "roundcube", "oauth-client-secret-fp" ],
[ "security", "acme", "certs" ], [ "security", "acme", "certs" ],
[ "selfprivacy", "domain" ], [ "selfprivacy", "domain" ],
[ "selfprivacy", "hashedMasterPassword" ], [ "selfprivacy", "hashedMasterPassword" ],

View File

@@ -5,20 +5,22 @@ let
inherit (import ./common.nix { inherit config pkgs; }) inherit (import ./common.nix { inherit config pkgs; })
auth-passthru auth-passthru
domain domain
group
is-auth-enabled is-auth-enabled
; ;
mailserver-service-account-name = "sp.mailserver.service-account"; mailserver-service-account-name = "sp.mailserver.service-account";
mailserver-service-account-token-name = "mailserver-service-account-token"; mailserver-service-account-token-name = "mailserver-service-account-token";
mailserver-service-account-token-fp = mailserver-service-account-token-fp =
"/run/keys/mailserver/kanidm-service-account-token"; # FIXME sync with auth module "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
kanidmExecStartPostScriptRoot = pkgs.writeShellScript kanidmExecStartPreScriptRoot = pkgs.writeShellScript
"mailserver-kanidm-ExecStartPost-root-script.sh" "mailserver-kanidm-ExecStartPre-root-script.sh"
'' ''
# set-group-ID bit allows for kanidm user to create files, # set-group-ID bit allows for kanidm user to create files inheriting group
mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/mailserver mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${group}
chown kanidm:kanidm /run/keys/mailserver chown kanidm:${group} /run/keys/${group}
''; '';
# create service account token, needed for LDAP
kanidmExecStartPostScript = pkgs.writeShellScript kanidmExecStartPostScript = pkgs.writeShellScript
"mailserver-kanidm-ExecStartPost-script.sh" "mailserver-kanidm-ExecStartPost-script.sh"
'' ''
@@ -173,7 +175,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
}; };
}; };
} }
# the following part is active only when "auth" module is enabled # the following parts are active only when "auth" module is enabled
(lib.attrsets.optionalAttrs (lib.attrsets.optionalAttrs
(options.selfprivacy.modules ? "auth") (options.selfprivacy.modules ? "auth")
(lib.mkIf is-auth-enabled { (lib.mkIf is-auth-enabled {
@@ -199,9 +201,11 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [
}; };
}; };
# FIXME set auth module option instead # FIXME set auth module option instead
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [
("-+" + kanidmExecStartPreScriptRoot)
];
systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [
("+" + kanidmExecStartPostScriptRoot) ("-" + kanidmExecStartPostScript)
kanidmExecStartPostScript
]; ];
})) }))
(lib.attrsets.optionalAttrs (lib.attrsets.optionalAttrs