Merge feature/oidc-sso-apps: native Keycloak OIDC SSO for gitea, nextcloud, semaphore
This commit is contained in:
11 files changed
+295
-41
No files matched your search
+63
-16
@@ -41,6 +41,23 @@ env:
|
||||
secretKeyRef:
|
||||
name: gitea-bootstrap
|
||||
key: admin-password
|
||||
# --- Keycloak native OIDC SSO (auth.mode oidc; docs/15 "Native OIDC"; R-IDP-02/06). The confidential
|
||||
# `gitea` realm client's secret is mirrored into this namespace's gitea-bootstrap Secret (the same
|
||||
# value SOPS-held in keycloak-realm-clients); the postStart hook registers it as the "keycloak"
|
||||
# OAuth2 login source. The LOCAL admin above stays as the break-glass path (docs/15). ---
|
||||
- name: GITEA_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gitea-bootstrap
|
||||
key: oidc-client-secret
|
||||
# app.ini [oauth2_client]: auto-provision a Gitea account on first Keycloak login and link it to an
|
||||
# existing local account by verified email, so SSO users don't need a pre-created local user.
|
||||
- name: GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION
|
||||
value: "true"
|
||||
- name: GITEA__oauth2_client__ACCOUNT_LINKING
|
||||
value: auto
|
||||
- name: GITEA__oauth2_client__USERNAME
|
||||
value: preferred_username
|
||||
lifecycle:
|
||||
postStart:
|
||||
exec:
|
||||
@@ -61,19 +78,47 @@ lifecycle:
|
||||
done
|
||||
if su-exec git gitea admin user list --admin 2>/dev/null | awk 'NR>1{print $2}' | grep -qx "$GITEA_ADMIN_USERNAME"; then
|
||||
echo "[gitea-bootstrap] admin '$GITEA_ADMIN_USERNAME' already present"
|
||||
exit 0
|
||||
else
|
||||
n=0
|
||||
until su-exec git gitea admin user create \
|
||||
--username "$GITEA_ADMIN_USERNAME" \
|
||||
--password "$GITEA_ADMIN_PASSWORD" \
|
||||
--email "[email protected]" \
|
||||
--admin --must-change-password=false; do
|
||||
n=$((n + 1))
|
||||
[ "$n" -ge 10 ] && { echo "[gitea-bootstrap] admin create failed after retries" >&2; break; }
|
||||
sleep 3
|
||||
done
|
||||
echo "[gitea-bootstrap] created break-glass admin '$GITEA_ADMIN_USERNAME'"
|
||||
fi
|
||||
# --- Keycloak OIDC login source (idempotent add-or-update; R-IDP-02/06) ---
|
||||
# Registers the confidential `gitea` client as the OAuth2 login source "keycloak" against the
|
||||
# homelab realm's discovery doc. The `groups` claim maps the platform-admins group to Gitea
|
||||
# admin (--admin-group). Auto-discovery pulls auth/token/userinfo/jwks endpoints. Runs on every
|
||||
# start: updates the existing source if present, else creates it. Missing secret => skip (the
|
||||
# local break-glass admin above still works), so an unfilled secret never crashes the pod.
|
||||
if [ -n "${GITEA_OIDC_CLIENT_SECRET:-}" ]; then
|
||||
oidc_id=$(su-exec git gitea admin auth list 2>/dev/null | awk '$2=="keycloak"{print $1; exit}')
|
||||
if [ -n "$oidc_id" ]; then
|
||||
su-exec git gitea admin auth update-oauth --id "$oidc_id" \
|
||||
--name keycloak --provider openidConnect --key gitea \
|
||||
--secret "$GITEA_OIDC_CLIENT_SECRET" \
|
||||
--auto-discover-url https://keycloak.home.keithjackson.dev/realms/homelab/.well-known/openid-configuration \
|
||||
--scopes "openid email profile" --group-claim-name groups --admin-group platform-admins \
|
||||
&& echo "[gitea-oidc] updated login source 'keycloak' (id $oidc_id)" \
|
||||
|| echo "[gitea-oidc] update-oauth failed" >&2
|
||||
else
|
||||
su-exec git gitea admin auth add-oauth \
|
||||
--name keycloak --provider openidConnect --key gitea \
|
||||
--secret "$GITEA_OIDC_CLIENT_SECRET" \
|
||||
--auto-discover-url https://keycloak.home.keithjackson.dev/realms/homelab/.well-known/openid-configuration \
|
||||
--scopes "openid email profile" --group-claim-name groups --admin-group platform-admins \
|
||||
&& echo "[gitea-oidc] added login source 'keycloak'" \
|
||||
|| echo "[gitea-oidc] add-oauth failed" >&2
|
||||
fi
|
||||
else
|
||||
echo "[gitea-oidc] GITEA_OIDC_CLIENT_SECRET unset; skipping OIDC source (break-glass admin only)"
|
||||
fi
|
||||
n=0
|
||||
until su-exec git gitea admin user create \
|
||||
--username "$GITEA_ADMIN_USERNAME" \
|
||||
--password "$GITEA_ADMIN_PASSWORD" \
|
||||
--email "[email protected]" \
|
||||
--admin --must-change-password=false; do
|
||||
n=$((n + 1))
|
||||
[ "$n" -ge 10 ] && { echo "[gitea-bootstrap] admin create failed after retries" >&2; exit 0; }
|
||||
sleep 3
|
||||
done
|
||||
echo "[gitea-bootstrap] created break-glass admin '$GITEA_ADMIN_USERNAME'"
|
||||
exposure: public # edge-forwarded git host at gitea.home.keithjackson.dev (docs/09; R-DNS/R-NET)
|
||||
egress: internet # normal WAN egress (docs/03; R-NET-05)
|
||||
storage:
|
||||
@@ -90,10 +135,12 @@ auth:
|
||||
mode: oidc # native Keycloak OIDC client (docs/13 SSO row, docs/15 "Native OIDC" set; R-IDP-02)
|
||||
groups:
|
||||
- platform-admins # access scoped to an authorized group (R-IDP-06)
|
||||
# NOTE (OIDC follow-up + break-glass): Gitea keeps a LOCAL admin as the break-glass path so an IdP
|
||||
# outage never locks the operator out of recovery tools (docs/15 "Availability caveat"). At initial
|
||||
# deploy Gitea comes up on that local admin; the Keycloak OIDC client (+ SOPS client secret) is wired
|
||||
# once Keycloak is live. This declaration records the target integration; it does not embed a secret.
|
||||
# WIRED (native OIDC): the Keycloak `gitea` confidential client (platform/keycloak/keycloak-realm-
|
||||
# import.yaml + live kcadm) is registered as Gitea's "keycloak" OAuth2 login source by the postStart
|
||||
# hook, using the client secret mirrored into the gitea-bootstrap Secret (oidc-client-secret; same
|
||||
# value SOPS-held in keycloak-realm-clients). platform-admins (groups claim) -> Gitea admin. Gitea
|
||||
# KEEPS a LOCAL admin as the break-glass path so an IdP outage never locks the operator out of
|
||||
# recovery tools (docs/15 "Availability caveat"). No secret is embedded here.
|
||||
update:
|
||||
channel: stable # R-UPD-01
|
||||
range: ">=1.22.0 <2.0.0" # stay on the 1.x stable line
|
||||
|
||||
+62
-8
@@ -57,6 +57,62 @@ env:
|
||||
value: https
|
||||
- name: OVERWRITECLIURL
|
||||
value: https://nextcloud.home.keithjackson.dev
|
||||
# --- Keycloak native OIDC SSO (auth.mode oidc; docs/15 "Native OIDC"; R-IDP-02/06). The confidential
|
||||
# `nextcloud` realm client's secret is mirrored into this namespace's nextcloud-bootstrap Secret
|
||||
# (oidc-client-secret; same value SOPS-held in keycloak-realm-clients). The postStart hook reads it
|
||||
# via user_oidc's --clientsecret-env (never on argv) to register the "keycloak" provider. The LOCAL
|
||||
# admin above stays as the break-glass path (docs/15). ---
|
||||
- name: NEXTCLOUD_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: nextcloud-bootstrap
|
||||
key: oidc-client-secret
|
||||
lifecycle:
|
||||
postStart:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
# Idempotent Keycloak OIDC wiring (R-IDP-02/06). Runs alongside the nextcloud entrypoint
|
||||
# (which installs/upgrades on first boot). Installs+enables the user_oidc app and creates OR
|
||||
# updates the "keycloak" login provider (occ user_oidc:provider is create-or-update by
|
||||
# identifier). Auto-discovery pulls the realm's endpoints; the `groups` claim drives Nextcloud
|
||||
# group provisioning. occ runs as www-data (owns config.php); the client secret is passed via
|
||||
# --clientsecret-env so it never lands on a command line. Safe to re-run every start; a missing
|
||||
# secret or not-yet-installed instance is skipped so the pod never crashloops (the local
|
||||
# break-glass admin still works).
|
||||
set -u
|
||||
OCC() { su -s /bin/sh www-data -c "php /var/www/html/occ $*"; }
|
||||
i=0
|
||||
until OCC status 2>/dev/null | grep -q "installed: true"; do
|
||||
i=$((i + 1))
|
||||
[ "$i" -ge 300 ] && { echo "[nextcloud-oidc] occ not ready after wait; skipping" >&2; exit 0; }
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "${NEXTCLOUD_OIDC_CLIENT_SECRET:-}" ]; then
|
||||
echo "[nextcloud-oidc] client secret unset; skipping OIDC (break-glass admin only)"
|
||||
exit 0
|
||||
fi
|
||||
if ! OCC app:list 2>/dev/null | grep -q "user_oidc"; then
|
||||
OCC app:install user_oidc || echo "[nextcloud-oidc] app:install user_oidc failed" >&2
|
||||
fi
|
||||
OCC app:enable user_oidc >/dev/null 2>&1 || true
|
||||
if OCC user_oidc:provider keycloak \
|
||||
--clientid=nextcloud \
|
||||
--clientsecret-env=NEXTCLOUD_OIDC_CLIENT_SECRET \
|
||||
--discoveryuri=https://keycloak.home.keithjackson.dev/realms/homelab/.well-known/openid-configuration \
|
||||
--scope="openid email profile" \
|
||||
--unique-uid=0 \
|
||||
--mapping-uid=preferred_username \
|
||||
--mapping-email=email \
|
||||
--mapping-display-name=name \
|
||||
--group-provisioning=1 \
|
||||
--mapping-groups=groups; then
|
||||
echo "[nextcloud-oidc] provider 'keycloak' configured"
|
||||
else
|
||||
echo "[nextcloud-oidc] provider config failed" >&2
|
||||
fi
|
||||
exposure: public # edge port-forward + public DNS (docs/09; R-DNS/R-NET)
|
||||
egress: internet # normal WAN egress (docs/03; R-NET-05) — app store, integrity checks
|
||||
storage:
|
||||
@@ -77,14 +133,12 @@ auth:
|
||||
mode: oidc # native Keycloak OIDC client (docs/13 SSO row, docs/15 "Native OIDC"; R-IDP-02)
|
||||
groups:
|
||||
- platform-admins # access scoped to an authorized group (R-IDP-06)
|
||||
# NOTE (OIDC follow-up + break-glass): like Gitea (docs/15 "Availability caveat"), Nextcloud keeps a
|
||||
# LOCAL admin as the break-glass path so an IdP outage never locks the operator out. At initial deploy
|
||||
# it comes up INSTALLED on that local admin (env above). The Keycloak `nextcloud` realm client + its
|
||||
# user_oidc app config is a DEFERRED follow-up: wiring it means editing the reconciled realm import in
|
||||
# platform-config (a malformed edit fails the whole platform-config apply / can leave the realm import
|
||||
# PENDING) AND post-install `occ` steps the image has no first-class env for — deferred rather than
|
||||
# risking the running Keycloak realm / Flux source (WS-APPS step 3 escape hatch). This records the
|
||||
# target integration; it embeds no secret.
|
||||
# WIRED (native OIDC): the Keycloak `nextcloud` confidential client (platform/keycloak/keycloak-realm-
|
||||
# import.yaml + live kcadm) is registered as a user_oidc login provider ("keycloak") by the postStart
|
||||
# hook (occ user_oidc:provider), using the client secret mirrored into nextcloud-bootstrap
|
||||
# (oidc-client-secret; same value SOPS-held in keycloak-realm-clients). The `groups` claim drives
|
||||
# user_oidc group provisioning (R-IDP-06). Nextcloud KEEPS its LOCAL admin as the break-glass path so
|
||||
# an IdP outage never locks the operator out (docs/15 "Availability caveat"). No secret is embedded here.
|
||||
update:
|
||||
channel: stable # R-UPD-01
|
||||
range: ">=31.0.0 <32.0.0" # stay on the 31.x stable line
|
||||
|
||||
+20
-8
@@ -73,6 +73,21 @@ env:
|
||||
# service links), pinning the listen port back to :3000 (matches containerPort/Service).
|
||||
- name: SEMAPHORE_PORT
|
||||
value: ":3000"
|
||||
# --- Keycloak native OIDC SSO (auth.mode oidc; docs/15 "Native OIDC"; R-IDP-02/06). Semaphore
|
||||
# v2.19.10 reads its OIDC providers from the SEMAPHORE_OIDC_PROVIDERS env as a JSON map
|
||||
# (util ConfigType `oidc_providers`, env-tagged SEMAPHORE_OIDC_PROVIDERS). The value is a single
|
||||
# JSON blob for the `keycloak` provider — provider_url is the homelab realm issuer (auto-discovery),
|
||||
# client_id=semaphore, redirect_url=/api/auth/oidc/keycloak/redirect, claims map to the realm's
|
||||
# token. It CONTAINS the confidential client secret, so the whole blob is held SOPS-encrypted in the
|
||||
# semaphore-bootstrap Secret (key oidc-providers-json; same secret value SOPS-held in
|
||||
# keycloak-realm-clients) and injected here by reference — never in cleartext. The name is NOT a
|
||||
# kubelet service-link var (those are SEMAPHORE_SERVICE_HOST / SEMAPHORE_PORT_*), so no collision.
|
||||
# The LOCAL SEMAPHORE_ADMIN above stays as the break-glass path (docs/15). ---
|
||||
- name: SEMAPHORE_OIDC_PROVIDERS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: semaphore-bootstrap
|
||||
key: oidc-providers-json
|
||||
# The semaphoreui/semaphore image runs as the non-root user UID 1001 / GID 0 (tini -> server-wrapper),
|
||||
# and on boot the wrapper writes its generated config.json into the config dir. A mounted Longhorn PVC
|
||||
# is root:root, so UID 1001 cannot write it -> the pod panics ("permission denied"). fsGroup=1001 makes
|
||||
@@ -101,14 +116,11 @@ auth:
|
||||
mode: oidc # native Keycloak OIDC client (docs/13 SSO row, docs/15 "Native OIDC"; R-IDP-02)
|
||||
groups:
|
||||
- platform-admins # access scoped to an authorized group (R-IDP-06)
|
||||
# NOTE (OIDC follow-up + break-glass): like Gitea/Nextcloud (docs/15 "Availability caveat"),
|
||||
# Semaphore keeps a LOCAL admin as the break-glass path so an IdP outage never locks the operator
|
||||
# out. At initial deploy it comes up on that local admin (SEMAPHORE_ADMIN* env above). The Keycloak
|
||||
# `semaphore` realm client + Semaphore's OIDC provider config (config.json `oidc_providers`) is a
|
||||
# DEFERRED follow-up: wiring it means editing the reconciled realm import in platform-config (a
|
||||
# malformed edit fails the whole platform-config apply / can leave the realm import PENDING) — so it
|
||||
# is deferred rather than risking the running Keycloak realm / Flux source. This records the target
|
||||
# integration; it embeds no secret.
|
||||
# WIRED (native OIDC): the Keycloak `semaphore` confidential client (platform/keycloak/keycloak-realm-
|
||||
# import.yaml + live kcadm) is consumed via the SEMAPHORE_OIDC_PROVIDERS env (JSON blob held encrypted
|
||||
# in the semaphore-bootstrap Secret, oidc-providers-json). The `groups` claim is exposed for
|
||||
# authorization (R-IDP-06). Semaphore KEEPS its LOCAL admin as the break-glass path so an IdP outage
|
||||
# never locks the operator out (docs/15 "Availability caveat"). No secret is embedded here.
|
||||
update:
|
||||
channel: stable # R-UPD-01
|
||||
range: ">=2.19.0 <3.0.0" # stay on the 2.x stable line
|
||||
|
||||
Reference in new issue
Block a user