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" ],
[ "selfprivacy", "domain" ],
[ "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;
auth-passthru = config.passthru.selfprivacy.auth;
auth-fqdn = auth-passthru.auth-fqdn;
oauth-client-id = "roundcube";
roundcube-user = "roundcube";
roundcube-group = "roundcube";
kanidmExecStartPreScriptRoot = pkgs.writeShellScript
"${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}
'';
sp-module-name = "roundcube";
user = "roundcube";
group = "roundcube";
oauth-donor = config.selfprivacy.passthru.mailserver;
kanidm-oauth-client-secret-fp =
"/run/keys/${oauth-client-id}/kanidm-oauth-client-secret";
kanidmExecStartPreScript = pkgs.writeShellScript
"${oauth-client-id}-kanidm-ExecStartPre-script.sh" ''
set -o xtrace
[ -f "${kanidm-oauth-client-secret-fp}" ] || \
"${lib.getExe pkgs.openssl}" rand -base64 -out "${kanidm-oauth-client-secret-fp}" 32
'';
"/run/keys/${group}/kanidm-oauth-client-secret";
kanidmExecStartPreScriptRoot = pkgs.writeShellScript
"${sp-module-name}-kanidm-ExecStartPre-root-script.sh"
''
# 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/${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
{
options.selfprivacy.modules.roundcube = {
@ -72,21 +69,23 @@ in
};
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
(lib.attrsets.optionalAttrs
(options.selfprivacy.modules ? "auth")
(lib.mkIf is-auth-enabled {
# 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 ''
$config['oauth_provider'] = 'generic';
$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_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2';
$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_auth_parameters'] = [];
$config['oauth_identity_fields'] = ['email'];
@ -96,11 +95,9 @@ in
# $config['oauth_pkce'] = 'S256'; # FIXME
'';
systemd.services.kanidm = {
serviceConfig.ExecStartPre = lib.mkAfter [
serviceConfig.ExecStartPre = lib.mkBefore [
("-+" + kanidmExecStartPreScriptRoot)
("-" + kanidmExecStartPreScript)
];
requires = [ auth-passthru.oauth2-systemd-service ];
};
services.kanidm.provision = {
groups = {
@ -108,7 +105,7 @@ in
"sp.roundcube.users".members =
[ "sp.roundcube.admins" auth-passthru.full-users-group ];
};
systems.oauth2.roundcube = {
systems.oauth2.${oauth-donor.oauth-client-id} = {
displayName = "Roundcube";
originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth";
originLanding = "https://${cfg.subdomain}.${domain}/";

View file

@ -1,14 +1,15 @@
{ config, lib, pkgs, ... }@nixos-args:
let
inherit (import ./common.nix nixos-args)
appendLdapBindPwd
appendSetting
auth-passthru
cfg
domain
group
is-auth-enabled
;
runtime-directory = "dovecot2";
runtime-directory = group;
ldapConfFile = "/run/${runtime-directory}/dovecot-ldap.conf.ext";
mkLdapSearchScope = scope: (
@ -37,7 +38,7 @@ let
user_filter = ${config.mailserver.ldap.dovecot.userFilter}
'';
};
setPwdInLdapConfFile = appendLdapBindPwd {
setPwdInLdapConfFile = appendSetting {
name = "ldap-conf-file";
file = dovecot-ldap-config;
prefix = ''dnpass = "'';
@ -45,24 +46,39 @@ let
passwordFile = config.mailserver.ldap.bind.passwordFile;
destination = ldapConfFile;
};
dovecot-oauth2-conf-file = pkgs.writeTextFile {
name = "dovecot-oauth2.conf.ext";
text = ''
oauth-client-id = "mailserver";
oauth-client-secret-fp =
"/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_url = ${auth-passthru.oauth2-introspection-url "roundcube" "VERYSTRONGSECRETFORROUNDCUBE"}
client_id = roundcube
client_secret = VERYSTRONGSECRETFORROUNDCUBE # FIXME
username_attribute = username
scope = email profile openid
tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt
active_attribute = active
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"
'';
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
{
# for dovecot2 to have access to get through /run/keys directory
users.groups.keys.members = [ group ];
mailserver.ldap = {
# note: in `ldapsearch` first comes filter, then attributes
dovecot.userAttrs = "+"; # all operational attributes
@ -76,7 +92,7 @@ in
passdb {
driver = oauth2
mechanisms = xoauth2 oauthbearer
args = ${dovecot-oauth2-conf-file}
args = ${dovecot-oauth2-conf-fp}
}
userdb {
@ -114,13 +130,22 @@ in
services.dovecot2.enablePAM = false;
systemd.services.dovecot2 = {
# 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?
wants = [ auth-passthru.oauth2-systemd-service ];
after = [ auth-passthru.oauth2-systemd-service ];
serviceConfig.RuntimeDirectory = lib.mkForce [ runtime-directory ];
};
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [
("-" + oauth-secret-ExecStartPreScript)
];
# 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:
let
inherit (import ./common.nix nixos-args)
appendLdapBindPwd
appendSetting
auth-passthru
is-auth-enabled
;
@ -29,7 +29,7 @@ let
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute}
'';
appendPwdInSenderLoginMap = appendLdapBindPwd {
appendPwdInSenderLoginMap = appendSetting {
name = "ldap-sender-login-map";
file = ldapSenderLoginMap;
prefix = "bind_pw = ";
@ -43,7 +43,7 @@ let
result_attribute = ${cfg.ldap.postfix.uidAttribute}
'';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
appendPwdInVirtualMailboxMap = appendSetting {
name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap;
prefix = "bind_pw = ";

View file

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

View file

@ -1,10 +1,15 @@
[
[ "mailserver" ],
[ "passthru", "selfprivacy", "auth", "admins-group" ],
[ "passthru", "selfprivacy", "auth", "full-users-group" ],
[ "passthru", "selfprivacy", "auth", "ldap-base-dn" ],
[ "passthru", "selfprivacy", "auth", "ldap-port" ],
[ "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", "roundcube", "oauth-client-id" ],
[ "passthru", "selfprivacy", "roundcube", "oauth-client-secret-fp" ],
[ "security", "acme", "certs" ],
[ "selfprivacy", "domain" ],
[ "selfprivacy", "hashedMasterPassword" ],

View file

@ -5,20 +5,22 @@ let
inherit (import ./common.nix { inherit config pkgs; })
auth-passthru
domain
group
is-auth-enabled
;
mailserver-service-account-name = "sp.mailserver.service-account";
mailserver-service-account-token-name = "mailserver-service-account-token";
mailserver-service-account-token-fp =
"/run/keys/mailserver/kanidm-service-account-token"; # FIXME sync with auth module
kanidmExecStartPostScriptRoot = pkgs.writeShellScript
"mailserver-kanidm-ExecStartPost-root-script.sh"
"/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module
kanidmExecStartPreScriptRoot = pkgs.writeShellScript
"mailserver-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/mailserver
chown kanidm:kanidm /run/keys/mailserver
# 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/${group}
chown kanidm:${group} /run/keys/${group}
'';
# create service account token, needed for LDAP
kanidmExecStartPostScript = pkgs.writeShellScript
"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
(options.selfprivacy.modules ? "auth")
(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
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [
("-+" + kanidmExecStartPreScriptRoot)
];
systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [
("+" + kanidmExecStartPostScriptRoot)
kanidmExecStartPostScript
("-" + kanidmExecStartPostScript)
];
}))
(lib.attrsets.optionalAttrs