Fix Authentik WIF client credentials
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Successful in 2m46s
Build Rust / Cargo Test (push) Successful in 4m1s
Build Site / Next.js Build (push) Failing after 4s
Lint Governance / BEP Metadata (push) Successful in 1s

This commit is contained in:
Conrad Kramer 2026-06-07 13:07:34 -07:00
parent 8180c3c35a
commit e7c0442a12
6 changed files with 107 additions and 35 deletions

View file

@ -11,7 +11,7 @@ client_id="${AUTHENTIK_GOOGLE_WIF_CLIENT_ID:-google-wif.burrow.net}"
client_secret="${AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET:-}"
launch_url="${AUTHENTIK_GOOGLE_WIF_LAUNCH_URL:-https://console.cloud.google.com/}"
redirect_uris_json="${AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON:-[]}"
access_group="${AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP:-burrow-automation}"
access_group="${AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP:-}"
usage() {
cat <<'EOF'
@ -33,7 +33,11 @@ Optional environment:
AUTHENTIK_GOOGLE_WIF_CLIENT_ID
AUTHENTIK_GOOGLE_WIF_LAUNCH_URL
AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON
AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP
AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP (optional; leave empty for client credentials)
AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET is the Authentik client-credentials secret
used by Forgejo runners. For service-account authentication, store
base64("<service-account-username>:<app-password-token>").
EOF
}
@ -214,9 +218,18 @@ existing_application="$(
| head -n1
)"
if [[ -z "$existing_application" ]]; then
direct_application_result="$(api_with_status GET "/api/v3/core/applications/${application_slug}/")"
direct_application_status="$(printf '%s\n' "$direct_application_result" | sed -n '1p')"
direct_application_body="$(printf '%s\n' "$direct_application_result" | sed '1d')"
if [[ "$direct_application_status" == "200" ]]; then
existing_application="$direct_application_body"
fi
fi
if [[ -n "$existing_application" ]]; then
application_pk="$(printf '%s\n' "$existing_application" | jq -r '.pk')"
api PATCH "/api/v3/core/applications/${application_pk}/" "$application_payload" >/dev/null
api PATCH "/api/v3/core/applications/${application_slug}/" "$application_payload" >/dev/null
else
create_application_result="$(
api_with_status POST "/api/v3/core/applications/" "$application_payload"
@ -235,6 +248,14 @@ else
| jq -r --arg slug "$application_slug" '.results[]? | select(.slug == $slug) | .pk // empty' \
| head -n1
)"
if [[ -z "$application_pk" ]]; then
direct_application_result="$(api_with_status GET "/api/v3/core/applications/${application_slug}/")"
direct_application_status="$(printf '%s\n' "$direct_application_result" | sed -n '1p')"
direct_application_body="$(printf '%s\n' "$direct_application_result" | sed '1d')"
if [[ "$direct_application_status" == "200" ]]; then
application_pk="$(printf '%s\n' "$direct_application_body" | jq -r '.pk // empty')"
fi
fi
else
printf '%s\n' "$create_application_body" >&2
echo "error: could not reconcile Authentik application ${application_slug}" >&2
@ -247,6 +268,32 @@ if [[ -z "${application_pk:-}" ]]; then
exit 1
fi
delete_binding() {
local binding_pk="$1"
api DELETE "/api/v3/policies/bindings/${binding_pk}/" >/dev/null || true
}
delete_application_group_bindings_except() {
local keep_group_pk="${1:-}"
local binding_pks
binding_pks="$(
api GET "/api/v3/policies/bindings/?page_size=500" \
| jq -r \
--arg target "$application_pk" \
--arg keep_group "$keep_group_pk" \
'.results[]?
| select((.target | tostring) == $target and .group != null)
| select(($keep_group == "") or ((.group | tostring) != $keep_group))
| .pk'
)"
while IFS= read -r binding_pk; do
[[ -n "$binding_pk" ]] || continue
delete_binding "$binding_pk"
done <<< "$binding_pks"
}
if [[ -n "$access_group" ]]; then
group_pk="$(
api GET "/api/v3/core/groups/?page_size=200" \
@ -256,21 +303,36 @@ if [[ -n "$access_group" ]]; then
if [[ -z "$group_pk" ]]; then
echo "warning: Authentik Google WIF access group ${access_group} was not found; application policy left unchanged." >&2
else
api POST "/api/v3/policies/bindings/" "$(
jq -n \
--arg target "$application_pk" \
--arg group "$group_pk" \
'{
target: $target,
group: $group,
negate: false,
order: 0,
enabled: true,
timeout: 30,
failure_result: false
}'
)" >/dev/null || true
delete_application_group_bindings_except "$group_pk"
existing_binding="$(
api GET "/api/v3/policies/bindings/?page_size=500" \
| jq -r \
--arg target "$application_pk" \
--arg group "$group_pk" \
'.results[]?
| select((.target | tostring) == $target and (.group | tostring) == $group)
| .pk' \
| head -n1
)"
if [[ -z "$existing_binding" ]]; then
api POST "/api/v3/policies/bindings/" "$(
jq -n \
--arg target "$application_pk" \
--arg group "$group_pk" \
'{
target: $target,
group: $group,
negate: false,
order: 0,
enabled: true,
timeout: 30,
failure_result: false
}'
)" >/dev/null || true
fi
fi
else
delete_application_group_bindings_except ""
fi
for _ in $(seq 1 30); do

