refactor: switch to upstream nixos kanidm module

This commit is contained in:
nhnn
2025-09-02 12:16:12 +03:00
committed by Inex Code
parent 169d1ca8df
commit 73cbdf994e
8 changed files with 189 additions and 1199 deletions

View File

@@ -11,105 +11,68 @@ let
; ;
auth-passthru = config.selfprivacy.passthru.auth; auth-passthru = config.selfprivacy.passthru.auth;
keys-path = auth-passthru.keys-path; keys-path = auth-passthru.keys-path;
# generate OAuth2 client secret
mkKanidmExecStartPreScript = mkKanidmTokenCreationSnippet =
oauthClientID: linuxGroup: {
clientID,
linuxGroupOfClient,
isMailserver,
...
}:
let let
secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup; kanidmServiceAccountName = "sp.${clientID}.service-account";
kanidmServiceAccountTokenName = "${clientID}-service-account-token";
kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroupOfClient;
isRW = clientID == "selfprivacy-api";
in in
pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' ''
set -o pipefail # try to get existing Kanidm service account
set -o errexit KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidmServiceAccountName}$")"
if ! [ -f "${secretFP}" ] echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
then then
"${lib.getExe pkgs.openssl}" rand -base64 32 \ echo "kanidm service account \"${kanidmServiceAccountName}\" is found"
| tr "\n:@/+=" "012345" > "${secretFP}" else
chmod 640 "${secretFP}" echo "kanidm service account \"${kanidmServiceAccountName}\" is not found"
echo "creating new kanidm service account \"${kanidmServiceAccountName}\""
if $KANIDM service-account create --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountName}" idm_admin
then
echo "kanidm service account \"${kanidmServiceAccountName}\" created"
else
echo "error: cannot create kanidm service account \"${kanidmServiceAccountName}\""
exit 1
fi
fi fi
# create a new token for kanidm
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" ${lib.strings.optionalString isRW "--rw"} --output json)"
then
echo "error: kanidm CLI returns an error when trying to generate service-account api-token"
exit 1
fi
if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)"
then
echo "error: cannot get service-account API token from JSON"
exit 1
fi
if ! install --mode=640 \
<(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \
${kanidmServiceAccountTokenFP}
then
echo "error: cannot write token to \"${kanidmServiceAccountTokenFP}\""
exit 1
fi
''
+ lib.strings.optionalString isMailserver ''
# add Kanidm service account to `idm_mail_servers` group
$KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}"
''
+ lib.strings.optionalString isRW ''
$KANIDM group add-members idm_admins "${kanidmServiceAccountName}"
''; '';
mkKanidmExecStartPostScript =
oauthClientID: linuxGroup: isMailserver:
let
kanidmServiceAccountName = "sp.${oauthClientID}.service-account";
kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token";
kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroup;
isRW = oauthClientID == "selfprivacy-api";
# TODO: Copied from Forgejo module. Maybe generalize as lib. function?
waitForURL = url: maxRetries: delaySec: ''
for ((i=1; i<=${toString maxRetries}; i++))
do
if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null
then
echo "${url} responds to GET HTTP request (attempt #$i)"
break
else
echo "${url} does not respond to GET HTTP request (attempt #$i)"
echo sleeping for ${toString delaySec} seconds
fi
sleep ${toString delaySec}
done
if [[ "$i" -gt "${toString maxRetries}" ]]
then
echo "error, max attempts to access "${url}" have been used unsuccessfully!"
exit 124
fi
'';
in
pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" (
''
export HOME=$RUNTIME_DIRECTORY/client_home
readonly KANIDM="${config.services.kanidm.package}/bin/kanidm"
${waitForURL config.services.kanidm.serverSettings.origin 10 10}
# try to get existing Kanidm service account
KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${kanidmServiceAccountName}$")"
echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
then
echo "kanidm service account \"${kanidmServiceAccountName}\" is found"
else
echo "kanidm service account \"${kanidmServiceAccountName}\" is not found"
echo "creating new kanidm service account \"${kanidmServiceAccountName}\""
if $KANIDM service-account create --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountName}" idm_admin
then
echo "kanidm service account \"${kanidmServiceAccountName}\" created"
else
echo "error: cannot create kanidm service account \"${kanidmServiceAccountName}\""
exit 1
fi
fi
# create a new token for kanidm
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin "${kanidmServiceAccountName}" "${kanidmServiceAccountTokenName}" ${lib.strings.optionalString isRW "--rw"} --output json)"
then
echo "error: kanidm CLI returns an error when trying to generate service-account api-token"
exit 1
fi
if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)"
then
echo "error: cannot get service-account API token from JSON"
exit 1
fi
if ! install --mode=640 \
<(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \
${kanidmServiceAccountTokenFP}
then
echo "error: cannot write token to \"${kanidmServiceAccountTokenFP}\""
exit 1
fi
''
+ lib.strings.optionalString isMailserver ''
# add Kanidm service account to `idm_mail_servers` group
$KANIDM group add-members idm_mail_servers "${kanidmServiceAccountName}"
''
+ lib.strings.optionalString isRW ''
$KANIDM group add-members idm_admins "${kanidmServiceAccountName}"
''
);
in in
{ {
options.selfprivacy.auth = { options.selfprivacy.auth = {
@@ -300,28 +263,64 @@ in
# for each OAuth2 client: scripts with Kanidm CLI commands # for each OAuth2 client: scripts with Kanidm CLI commands
systemd.services.kanidm = { systemd.services.kanidm = {
before = lib.lists.concatMap ({ clientSystemdUnits, ... }: clientSystemdUnits) clientsAttrsList; before = lib.lists.concatMap ({ clientSystemdUnits, ... }: clientSystemdUnits) clientsAttrsList;
serviceConfig = lib.mkMerge ( serviceConfig = {
lib.forEach clientsAttrsList ( ExecStartPre = [
{ (pkgs.writeShellScript "create-kanidm-client-secrets" ''
clientID, set -euo pipefail
isTokenNeeded, ${lib.concatLines (
linuxGroupOfClient, map (
isMailserver, { linuxGroupOfClient, ... }:
... let
}: secretPath = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfClient;
{ in
ExecStartPre = [ ''
# "-" prefix means to ignore exit code of prefixed script if ! [ -f "${secretPath}" ]
("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) then
]; "${lib.getExe pkgs.openssl}" rand -base64 32 \
ExecStartPost = lib.mkIf isTokenNeeded ( | tr "\n:@/+=" "012345" > "${secretPath}"
lib.mkAfter [ chmod 640 "${secretPath}"
("-" + mkKanidmExecStartPostScript clientID linuxGroupOfClient isMailserver) fi
] ''
); ) clientsAttrsList
} )}
) '')
); ];
ExecStartPost = lib.mkOrder 1300 [
(pkgs.writeShellScript "create-kanidm-tokens" ''
set -euo pipefail
readonly KANIDM="${config.services.kanidm.package}/bin/kanidm"
readonly CLIENT_HOME=$RUNTIME_DIRECTORY/client_home
mkdir -p "$CLIENT_HOME"
export HOME="$CLIENT_HOME"
export KANIDM_NAME=idm_admin
export KANIDM_URL="${config.services.kanidm.provision.instanceUrl}"
export KANIDM_SKIP_HOSTNAME_VERIFICATION="true"
if ! recover_out=$(${config.services.kanidm.package}/bin/kanidmd recover-account -c ${
config.environment.etc."kanidm/server.toml".source
} idm_admin -o json); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to recover admin account" >&2
exit 1
fi
if ! KANIDM_IDM_ADMIN_PASSWORD=$(grep '{"password' <<< "$recover_out" | ${lib.getExe pkgs.jq} -r .password); then
echo "$recover_out" >&2
echo "kanidm provision: Failed to parse password for idm_admin account" >&2
exit 1
fi
KANIDM_PASSWORD="$KANIDM_IDM_ADMIN_PASSWORD" $KANIDM login
# disable anonymous account because it allows to freely iterate over all users on kanidm instance.
$KANIDM service-account validity expire-at anonymous epoch
${lib.concatLines (
lib.map mkKanidmTokenCreationSnippet (lib.filter (x: x.isTokenNeeded) clientsAttrsList)
)}
'')
];
};
}; };
# for each OAuth2 client: Kanidm provisioning options # for each OAuth2 client: Kanidm provisioning options
@@ -344,13 +343,23 @@ in
... ...
}: }:
{ {
groups = lib.mkIf (clientID != "selfprivacy-api") ({ groups = lib.mkIf (clientID != "selfprivacy-api") (
"${usersGroup}".members = [ {
auth-passthru.full-users-group "${usersGroup}" = {
] ++ lib.optional adminsGroupDefined adminsGroup; members = [
} // lib.optionalAttrs adminsGroupDefined { auth-passthru.full-users-group
"${adminsGroup}".members = [ auth-passthru.admins-group ]; ]
}); ++ lib.optional adminsGroupDefined adminsGroup;
overwriteMembers = false; # allow our api to modify group imperatively
};
}
// lib.optionalAttrs adminsGroupDefined {
"${adminsGroup}" = {
members = [ auth-passthru.admins-group ];
overwriteMembers = false;
};
}
);
systems.oauth2.${clientID} = { systems.oauth2.${clientID} = {
inherit inherit
basicSecretFile basicSecretFile

View File

@@ -89,8 +89,21 @@ lib.mkIf config.selfprivacy.sso.enable {
provision = { provision = {
enable = true; enable = true;
autoRemove = true; # if false, obsolete oauth2 scopeMaps remain autoRemove = true; # if false, obsolete oauth2 scopeMaps remain
groups.${admins-group}.present = true; groups.${admins-group} = {
groups.${full-users-group}.present = true; present = true;
overwriteMembers = false;
};
groups.${full-users-group} = {
present = true;
members = [
admins-group # admins are full users too.
];
overwriteMembers = false;
};
groups.idm_all_persons = {
present = true;
overwriteMembers = false;
};
}; };
enableClient = true; enableClient = true;
clientSettings = { clientSettings = {
@@ -156,11 +169,29 @@ lib.mkIf config.selfprivacy.sso.enable {
}; };
}; };
systemd.services.kanidm.serviceConfig.ExecStartPre = systemd.services.kanidm.serviceConfig = {
# idempotent script to run on each startup only for kanidm v1.5.0 BindPaths = [
lib.mkIf (lib.versionAtLeast config.services.kanidm.package.version "1.5.0") ( keys-path
lib.mkBefore [ kanidmMigrateDbScript ] ];
); # mkForce is used there to overwrite paths to secrets provisioning will use because those are created in ExecStartPre and systemd sandbox breaks.
BindReadOnlyPaths = lib.mkForce [
"/nix/store"
"/run/systemd/notify" # For healthcheck notifications
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
"-/etc/passwd"
"-/etc/group"
config.services.kanidm.serverSettings.tls_chain
config.services.kanidm.serverSettings.tls_key
];
ExecStartPre =
# idempotent script to run on each startup only for kanidm v1.5.0
lib.mkIf (lib.versionAtLeast config.services.kanidm.package.version "1.5.0") (
lib.mkBefore [ kanidmMigrateDbScript ]
);
};
selfprivacy.passthru.auth = { selfprivacy.passthru.auth = {
inherit inherit

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ in
{ {
imports = [ imports = [
./selfprivacy-module.nix ./selfprivacy-module.nix
./auth/auth.nix
./auth/auth-module.nix ./auth/auth-module.nix
./volumes.nix ./volumes.nix
./users.nix ./users.nix

View File

@@ -31,11 +31,6 @@
hardware-configuration hardware-configuration
deployment deployment
./configuration.nix ./configuration.nix
./auth/auth.nix
{
disabledModules = [ "services/security/kanidm.nix" ];
imports = [ ./auth/kanidm.nix ];
}
selfprivacy-api.nixosModules.default selfprivacy-api.nixosModules.default
( (
{ pkgs, lib, ... }: { pkgs, lib, ... }:

View File

@@ -22,10 +22,6 @@ let
oauth-donor = config.selfprivacy.passthru.mailserver; oauth-donor = config.selfprivacy.passthru.mailserver;
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
# copy client secret from mailserver
kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" ''
install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
'';
in in
{ {
options.selfprivacy.modules.roundcube = { options.selfprivacy.modules.roundcube = {
@@ -121,9 +117,16 @@ in
after = [ "dovecot2.service" ]; after = [ "dovecot2.service" ];
requires = [ "dovecot2.service" ]; requires = [ "dovecot2.service" ];
}; };
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ systemd.services.kanidm.serviceConfig = {
("-+" + kanidmExecStartPreScriptRoot) ExecStartPre = lib.mkAfter [
]; (pkgs.writeShellScript "copy-mailserver-client-secret-to-roundcube" ''
install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP}
'')
];
SystemCallFilter = [
"@chown"
];
};
selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = { selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = {
inherit adminsGroup usersGroup; inherit adminsGroup usersGroup;

View File

@@ -22,10 +22,12 @@ let
runtime-folder = group; runtime-folder = group;
keysPath = auth-passthru.keys-path; keysPath = auth-passthru.keys-path;
# create service account token, needed for LDAP kanidmExecStartPostScript = pkgs.writeShellScript "create-dovecot-service-account-token-for-ldap" ''
kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" ''
export HOME=$RUNTIME_DIRECTORY/client_home export HOME=$RUNTIME_DIRECTORY/client_home
readonly KANIDM="${config.services.kanidm.package}/bin/kanidm" readonly KANIDM="${config.services.kanidm.package}/bin/kanidm"
export KANIDM_NAME=idm_admin
export KANIDM_URL="${config.services.kanidm.provision.instanceUrl}"
export KANIDM_SKIP_HOSTNAME_VERIFICATION="true"
# get Kanidm service account for mailserver # get Kanidm service account for mailserver
KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")"
@@ -111,7 +113,7 @@ let
}; };
oauth-client-id = "mailserver"; oauth-client-id = "mailserver";
oauth-client-secret-fp = "${keysPath}/${group}/kanidm-oauth-client-secret"; oauth-client-secret-fp = "${keysPath}/${group}/kanidm-oauth-client-secret";
oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-create-client-secret.sh" ''
set -o xtrace set -o xtrace
[ -f "${oauth-client-secret-fp}" ] || \ [ -f "${oauth-client-secret-fp}" ] || \
"${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}" "${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}"
@@ -202,10 +204,10 @@ in
}; };
systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [ systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkBefore [
("-" + oauth-secret-ExecStartPreScript) oauth-secret-ExecStartPreScript
]; ];
systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [ systemd.services.kanidm.serviceConfig.ExecStartPost = lib.mkAfter [
("-" + kanidmExecStartPostScript) kanidmExecStartPostScript
]; ];
systemd.services.postfix.restartTriggers = [ systemd.services.postfix.restartTriggers = [

View File

@@ -25,5 +25,6 @@
[ "services", "postfix", "user" ], [ "services", "postfix", "user" ],
[ "services", "redis", "servers", "rspamd" ], [ "services", "redis", "servers", "rspamd" ],
[ "services", "rspamd" ], [ "services", "rspamd" ],
[ "services", "kanidm", "package" ] [ "services", "kanidm", "package" ],
[ "services", "kanidm", "provision", "instanceUrl" ]
] ]