fix(gitea): OIDC Zitadel réellement fonctionnel (source OAuth2, app Web)

L'ancien câblage ne pouvait pas marcher : client_id littéral "gitea"
(impossible, Zitadel génère les client_id) et oneshot qui échouait à chaque
boot sur --enabled (flag inexistant en Gitea 1.27.x), avalé par `|| true`.

- App Zitadel "Gitea" (projet marcpartensky) : appId 396570385799184684,
  clientId 396570385799250220, type Web, redirect
  https://git.marcpartensky.com/user/oauth2/zitadel/callback
- oneshot gitea-oidc-init : add-or-update (id de login_source via psql),
  secret lu depuis sops à chaque exécution, idempotent même en dérive
- [openid] d'app.ini : OpenID 2.0 legacy, désactivé (sans rapport avec OIDC,
  échouait en "rel=openid2.provider not found" avec une URL Zitadel)
- [oauth2_client] : ENABLE_AUTO_REGISTRATION, USERNAME=email,
  ACCOUNT_LINKING=login (page de liaison unique pour le compte local marc)
- ressource Pangolin : auth.sso-enabled=false (auth Gitea seule, une seule
  couche, clients git/API à token non bloqués par un redirect SSO)
- secrets/gitea-oidc.yml : vrai oidc_client_secret (chiffré, clé tower)
This commit is contained in:
Hermes Agent
2026-10-24 15:44:57 -04:00
parent dd7ed87ac2
commit eb71f8ae64
2 changed files with 75 additions and 38 deletions
+8 -8
View File
@@ -1,16 +1,16 @@
oidc_client_secret: ENC[AES256_GCM,data:2TSgQrZww3yuwyuR1mG6qUp7KrBJuR1tOOE6BdSbGZXWaDDI+dyGSSXN5+rFQQ==,iv:qVuET4ocZaxIJfpaMsLuhDMuC/G1EYDStfnbLnh7rzo=,tag:VURs7XDrU+46/J3PyrGi+Q==,type:str]
oidc_client_secret: ENC[AES256_GCM,data:XKAt2uy7rcBivcfS8d16h/SZVnyIJef6K4lskRCbMWg0Jw4ju4zLDwzONwFFURni/z7CoQ95UxLWeWo+/w5krQ==,iv:hHvNGDaECgr05jp6Od+SmFb49yBxiMrn7bRUU3/x4uw=,tag:vAJAqzoApk1OioRmHhEmOA==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNWDdsbG4zK2ZMVnc0RGRr
TVhXN1NZSEtqblp2OWtGaDBTajZsRzhrSVZ3Cm50L3owUlJ3SWlPZjU5VnlOQk94
TGZlb3VVUjdTejBMQjA4aGRkV3l2TmcKLS0tIHVpWXc1UTd6OTJqd256T1Vqb3Fp
U2NISUlUd21OWXNiclUvaTJEV0NJbVEKks34IfOTgEbF3szG0w9/4co8KlYweJIa
QXj8QEkIB4BgavfXsERycDTywG9fBQBrIyZGL0/bHteszDtdTkNOLw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoNE1jRjNHbVh5UDdIQUQx
VmZNTUdOSDMvSTA1THhhcVlIaVZnd3JpZGtjClFwRFJ0MGJlTEdjbXNLK0FoSFlQ
eTExQzNFRmhzM001K0FCVSttMXdqK00KLS0tIG44Q29Tb1AzZUFjRWFCMGh6a0d5
TFl5WjVRaUUxT2JBZkxUSGNrNVFOKzQKZjDLuqbMGeBjJUyLNJ/ZQOVvZIA6aufK
Tgjt3afnIsWinmLKyJvxbVWSdg1oN03CYLqsakdwPG7C/1d0pwUAxw==
-----END AGE ENCRYPTED FILE-----
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
lastmodified: "2026-09-27T21:55:00Z"
mac: ENC[AES256_GCM,data:Cd18XRtmFHoUv6o9lzE0llWZaRjk0PYNOScb/tlxCiDIgf7wnD6u5u0IR//aSk4oyWvI3DOgdV5mPtypu/eLkHAOUwFM8yIMU5vvfLjkAuJzLWa+BrQjyZJvd98u1vNMEPhMAA4JLcBQi2EJChfMwdcN6RbMv6q2mEN2sHRjqck=,iv:UwNrKDDfhYO+A26AclSwxukF5mMhw5C3iPy+UI4+/Io=,tag:84OtSeSR4DMaEREXxt6QfA==,type:str]
lastmodified: "2026-10-24T19:33:29Z"
mac: ENC[AES256_GCM,data:/ml3AS+tvqTNe49fhGrGh5a3iraFyBBHw6bfkLiJaZMxrkLoQOkgvM97mJQ1qEq5C9TwmymfFn99fOr1G8ZRgOIRsZ44oJudPjovWfYb5t+krQ32oNMLOrPIEVWg3ohLu+RujA2gpeJ8Vw51QOtTDSPd5pFmlQaClk1izv31/fc=,iv:OvbJdk5vgjBfP/UgO/2//92LyPX9WAGyPY2FDRxHyB4=,tag:NpuGrMDrmCxLMtPPucQUIQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3
+67 -30
View File
@@ -2,10 +2,12 @@
# Instance Gitea (forge git auto-hébergée) sur git.marcpartensky.com.
#
# Écoute en loopback (127.0.0.1:3001), exposée uniquement via le blueprint
# Pangolin/newt (SSO devant, comme hermes-webui/hermes-dashboard). Base
# postgres locale via socket unix (peer auth, pas de secret DB nécessaire :
# services.gitea.database.createDatabase=true crée le rôle "gitea" et se
# connecte par défaut sur /run/postgresql avec le user système "gitea").
# Pangolin/newt, sans SSO Pangolin (auth.sso-enabled=false) : Gitea porte son
# propre login (comptes locaux + OIDC Zitadel) et ses tokens d'API — une seule
# couche d'auth, et les clients git/API à token ne sont pas redirigés vers une
# page SSO. Base postgres locale via socket unix (peer auth, pas de secret DB
# nécessaire : services.gitea.database.createDatabase=true crée le rôle "gitea"
# et se connecte par défaut sur /run/postgresql avec le user système "gitea").
#
# Inscription publique désactivée (DISABLE_REGISTRATION) : le compte admin
# "marc" est créé une fois par un oneshot idempotent (gitea-admin-init) via
@@ -16,18 +18,22 @@
# ne supporte pas le mode de ressource "ssh" de Pangolin (exige > 1.13). Clone
# HTTPS uniquement pour l'instant (token d'accès personnel Gitea).
#
# OpenID Connect (OIDC) vers Zitadel (auth.marcpartensky.com) :
# - configuration [openid] dans app.ini via settings.openid
# - source d'auth OAuth2 "Zitadel" créée via gitea admin auth add-oauth
# (oneshot gitea-oidc-init, idempotent).
# OIDC vers Zitadel (auth.marcpartensky.com) : source d'auth OAuth2 "Zitadel"
# (bouton sur la page de login), entretenue par le oneshot gitea-oidc-init
# (idempotent, add-or-update). App Zitadel "Gitea" (projet marcpartensky,
# appId 396570385799184684) de type Web : client_id ci-dessous, client_secret
# dans secrets/gitea-oidc.yml. NB : la section [openid] d'app.ini n'est PAS de
# l'OIDC (OpenID 2.0 legacy, incompatible avec Zitadel) : désactivée ci-dessous.
{
config,
lib,
pkgs,
...
}: let
# Secret OIDC client pour Zitadel (créé dans la console Zitadel)
# Le client_id/secret sont stockés dans secrets/gitea.yml (sops)
# App OIDC "Gitea" dans Zitadel (projet marcpartensky), créée le 24/10/2026.
# Zitadel GÉNÈRE les client_id : "gitea" (l'ancienne valeur) n'a jamais pu
# exister. Le client_secret vit hors du store nix (secrets/gitea-oidc.yml).
oidcClientId = "396570385799250220";
oidcClientSecret = config.sops.secrets."gitea/oidc_client_secret";
in {
sops.secrets."gitea/admin_password" = {
@@ -59,11 +65,24 @@ in {
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
};
# OpenID Connect (OIDC) pour Zitadel
# OpenID 2.0 legacy (bouton « OpenID » de la page de login) : sans
# rapport avec Zitadel qui parle OIDC. Désactivé pour ne pas induire en
# erreur (avec une URL Zitadel il échoue en "LINK with
# rel=openid2.provider not found"). Le SSO passe par la source OAuth2.
openid = {
ENABLE_OPENID_SIGNIN = true;
ENABLE_OPENID_SIGNUP = true;
WHITELISTED_URIS = "auth.marcpartensky.com";
ENABLE_OPENID_SIGNIN = false;
ENABLE_OPENID_SIGNUP = false;
};
# Source OAuth2 (Zitadel) : création automatique du compte local au
# premier login OIDC. USERNAME=email → localpart de l'email Zitadel
# (marc@marcpartensky.com → "marc"). Si le nom dérivé entre en collision
# avec un compte existant (cas de marc), ACCOUNT_LINKING=login affiche
# une page de liaison : on saisit son mot de passe local une fois, puis
# l'identité OIDC est rattachée au compte existant.
oauth2_client = {
ENABLE_AUTO_REGISTRATION = true;
USERNAME = "email";
ACCOUNT_LINKING = "login";
};
# Pangolin/newt tunnelent en local : gitea verrait toute requête
# publique comme venant de 127.0.0.1. Rien de sensible n'est protégé
@@ -104,10 +123,11 @@ in {
'';
};
# Enregistre la source OAuth2 OIDC "Zitadel" dans Gitea (idempotent).
# Nécessite un client OIDC déjà créé dans Zitadel (client_id, client_secret,
# redirect_uri = https://git.marcpartensky.com/user/oauth2/zitadel/callback).
# Le secret est lu depuis sops.
# Enregistre/actualise la source OAuth2 OIDC "Zitadel" dans Gitea
# (add-or-update idempotent). S'appuie sur l'app Zitadel "Gitea" :
# client_id = oidcClientId, redirect_uri enregistrée côté Zitadel =
# https://git.marcpartensky.com/user/oauth2/zitadel/callback.
# Le client_secret est lu depuis sops à chaque exécution.
systemd.services.gitea-oidc-init = {
description = "Enregistre la source OAuth2 OIDC Zitadel dans Gitea (idempotent)";
after = ["gitea.service" "gitea-admin-init.service" "postgresql.service"];
@@ -126,15 +146,31 @@ in {
Group = config.services.gitea.group;
};
script = ''
${config.services.gitea.package}/bin/gitea admin auth add-oauth \
--name zitadel \
--provider openidConnect \
--key "gitea" \
--secret "$(cat ${oidcClientSecret.path})" \
--auto-discover-url "https://auth.marcpartensky.com/.well-known/openid-configuration" \
--scopes "openid email profile" \
--enabled true \
--allow-deactivate false || true
set -eu
DISCOVERY="https://auth.marcpartensky.com/.well-known/openid-configuration"
SECRET="$(cat ${oidcClientSecret.path})"
# Add-or-update : si la source existe (id en base), la mettre à jour
# (toute dérive de client_id/secret est corrigée à chaque activation),
# sinon la créer. En 1.27.x les sources créées par la CLI sont actives
# par défaut (les flags --enabled/--allow-deactivate n'existent plus).
PSQL="${config.services.postgresql.package}/bin/psql"
SRC_ID="$("$PSQL" -U gitea -d gitea -Atc "select id from login_source where name = 'zitadel'")"
if [ -n "$SRC_ID" ]; then
${config.services.gitea.package}/bin/gitea admin auth update-oauth \
--id "$SRC_ID" \
--key "${oidcClientId}" \
--secret "$SECRET" \
--auto-discover-url "$DISCOVERY" \
--scopes openid --scopes email --scopes profile
else
${config.services.gitea.package}/bin/gitea admin auth add-oauth \
--name zitadel \
--provider openidConnect \
--key "${oidcClientId}" \
--secret "$SECRET" \
--auto-discover-url "$DISCOVERY" \
--scopes openid --scopes email --scopes profile
fi
'';
};
@@ -142,12 +178,13 @@ in {
# (tower) : `site` omis volontairement, cf. services/newt. Schéma
# `proxy-resources` + `protocol` (celui vérifié fonctionnel côté hermes-webui
# / hermes-dashboard sur ce contrôleur Pangolin, cf. selfhosted-remote-access
# / references/pangolin-blueprint.md).
# / references/pangolin-blueprint.md). Pas de SSO Pangolin : l'auth est
# portée par Gitea lui-même (local + OIDC Zitadel), une seule couche.
services.newt.blueprint.proxy-resources.gitea = {
name = "gitea";
protocol = "http";
full-domain = "git.marcpartensky.com";
auth.sso-enabled = true;
auth.sso-enabled = false;
targets = [
{
hostname = "127.0.0.1";
@@ -178,4 +215,4 @@ in {
message = "gitea : le port HTTP local ne doit pas être ouvert au firewall (exposition uniquement via Pangolin/newt).";
}
];
}
}