View file

@ -115,12 +115,13 @@ else
fi
token_endpoint="$(resolve_token_endpoint)"
curl -fsS \
-u "${authentik_client_id}:${authentik_client_secret}" \
-d grant_type=client_credentials \
-d client_id="$authentik_client_id" \
-d client_secret="$authentik_client_secret" \
-d scope="openid profile email groups" \
-d audience="$authentik_audience" \
"$token_endpoint" \
| jq -r '.id_token // empty' > "$token_path"
| jq -r '.id_token // .access_token // empty' > "$token_path"
fi
chmod 0600 "$token_path"

View file

@ -85,6 +85,14 @@ The same change also makes the forge itself the release authority: release tags
token endpoint, not a provider-slug child path. The helper should prefer an
explicit `BURROW_AUTHENTIK_WIF_TOKEN_ENDPOINT`, then OIDC discovery, then the
Authentik `/application/o/token/` fallback derived from the configured issuer.
- The Google WIF Authentik application is a machine-to-machine
client-credentials provider. It must not default to a human group policy
binding, because the token exchange has no interactive user and Authentik will
reject the grant before Google WIF can validate the issued JWT.
- Forgejo should store the Authentik WIF credential as the client-credentials
`client_secret` form value. For service-account authentication, that value is
`base64("<service-account-username>:<app-password-token>")`; the helper must
send it as form data, not HTTP Basic auth.
- Produce unsigned Apple validation artifacts when signing is absent, but require signing for an App Store requested iOS release and for a Sparkle tester release. Uploadable artifacts are produced only after provisioning profiles can be synced from App Store Connect credentials and the relevant Apple certificate is proven to match the selected Google KMS key.
- Resolve release credentials through age/agenix before signed Apple lanes run. Forgejo secrets should carry only the runner age identity fallback and the Authentik WIF client secret until OpenBao can mint the runner token directly. App Store Connect keys and signing certificates live as sealed files under `secrets/`.
- Persist the forge runner SSH key as `/var/lib/forgejo-runner-agent/age_keystore` and export `BURROW_RUNNER_AGE_IDENTITY_PATH` so jobs can resolve agenix identities without embedding long-lived material in every workflow.

View file

@ -302,7 +302,6 @@ in
googleClientSecretFile = config.age.secrets.burrowAuthentikGoogleClientSecret.path;
googleAccountMapFile = config.age.secrets.burrowAuthentikGoogleAccountMap.path;
googleWifClientSecretFile = config.age.secrets.burrowAuthentikGoogleWifClientSecret.path;
googleWifAccessGroupName = contributors.groups.automation;
googleLoginMode = "redirect";
userGroupName = contributors.groups.users;
adminGroupName = contributors.groups.admins;

View file

