feat(kanidm): configure OIDC client integrations

This commit is contained in:
Philipp 2026-09-08 01:15:25 +02:00
commit d51f8702c4
Signed by: Philipp
GPG key ID: B27C3DE2FD94AFC3
11 changed files with 473 additions and 0 deletions

View file

@ -0,0 +1 @@
<svg fill="#FB923C" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Forgejo</title><path d="M16.7773 0c1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.1175a7.0759 7.0759 0 0 1 4.148-1.4205l.1176-.001 1.3385.0002c.4973-.8827 1.4434-1.4788 2.5288-1.4788 1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.319c.8827.4973 1.4788 1.4434 1.4788 2.5287 0 1.602-1.2986 2.9005-2.9005 2.9005-1.6018 0-2.9004-1.2986-2.9004-2.9005 0-1.0853.596-2.0314 1.4788-2.5287l-.0002-9.9831c0-3.887 3.1195-7.0453 6.9915-7.108l.1176-.001h1.3385C14.7458.5962 15.692 0 16.7773 0ZM7.2227 19.9052c-.6596 0-1.1943.5347-1.1943 1.1943s.5347 1.1943 1.1943 1.1943 1.1944-.5347 1.1944-1.1943-.5348-1.1943-1.1944-1.1943Zm9.5546-10.4644c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Zm0-7.7346c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -12,6 +12,8 @@ let
codebergThemes = pkgs.${namespace}.codeberg-themes;
in
{
imports = [ ./kanidm.nix ];
options.${namespace}.services.forgejo = {
enable = mkEnableOption "Forgejo";
nginx = {

View file

@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
namespace,
...
}:
with lib;
let
cfg = config.${namespace}.services.forgejo;
kanidmCfg = config.${namespace}.services.kanidm;
secret = config.sops.secrets."kanidm/oauth2/forgejo/basic-secret";
forgejo = "${cfg.package}/bin/forgejo";
forgejoArgs = [
"--config"
"/var/lib/forgejo/custom/conf/app.ini"
"--work-path"
"/var/lib/forgejo"
"--custom-path"
"/var/lib/forgejo/custom"
];
forgejoArgsString = concatStringsSep " " (map escapeShellArg forgejoArgs);
in
{
config = mkIf (cfg.enable && kanidmCfg.enable) {
sops.secrets."kanidm/oauth2/forgejo/basic-secret" = {
format = "yaml";
key = "kanidm/oauth2/forgejo/basic-secret";
sopsFile = kanidmCfg.secretsFile;
owner = "kanidm";
group = cfg.user;
mode = "0440";
};
services.kanidm.provision = {
groups.forgejo_users = {
members = [ kanidmCfg.adminUser.name ];
overwriteMembers = false;
};
systems.oauth2.forgejo = {
displayName = "Forgejo";
originUrl = "https://${cfg.domain}/user/oauth2/kanidm/callback";
originLanding = "https://${cfg.domain}/";
imageFile = ./assets/forgejo.svg;
basicSecretFile = secret.path;
enableLegacyCrypto = true;
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
scopeMaps.forgejo_users = [
"openid"
"profile"
"email"
];
};
};
systemd.services.forgejo-kanidm-auth-source = {
description = "Forgejo Kanidm OIDC auth source provisioning";
wantedBy = [ "multi-user.target" ];
after = [
"forgejo.service"
"kanidm.service"
];
requires = [ "forgejo.service" ];
partOf = [ "forgejo.service" ];
path = [
pkgs.gawk
pkgs.gnugrep
];
serviceConfig = {
Type = "oneshot";
User = cfg.user;
Group = cfg.user;
};
script = ''
set -euo pipefail
secret=$(cat ${escapeShellArg secret.path})
common_args=(
--name kanidm
--provider openidConnect
--key forgejo
--secret "$secret"
--auto-discover-url https://${kanidmCfg.domain}/oauth2/openid/forgejo/.well-known/openid-configuration
--icon-url https://${cfg.domain}/assets/img/logo.svg
--scopes openid
--scopes profile
--scopes email
--allow-username-change
)
auth_id=$(${forgejo} admin auth list ${forgejoArgsString} | awk -F '\t' '$2 == "kanidm" { print $1; exit }')
if [ -n "$auth_id" ]; then
${forgejo} admin auth update-oauth ${forgejoArgsString} --id "$auth_id" "''${common_args[@]}"
else
${forgejo} admin auth add-oauth ${forgejoArgsString} "''${common_args[@]}"
fi
'';
};
};
}

View file

@ -0,0 +1 @@
<svg fill="#18BCF2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Home Assistant</title><path d="M22.939 10.627 13.061.749a1.505 1.505 0 0 0-2.121 0l-9.879 9.878C.478 11.21 0 12.363 0 13.187v9c0 .826.675 1.5 1.5 1.5h9.227l-4.063-4.062a2.034 2.034 0 0 1-.664.113c-1.13 0-2.05-.92-2.05-2.05s.92-2.05 2.05-2.05 2.05.92 2.05 2.05c0 .233-.041.456-.113.665l3.163 3.163V9.928a2.05 2.05 0 0 1-1.15-1.84c0-1.13.92-2.05 2.05-2.05s2.05.92 2.05 2.05a2.05 2.05 0 0 1-1.15 1.84v8.127l3.146-3.146A2.051 2.051 0 0 1 18 12.239c1.13 0 2.05.92 2.05 2.05s-.92 2.05-2.05 2.05c-.25 0-.488-.047-.709-.13L12.9 20.602v3.088h9.6c.825 0 1.5-.675 1.5-1.5v-9c0-.825-.477-1.977-1.061-2.561z"/></svg>

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1 @@
<svg fill="#4250AF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Immich</title><path d="M11.9863.2695c-2.409 0-5.207 1.091-5.207 3.8946v.1523c1.3428.597 2.9347 1.6629 4.4121 2.9707 1.5713 1.3912 2.8374 2.8821 3.6524 4.2871 1.3997-2.5034 2.3358-5.4784 2.3476-7.373V4.164c0-2.8035-2.796-3.8946-5.205-3.8946m7.5117 4.4903c-.3778-.0081-.7747.0502-1.1914.1855-.0366.0118-.086.0278-.1445.0469-.1525 1.4611-.6756 3.304-1.4629 5.1133-.8373 1.9243-1.8627 3.5898-2.9472 4.7988 2.8132.558 5.9307.5273 7.7363-.0469.0126-.004.0246-.0065.0351-.0097 2.6665-.8666 2.84-3.8636 2.0957-6.1543-.6279-1.9332-2.081-3.89-4.121-3.9336m-14.996.039C2.4618 4.8424 1.0088 6.7973.3809 8.7305c-.7442 2.291-.5708 5.288 2.0957 6.1543l.1445.0468c.982-1.0926 2.4873-2.2761 4.1875-3.2773 1.8088-1.0646 3.619-1.808 5.207-2.1484-1.9483-2.1049-4.4884-3.9132-6.287-4.5098l-.0352-.0117c-.4167-.1354-.8136-.1936-1.1914-.1856m4.6718 6.7578c-2.6038 1.2025-5.1088 3.0598-6.2324 4.586l-.0215.0293c-1.6478 2.2683-.0272 4.7953 1.9219 6.211 1.9487 1.4159 4.8518 2.1765 6.5-.0919.0228-.0309.0536-.071.0898-.121-.7356-1.2717-1.396-3.0718-1.8222-4.9981-.4534-2.0492-.6023-4-.4356-5.6153m1.0723 3.338c.3387 2.8478 1.3315 5.8037 2.4355 7.3437l.0215.0293c1.6478 2.2683 4.551 1.5078 6.5.0918 1.9487-1.416 3.5697-3.943 1.9219-6.211-.0228-.0309-.0517-.073-.0879-.123-1.4367.3066-3.3522.3794-5.3164.1894-2.089-.2017-3.9895-.6623-5.4746-1.3203"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg fill="#E57000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Proxmox</title><path d="M4.928 1.825c-1.09.553-1.09.64-.07 1.78 5.655 6.295 7.004 7.782 7.107 7.782.139.017 7.971-8.542 8.058-8.801.034-.07-.208-.312-.519-.536-.415-.312-.864-.433-1.712-.467-1.59-.104-2.144.242-4.115 2.455-.899 1.003-1.66 1.833-1.66 1.833-.017 0-.76-.813-1.642-1.798S8.473 2.1 8.127 1.91c-.796-.45-2.421-.484-3.2-.086zM1.297 4.367C.45 4.695 0 5.007 0 5.248c0 .121 1.331 1.678 2.94 3.459 1.625 1.78 2.939 3.268 2.939 3.302 0 .035-1.331 1.522-2.94 3.303C1.314 17.11.017 18.683.035 18.822c.086.467 1.504 1.055 2.541 1.055 1.678-.018 2.058-.312 5.603-4.202 1.78-1.954 3.233-3.614 3.233-3.666 0-.069-1.435-1.694-3.199-3.63-2.3-2.508-3.423-3.632-3.96-3.874-.812-.398-2.126-.467-2.956-.138zm18.467.12c-.502.26-1.764 1.505-3.943 3.891-1.763 1.937-3.199 3.562-3.199 3.631 0 .07 1.453 1.712 3.234 3.666 3.544 3.89 3.925 4.184 5.602 4.202 1.038 0 2.455-.588 2.542-1.055.017-.156-1.28-1.712-2.905-3.493-1.608-1.78-2.94-3.285-2.94-3.32 0-.034 1.332-1.539 2.94-3.32C22.72 6.91 24.017 5.352 24 5.214c-.087-.45-1.366-.968-2.473-1.038-.795-.034-1.21.035-1.763.312zM7.954 16.973c-2.144 2.369-3.908 4.374-3.943 4.46-.034.07.208.312.52.537.414.311.864.432 1.711.467 1.574.103 2.161-.26 4.15-2.508.864-.968 1.608-1.78 1.625-1.78s.761.812 1.643 1.798c2.023 2.248 2.559 2.576 4.132 2.49.848-.035 1.297-.156 1.712-.467.311-.225.553-.467.519-.536-.087-.26-7.92-8.819-8.058-8.801-.069 0-1.867 1.954-4.011 4.34z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,193 @@
{
config,
lib,
pkgs,
namespace,
...
}:
with lib;
with lib.${namespace};
let
cfg = config.${namespace}.services.kanidm;
sopsCfg = config.${namespace}.system.sops;
in
{
imports = [
./home-assistant.nix
./immich.nix
./proxmox.nix
];
options.${namespace}.services.kanidm = {
enable = mkEnableOption "Kanidm identity provider";
package = mkOption {
description = "The Kanidm package to use.";
type = types.package;
default = pkgs.kanidm_1_11.withSecretProvisioning;
};
domain = mkOption {
description = "The public domain to serve Kanidm on.";
type = types.str;
default = "id.stahl.sh";
};
managedDomain = mkOption {
description = "The identity domain managed by Kanidm.";
type = types.str;
default = "stahl.sh";
};
bindAddress = mkOption {
description = "Local address and port Kanidm listens on.";
type = types.str;
default = "127.0.0.1:8443";
};
acmeHost = mkOption {
description = "ACME certificate name used for Kanidm TLS.";
type = types.str;
default = "stahl.sh";
};
adminUser.name = mkOption {
description = "Kanidm administrator account name.";
type = types.str;
default = "philipp";
};
secretsFile = mkOption {
description = "SOPS file containing Kanidm provisioning secrets.";
type = types.path;
default = sopsCfg.secretsDir + "/blarm-kanidm.yaml";
};
backup = {
versions = mkOption {
description = "Number of online Kanidm backups to keep.";
type = types.ints.unsigned;
default = 7;
};
schedule = mkOption {
description = "Cron schedule for Kanidm online backups.";
type = types.str;
default = "30 22 * * *";
};
};
nginx = {
enable = mkEnabledOption "Enable nginx reverse proxy for Kanidm.";
};
};
config = mkIf cfg.enable {
${namespace} = {
system.sops = enabled;
services.acme.enable = true;
};
sops.secrets = {
"kanidm/idm-admin-password" = {
format = "yaml";
key = "kanidm/idm-admin-password";
sopsFile = cfg.secretsFile;
owner = "kanidm";
group = "kanidm";
mode = "0400";
};
"kanidm/admin-user/display-name" = {
format = "yaml";
key = "kanidm/admin-user/display-name";
sopsFile = cfg.secretsFile;
};
"kanidm/admin-user/legal-name" = {
format = "yaml";
key = "kanidm/admin-user/legal-name";
sopsFile = cfg.secretsFile;
};
"kanidm/admin-user/mail-address" = {
format = "yaml";
key = "kanidm/admin-user/mail-address";
sopsFile = cfg.secretsFile;
};
};
sops.templates."kanidm-provision-extra.json" = {
owner = "kanidm";
group = "kanidm";
mode = "0400";
restartUnits = [ "kanidm.service" ];
content = ''
{
"persons": {
"${cfg.adminUser.name}": {
"displayName": "${config.sops.placeholder."kanidm/admin-user/display-name"}",
"legalName": "${config.sops.placeholder."kanidm/admin-user/legal-name"}",
"mailAddresses": [
"${config.sops.placeholder."kanidm/admin-user/mail-address"}"
]
}
}
}
'';
};
networking.firewall.allowedTCPPorts = mkIf cfg.nginx.enable [
80
443
];
services.kanidm = {
package = cfg.package;
client.settings.uri = "https://${cfg.domain}";
provision = {
enable = true;
instanceUrl = "https://localhost:8443";
idmAdminPasswordFile = config.sops.secrets."kanidm/idm-admin-password".path;
extraJsonFile = config.sops.templates."kanidm-provision-extra.json".path;
groups.idm_admins = {
members = [ cfg.adminUser.name ];
overwriteMembers = false;
};
persons.${cfg.adminUser.name} = {
displayName = cfg.adminUser.name;
groups = [ "idm_admins" ];
};
};
server = {
enable = true;
settings = {
bindaddress = cfg.bindAddress;
origin = "https://${cfg.domain}";
domain = cfg.managedDomain;
tls_chain = "/var/lib/acme/${cfg.acmeHost}/fullchain.pem";
tls_key = "/var/lib/acme/${cfg.acmeHost}/key.pem";
online_backup = {
inherit (cfg.backup) schedule versions;
path = "/var/lib/kanidm/backups";
};
};
};
};
users.users.kanidm.extraGroups = [ "acme" ];
services.nginx = mkIf cfg.nginx.enable {
enable = true;
virtualHosts."${cfg.domain}" = mkNginxProxyHost {
proxyPass = "https://${cfg.bindAddress}";
location = {
proxyWebsockets = true;
recommendedProxySettings = true;
extraConfig = ''
proxy_ssl_name ${cfg.domain};
proxy_ssl_server_name on;
proxy_ssl_verify off;
'';
};
};
};
};
}

View file

@ -0,0 +1,44 @@
{
config,
lib,
namespace,
...
}:
with lib;
let
cfg = config.${namespace}.services.kanidm;
in
{
config = mkIf cfg.enable {
sops.secrets."kanidm/oauth2/home-assistant/basic-secret" = {
format = "yaml";
key = "kanidm/oauth2/home-assistant/basic-secret";
sopsFile = cfg.secretsFile;
owner = "kanidm";
group = "kanidm";
mode = "0400";
};
services.kanidm.provision = {
groups.home_assistant_users = {
members = [ cfg.adminUser.name ];
overwriteMembers = false;
};
systems.oauth2.home_assistant = {
displayName = "Home Assistant";
originUrl = "https://ha.monapona.de/auth/openid/callback";
originLanding = "https://ha.monapona.de/";
imageFile = ./assets/home-assistant.svg;
basicSecretFile = config.sops.secrets."kanidm/oauth2/home-assistant/basic-secret".path;
enableLegacyCrypto = true;
preferShortUsername = true;
scopeMaps.home_assistant_users = [
"openid"
"profile"
"email"
];
};
};
};
}

View file

@ -0,0 +1,47 @@
{
config,
lib,
namespace,
...
}:
with lib;
let
cfg = config.${namespace}.services.kanidm;
in
{
config = mkIf cfg.enable {
sops.secrets."kanidm/oauth2/immich/basic-secret" = {
format = "yaml";
key = "kanidm/oauth2/immich/basic-secret";
sopsFile = cfg.secretsFile;
owner = "kanidm";
group = "kanidm";
mode = "0400";
};
services.kanidm.provision = {
groups.immich_users = {
members = [ cfg.adminUser.name ];
overwriteMembers = false;
};
systems.oauth2.immich = {
displayName = "Immich";
originUrl = [
"https://im.monapona.de/auth/login"
"app.immich:/oauth-callback"
];
originLanding = "https://im.monapona.de/";
imageFile = ./assets/immich.svg;
basicSecretFile = config.sops.secrets."kanidm/oauth2/immich/basic-secret".path;
enableLegacyCrypto = true;
preferShortUsername = true;
scopeMaps.immich_users = [
"openid"
"profile"
"email"
];
};
};
};
}

View file

@ -0,0 +1,47 @@
{
config,
lib,
namespace,
...
}:
with lib;
let
cfg = config.${namespace}.services.kanidm;
in
{
config = mkIf cfg.enable {
sops.secrets."kanidm/oauth2/proxmox/basic-secret" = {
format = "yaml";
key = "kanidm/oauth2/proxmox/basic-secret";
sopsFile = cfg.secretsFile;
owner = "kanidm";
group = "kanidm";
mode = "0400";
};
services.kanidm.provision = {
groups.proxmox_users = {
members = [ cfg.adminUser.name ];
overwriteMembers = false;
};
systems.oauth2.proxmox = {
displayName = "Proxmox";
originUrl = [
"https://pve.monapona.de"
];
originLanding = "https://pve.monapona.de/";
imageFile = ./assets/proxmox.svg;
basicSecretFile = config.sops.secrets."kanidm/oauth2/proxmox/basic-secret".path;
enableLegacyCrypto = true;
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
scopeMaps.proxmox_users = [
"openid"
"profile"
"email"
];
};
};
};
}

30
secrets/blarm-kanidm.yaml Normal file
View file

@ -0,0 +1,30 @@
kanidm:
idm-admin-password: ENC[AES256_GCM,data:flNsozpi1/wlihELzkchltEVqOt1eBbO33GHmuM3fPfqo2AlqtwzCiWmeO4=,iv:ERyGZ/+CtkTQScdd+qRg/ixzmEuOy8vcZ3eJo8tnmak=,tag:bs/8KfbXxLXUTpslWDfDVA==,type:str]
admin-user:
display-name: ENC[AES256_GCM,data:UZtRNxgRZJU/XPC6Rw==,iv:sYhFT9n4q1q0PXylxkfnnIDuolhLoLJhg/QkiFlUOJs=,tag:0B32mrGz+Nm5bUZVqHJtvA==,type:str]
legal-name: ENC[AES256_GCM,data:TT/QPam3Isg9Nc9DaQ==,iv:m0TUxUNuTWQxwtES9h0T7fZHYOYlqPCpJiTuwGfsZs4=,tag:TI9eI131AQ9iKgK6RgHx5A==,type:str]
mail-address: ENC[AES256_GCM,data:NYDhK1TXIOoZSKA1Pe4I5g==,iv:4AMsvOYE1P5QjRYCAh1dpkCkdzzJLS+gY6FrwomB7GA=,tag:sI15OJH58l6YPsPQHe83uQ==,type:str]
oauth2:
home-assistant:
basic-secret: ENC[AES256_GCM,data:VP55eTX50+RX0rlEwU5tbA4UND47QemfmI9kQRjFf96Uk1b4CCf8tEaF0cTONS4o6CJLrGFlrtRwlTUnLqnbfA==,iv:DJEmTmrVRJ1rfUBA31oOZUVpSQrboNY3UOtSUiluGDg=,tag:uZHkj4XRBpAnhg8iLhKiTA==,type:str]
immich:
basic-secret: ENC[AES256_GCM,data:i759iM5Tl6XVZOT68IWNWRFCk0axoHJBbnbilgpmN2RJrjZYjGDqsz0/MXBiibpekFYa7i7TWGEGPw49fr4bPw==,iv:yZ+W+521YOwKHbaF/ZBH/b1MTYkGmpyRgIc00mpqXqo=,tag:4l0599SulgCApXddxH3woA==,type:str]
forgejo:
basic-secret: ENC[AES256_GCM,data:RxUbAF6Hv+6Hq2KCbBw7jwwIXr7CA5VraU5xfWSirpMb/F3s4ClEehcglYokQK9Q3b7DE4ITy7Bx/W52U9/UPg==,iv:VQ1w+fW4AF16+cZCdTkdrBArL0lBmco0m87kIfbGTFk=,tag:rXNIEEhS5JxKGm6KSAKQ8A==,type:str]
proxmox:
basic-secret: ENC[AES256_GCM,data:RA+pOI1CSLRmKkT72Vr7kUC7N37+et2u3zCBakeWe8tKE5kVDa7T0tIrQPZPnNMRhUPDdSTOmVkXW+x6B+UQKw==,iv:c3OvhShMiMx0OfAji8yR2Pmo9VNbanWd/vFg3OT9nvE=,tag:Jy3e4Rs0rwcjYlaJQtIgxw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHODRwaTJuUHEzYXFwU2li
VVpkVUtXUXBXNm1hZUp6NTc1Q3M2eWU4N1VNClVXc25QNmtpdGMwZU00SW5Kbk8r
Ykg2Sm1QOVdTTEZVMjE0N2FQMDYzUjgKLS0tIFhSMFo1aFBudjd2aFB0MnlVbURO
UnFUWkJKOTZ1RGlkSW1tQW5xR1MzWVkK5NiWZLeGh4a9AlR83FfRmmf/XhOsQjaX
JMFkdkjqiV1MqlcwfXmAimT/sVb0dHcsbUqHUsL/uZVeSvSjEf6B5w==
-----END AGE ENCRYPTED FILE-----
recipient: age132m0pg4utk3cjve2lgcjffvz7cevl0fq5krufu9sgud7wu2wgurqk49kgl
lastmodified: "2026-09-07T22:07:02Z"
mac: ENC[AES256_GCM,data:+3DlPcfvyHEchseo11ISbbUEb1LgGdjZPCMsjt/Mo290oX3PocVUtHY8KLNVAUs5j/T9fMorKBpFz250IfepHSSBvAeG5mCi0yMdfI0wjXIuGE+3fm/R+8VmCQ3hW6B2TW/4ls+LAr5nqjUknZUhqEabY7N5xiovtSXFw5K35eI=,iv:9YdeRvwTmljqy6Po3RpbvIujK6KrWqH5XygBYRJUQiE=,tag:emItS0XBgEBDjjMGNF1s3g==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3