@ -183,13 +183,13 @@ in
googleWifClientSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Host-local file containing the Authentik Google WIF OIDC client secret.";
description = "Host-local file containing the Authentik Google WIF client-credentials secret.";
};
googleWifAccessGroupName = lib.mkOption {
type = lib.types.str;
default = "burrow-automation";
description = "Authentik group allowed to mint Google WIF tokens.";
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional Authentik group allowed to launch the Google WIF application. Leave null for client-credentials token exchange.";
};
tailscaleProviderSlug = lib.mkOption {
@ -979,7 +979,9 @@ EOF
export AUTHENTIK_GOOGLE_WIF_CLIENT_SECRET="$(tr -d '\r\n' < ${lib.escapeShellArg cfg.googleWifClientSecretFile})"
export AUTHENTIK_GOOGLE_WIF_LAUNCH_URL=https://console.cloud.google.com/
export AUTHENTIK_GOOGLE_WIF_REDIRECT_URIS_JSON='[]'
export AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP=${lib.escapeShellArg cfg.googleWifAccessGroupName}
${lib.optionalString (cfg.googleWifAccessGroupName != null) ''
export AUTHENTIK_GOOGLE_WIF_ACCESS_GROUP=${lib.escapeShellArg cfg.googleWifAccessGroupName}
''}
${pkgs.bash}/bin/bash ${googleWifOidcSyncScript}
'';

View file

@ -1,11 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 ux4N8Q tBglVPqKvCyAssCU7wVjFHHKlWCsbmar9sWJOThjPTY
4GKg+66JVi6ruPTlZg2SgmLPgMlGMDMtGUu8KmlxjRQ
-> ssh-ed25519 IrZmAg C7CT5hJ2/TIVWBfG9v7CCeDTclDt9S5ejzLeE86EO0o
lSSn+DgRhJGCIQRRvpA3ErRD0OZtwSVgF/bu9Wj6OyU
-> ssh-ed25519 6PdzAw 3ZN3lfIb13sJL7ZJhYdP27RxLfZQlY0TDFM+5zOdnwE
A3ccTSOvg4RNQRBOFsqdEIsO6EfXHoE8gIm1/rwVUAk
-> X25519 M3bXNlL5lplqUSYeDi/MO7HhEnToTsDA1Xfhx9rI8TU
SlWPhylIEC5fpzj+gn9/tHmOEVoom1pyjlmxY+XhSPk
--- Op4e1VN3vK0xqjhndSOCTQcKwKGK76Y3gr6+B5P96tQ
úJìh}âcd¸ŠJ˜ý]¨–ÖÀì… ö,BœÏNë×{¡0o‰  ¬<>n•ÚºÔN*”ï`HÌ-» ýÁ—rþÍ
-> ssh-ed25519 ux4N8Q dmNzqS+jc7JGI31mzsRRxSr7vAxRw3uxkbs4vzG20gk
sJWziiiMhKGzEiAeIxPfeqUYIu4CzZRw+eMXle5zMRs
-> ssh-ed25519 IrZmAg McLHhYu/3wDQYB1bJ/rXsoTv2UgpVTRm4AqsCTLkhCc
Rrl0vy0mhdXR0pSHckRI39Eg0bxB/Km3lCXXxl9t9Io
-> ssh-ed25519 0kWPgQ UQoa9fP3OPYtesWmMiWL1q8MqPxsXy//hj6805JQBRg
UKt/WM0t90OPDH9A+RVgrW60vlI2FmRewGsXGD5FCkU
-> X25519 48f0e9+P3ihg3mJIQxvHvq1P/4JEfS1t7DO73/TRGwg
k70uLkBQt5F1ovKhemYRhyKuPQXfT5XSXMzaQczPC8Q
--- L9B029nBJvUvCKhU2Avmmdmw5sce7bvhFpiPm0p+TVE
ÄnŕG$űä\ä_$s&éźőÎŞAŞţL­ČlßýZ<C3BD>ŕÉŞ« Nů¬đ!´ĹĚŰZÔA%LVşb$ⱝ`aĆKŔ<Ău•¸Ť&Yj"ţ&cvC{1÷ş¨|řN­Ą¨Ş÷ŤŰĹ{kĎ9Ó@tŠł*UÍ™~2Î/Ă<>·äGBăF+űĄ$’îű?;Ĺ˙Îv