diff --git a/CLAUDE.md b/CLAUDE.md index ccebf03..4b2a30f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,7 +369,26 @@ collision.) **Has built-in auth — no Authelia needed:** `emby`, `jellyfin`, `audiobookshelf`, `immich`, `mealie`, `actualbudget`, `homeassistant`, `portainer`, `meshcentral`, `traccar`, `uptimekuma`, -`filebrowser`, `wg-easy`, `ntfy` (configurable) +`filebrowser`, `wg-easy`, `ntfy` (configurable), `gitea` + +**Native OIDC login as an addition, not a Caddy gate — `gitea`'s pattern.** +Some apps with their own built-in login *also* have their own "add an +OAuth2/OIDC provider" setting — a genuinely different integration from +both the forward_auth gate above and the "Enable OpenID" client-registration +flow below. `services/gitea.sh`'s `_gitea_offer_authelia_sso()` is the +reference: if Authelia is installed, offers to register Gitea as an OIDC +client (via `services/authelia.sh`'s `_authelia_provision_oidc_client()` — +the same non-interactive, out-param-returning core that +`_authelia_add_oidc_client()`'s menu flow uses) and then runs `gitea admin +auth add-oauth` itself to add Authelia as an authentication source — no +manual web-UI copy-paste on either side, matching this repo's "no manual +wizard" philosophy elsewhere in gitea.sh (admin account/token creation). +Local login keeps working unchanged; this only adds an extra button on the +existing login page. Reuse `_authelia_provision_oidc_client()` (guarded by +`declare -F`, same convention as chaining into another service's +`install_()`) for any future service with its own native OIDC field, +instead of duplicating Authelia's client-secret-generation/config-patching +logic again. **No built-in auth — should be protected:** `magicmirror`, `wolf-pair`, `js99er`, `drum-rhythm-game`, `iopaint`, diff --git a/services/authelia.sh b/services/authelia.sh index 4009c55..e1194d3 100644 --- a/services/authelia.sh +++ b/services/authelia.sh @@ -1190,12 +1190,32 @@ _authelia_ensure_oidc_provider() { log_success "OIDC provider enabled (signing key + HMAC secret generated)" } -# Registers an OIDC client for another app to log in via Authelia — the -# "Other" provider option in an app's own "Enable OpenID"/SSO dialog. Presets -# below hand back the app's own known redirect URI path and the exact fields -# to paste where; "Other/custom" covers anything not listed (the app's own -# OIDC/SSO docs will say what redirect URI it expects). -_authelia_add_oidc_client() { +# Non-interactive core of _authelia_add_oidc_client() below — generates a +# client secret, patches it into identity_providers.oidc.clients, and +# (optionally) restarts Authelia. Fully self-contained (re-validates +# everything itself rather than trusting a caller's state) so other +# services can call it directly to register themselves as an OIDC client +# without walking a human through this file's own menu — see +# services/gitea.sh's "Sign in with Authelia" step for the reference caller. +# Guard every cross-file call with `declare -F` per this repo's chaining +# convention (services/gitea.sh does). +# +# Args: APP_NAME CLIENT_ID AUTH_POLICY RESTART_AUTH(y/n) [ ...] +# Out-params (not `local` — read them after the call returns): +# OIDC_CLIENT_SECRET_PLAIN the plaintext secret. Shown once — Authelia's +# config only ever stores the hash — so the +# caller must capture and use/display it now. +# OIDC_AUTHELIA_DOMAIN this Authelia instance's apex domain, for +# building discovery/authorization/token URLs. +# Returns 1 on failure (Authelia not installed, client ID already taken, +# secret generation failed) with the reason already logged. +_authelia_provision_oidc_client() { + local APP_NAME="$1" CLIENT_ID="$2" AUTH_POLICY="$3" RESTART_AUTH="$4"; shift 4 + local -a REDIRECT_URIS=("$@") + + OIDC_CLIENT_SECRET_PLAIN="" + OIDC_AUTHELIA_DOMAIN="" + local AUTHELIA_DIR="$DOCKER_DIR/authelia" local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml" @@ -1206,12 +1226,101 @@ _authelia_add_oidc_client() { _authelia_ensure_oidc_provider "$AUTHELIA_DIR" || return 1 + # The apex domain this Authelia instance already serves — read back from + # its own session.cookies (same structure install_authelia()/ + # add_authelia_domain() write), rather than asking again or assuming a + # variable set earlier in this run is still in scope (this flow can be + # reached standalone from the "already exists" menu, or from another + # service entirely, with none of install_authelia()'s own locals ever + # having run this session). + OIDC_AUTHELIA_DOMAIN="$(awk '/^ cookies:$/{f=1; next} f && /domain:/{print $3; exit}' "$CONFIG_FILE")" + if [ -z "$OIDC_AUTHELIA_DOMAIN" ]; then + log_warning "Couldn't determine this Authelia instance's domain from $CONFIG_FILE — aborting." + return 1 + fi + + if grep -qF "client_id: '${CLIENT_ID}'" "$CONFIG_FILE" 2>/dev/null; then + log_warning "A client with ID '$CLIENT_ID' is already registered in $CONFIG_FILE." + return 1 + fi + + log_info "Generating client secret..." + local _hash_out CLIENT_SECRET_HASH + _hash_out="$(docker run --rm authelia/authelia:4.39.20 \ + authelia crypto hash generate pbkdf2 --variant sha512 --random \ + --random.length 72 --random.charset rfc3986 2>/dev/null)" + OIDC_CLIENT_SECRET_PLAIN="$(echo "$_hash_out" | sed -n 's/^Random Password: //p')" + CLIENT_SECRET_HASH="$(echo "$_hash_out" | sed -n 's/^Digest: //p')" + if [ -z "$OIDC_CLIENT_SECRET_PLAIN" ] || [ -z "$CLIENT_SECRET_HASH" ]; then + log_warning "Couldn't generate the client secret automatically. Run manually, then add the" + log_warning "client to $CONFIG_FILE's identity_providers.oidc.clients by hand:" + echo " docker run --rm authelia/authelia:4.39.20 authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986" + OIDC_CLIENT_SECRET_PLAIN="" + return 1 + fi + + grep -q '^ clients: \[\]$' "$CONFIG_FILE" && sed -i 's/^ clients: \[\]$/ clients:/' "$CONFIG_FILE" + + local REDIRECT_URIS_YAML + REDIRECT_URIS_YAML="$(printf " - '%s'\n" "${REDIRECT_URIS[@]}")" + REDIRECT_URIS_YAML="${REDIRECT_URIS_YAML%$'\n'}" + + local CLIENT_BLOCK=" - client_id: '${CLIENT_ID}' + client_name: '${APP_NAME}' + client_secret: '${CLIENT_SECRET_HASH}' + public: false + authorization_policy: '${AUTH_POLICY}' + redirect_uris: +${REDIRECT_URIS_YAML} + scopes: + - 'openid' + - 'profile' + - 'email' + grant_types: + - 'authorization_code' + response_types: + - 'code' + response_modes: + - 'query' + userinfo_signed_response_alg: 'none'" + + awk -v block="$CLIENT_BLOCK" ' + { print } + /^ clients:$/ && !done { print block; done=1 } + ' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" + chown 1000:1000 "$CONFIG_FILE" 2>/dev/null || true + + if [[ "$RESTART_AUTH" =~ ^[Yy]$ ]]; then + (cd "$AUTHELIA_DIR" && docker compose restart authelia 2>/dev/null) \ + && log_success "Authelia restarted" \ + || log_warning "Authelia restart failed — check: docker compose logs authelia" + fi + return 0 +} + +# Registers an OIDC client for another app to log in via Authelia — the +# "Other" provider option in an app's own "Enable OpenID"/SSO dialog. Presets +# below hand back the app's own known redirect URI path and the exact fields +# to paste where; "Other/custom" covers anything not listed (the app's own +# OIDC/SSO docs will say what redirect URI it expects). Interactive wrapper +# around _authelia_provision_oidc_client() above, which does the actual work. +_authelia_add_oidc_client() { + local AUTHELIA_DIR="$DOCKER_DIR/authelia" + local CONFIG_FILE="$AUTHELIA_DIR/config/configuration.yml" + + if [ ! -f "$CONFIG_FILE" ]; then + log_warning "No configuration.yml found at $CONFIG_FILE — install Authelia first." + return 1 + fi + # The apex domain this Authelia instance already serves — read back from # its own session.cookies (same structure install_authelia()/ # add_authelia_domain() write), rather than asking again or assuming a # variable set earlier in this run is still in scope (this flow can be # reached standalone from the "already exists" menu with none of - # install_authelia()'s own locals ever having run this session). + # install_authelia()'s own locals ever having run this session). Used + # below only to suggest a domain default — _authelia_provision_oidc_client + # re-derives its own copy independently. local AUTHELIA_DOMAIN AUTHELIA_DOMAIN="$(awk '/^ cookies:$/{f=1; next} f && /domain:/{print $3; exit}' "$CONFIG_FILE")" if [ -z "$AUTHELIA_DOMAIN" ]; then @@ -1288,58 +1397,12 @@ _authelia_add_oidc_client() { prompt_yn " Require two-factor for ${APP_NAME} logins too? (y/n):" "y" _2fa [[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor" - log_info "Generating client secret..." - local _hash_out CLIENT_SECRET_PLAIN CLIENT_SECRET_HASH - _hash_out="$(docker run --rm authelia/authelia:4.39.20 \ - authelia crypto hash generate pbkdf2 --variant sha512 --random \ - --random.length 72 --random.charset rfc3986 2>/dev/null)" - CLIENT_SECRET_PLAIN="$(echo "$_hash_out" | sed -n 's/^Random Password: //p')" - CLIENT_SECRET_HASH="$(echo "$_hash_out" | sed -n 's/^Digest: //p')" - if [ -z "$CLIENT_SECRET_PLAIN" ] || [ -z "$CLIENT_SECRET_HASH" ]; then - log_warning "Couldn't generate the client secret automatically. Run manually, then add the" - log_warning "client to $CONFIG_FILE's identity_providers.oidc.clients by hand:" - echo " docker run --rm authelia/authelia:4.39.20 authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986" - return 1 - fi - - grep -q '^ clients: \[\]$' "$CONFIG_FILE" && sed -i 's/^ clients: \[\]$/ clients:/' "$CONFIG_FILE" - - local REDIRECT_URIS_YAML - REDIRECT_URIS_YAML="$(printf " - '%s'\n" "${REDIRECT_URIS[@]}")" - REDIRECT_URIS_YAML="${REDIRECT_URIS_YAML%$'\n'}" - - local CLIENT_BLOCK=" - client_id: '${CLIENT_ID}' - client_name: '${APP_NAME}' - client_secret: '${CLIENT_SECRET_HASH}' - public: false - authorization_policy: '${AUTH_POLICY}' - redirect_uris: -${REDIRECT_URIS_YAML} - scopes: - - 'openid' - - 'profile' - - 'email' - grant_types: - - 'authorization_code' - response_types: - - 'code' - response_modes: - - 'query' - userinfo_signed_response_alg: 'none'" - - awk -v block="$CLIENT_BLOCK" ' - { print } - /^ clients:$/ && !done { print block; done=1 } - ' "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" - chown 1000:1000 "$CONFIG_FILE" 2>/dev/null || true - local RESTART_AUTH="" prompt_yn " Restart Authelia to apply? (y/n):" "y" RESTART_AUTH - if [ "$RESTART_AUTH" = "y" ] || [ "$RESTART_AUTH" = "Y" ]; then - (cd "$AUTHELIA_DIR" && docker compose restart authelia 2>/dev/null) \ - && log_success "Authelia restarted" \ - || log_warning "Restart failed — check: docker compose logs authelia" - fi + + _authelia_provision_oidc_client "$APP_NAME" "$CLIENT_ID" "$AUTH_POLICY" "$RESTART_AUTH" "${REDIRECT_URIS[@]}" \ + || return 1 + local CLIENT_SECRET_PLAIN="$OIDC_CLIENT_SECRET_PLAIN" echo "" echo " ${APP_NAME} is registered. Paste these into its OpenID/SSO settings" diff --git a/services/gitea.sh b/services/gitea.sh index eec9f5f..c815714 100644 --- a/services/gitea.sh +++ b/services/gitea.sh @@ -159,7 +159,8 @@ _gitea_prompt_token() { # SQLite DB then fails with "attempt to write a readonly database" — the # directory holding the DB file is owned by root, not the UID 1000 process # trying to write it. Confirmed live. Chown everything else as normal in -# $DIR; leave data/ for the container to manage. +# $DIR; leave data/ (and the Actions runner's own runner-data/, same reason) +# for their respective containers to manage. _gitea_fix_ownership() { local _dir="$1" [ "$DRY_RUN" = true ] && return 0 @@ -167,11 +168,141 @@ _gitea_fix_ownership() { local _entry for _entry in "$_dir"/*; do [ -e "$_entry" ] || continue - [ "$(basename "$_entry")" = "data" ] && continue + case "$(basename "$_entry")" in + data|runner-data) continue ;; + esac chown -R "$ACTUAL_USER:$ACTUAL_USER" "$_entry" 2>/dev/null || true done } +# Offers to add "Sign in with Authelia" (OpenID Connect) to Gitea's own +# login page. This is a different thing from Caddy forward_auth, which this +# service deliberately skips (see the Caddy call below) since Gitea already +# has a solid built-in login — this adds Authelia as an *additional* OAuth2 +# login option on the same page, on top of that built-in login, not a +# replacement for it. Nothing about local admin login changes. +# +# Fully automated on both sides, matching how the rest of this installer +# avoids manual web wizards: registers Gitea as an OIDC client in Authelia +# (services/authelia.sh's _authelia_provision_oidc_client), then adds the +# resulting client as an authentication source in Gitea via its own CLI. If +# either side isn't available (Authelia not installed, or the Gitea CLI +# call fails for some reason — e.g. an older image without `admin auth +# add-oauth`), falls back to printing the values for a two-minute manual +# add in Gitea's Site Administration UI instead of losing the setup. +_gitea_offer_authelia_sso() { + local DIR="$1" + + [ -d "$DOCKER_DIR/authelia" ] || return 0 + declare -F _authelia_provision_oidc_client >/dev/null 2>&1 || return 0 + + echo "" + local USE_SSO="" + prompt_yn " Add \"Sign in with Authelia\" (OpenID Connect) to Gitea's login page? (y/n):" "n" USE_SSO + [[ "$USE_SSO" =~ ^[Yy]$ ]] || return 0 + + local _default_domain="" + [ -n "${SITE_DOMAIN:-}" ] && [ "$SITE_DOMAIN" != "example.com" ] && _default_domain="git.${SITE_DOMAIN}" + local GITEA_OIDC_DOMAIN="" + prompt_text " Domain Gitea is reachable at [${_default_domain:-required}]:" "$_default_domain" GITEA_OIDC_DOMAIN + if [ -z "$GITEA_OIDC_DOMAIN" ]; then + log_warning "No domain entered — skipping Authelia SSO for Gitea." + return 0 + fi + + local _2fa="" AUTH_POLICY="two_factor" + prompt_yn " Require two-factor for Gitea logins via Authelia too? (y/n):" "y" _2fa + [[ "$_2fa" =~ ^[Yy]$ ]] || AUTH_POLICY="one_factor" + + if ! _authelia_provision_oidc_client "Gitea" "gitea" "$AUTH_POLICY" "y" \ + "https://${GITEA_OIDC_DOMAIN}/user/oauth2/authelia/callback"; then + log_warning "Couldn't register Gitea as an OIDC client in Authelia — skipping SSO setup." + return 0 + fi + + local _discovery_url="https://auth.${OIDC_AUTHELIA_DOMAIN}/.well-known/openid-configuration" + log_info "Adding Authelia as an authentication source in Gitea..." + if docker exec -u git gitea gitea admin auth add-oauth \ + --name authelia --provider openidConnect \ + --key gitea --secret "$OIDC_CLIENT_SECRET_PLAIN" \ + --auto-discover-url "$_discovery_url" &>/dev/null; then + log_success "\"Sign in with Authelia\" added to Gitea's login page — local admin login still works too." + else + log_warning "Couldn't add the auth source automatically. Add it by hand:" + log_warning " Gitea -> Site Administration -> Authentication Sources -> Add Authentication Source" + log_warning " Type: OAuth2, Provider: OpenID Connect, Name: authelia" + log_warning " Client ID: gitea" + log_warning " Client Secret: $OIDC_CLIENT_SECRET_PLAIN" + log_warning " Discovery URL: $_discovery_url" + log_warning " (The Client Secret above is shown once — it isn't stored in plaintext anywhere.)" + fi +} + +# Offers to enable Gitea Actions (Gitea's own CI, largely GitHub-Actions- +# workflow-compatible) with a local runner — mainly useful as a fallback so +# .gitea/workflows/*.yml can still run something like a GitHub Actions build +# if GitHub itself is ever unreachable, since this Gitea is otherwise just a +# passive pull mirror. Off by default; opt-in on fresh installs and Update +# reruns alike (idempotent — a rerun after it's already set up just no-ops). +# +# The runner (gitea/act_runner) polls Gitea for jobs and needs the host's +# Docker socket to launch a fresh container per job — same pattern this repo +# already uses for portainer/watchtower/uptimekuma/beszel/traccar's autoheal, +# not something new to this file. Worth knowing: that's root-equivalent +# access to this host, standard for any CI runner, not unique to Gitea's. +_gitea_offer_actions_runner() { + local DIR="$1" + + grep -q '^ act_runner:$' "$DIR/docker-compose.yml" 2>/dev/null && return 0 + + echo "" + local USE_ACTIONS="" + prompt_yn " Enable Gitea Actions (CI) with a local runner — runs .gitea/workflows/*.yml the same way GitHub Actions runs .github/workflows/*.yml, useful as a fallback if GitHub is ever unreachable? (y/n):" "n" USE_ACTIONS + [[ "$USE_ACTIONS" =~ ^[Yy]$ ]] || return 0 + + if ! grep -q 'GITEA__actions__ENABLED' "$DIR/docker-compose.yml"; then + log_info "Enabling Gitea Actions..." + sed -i '/GITEA__security__INSTALL_LOCK=true/a\ - GITEA__actions__ENABLED=true\n - GITEA__actions__DEFAULT_ACTIONS_URL=github' "$DIR/docker-compose.yml" + (cd "$DIR" && docker compose up -d) \ + && log_success "Actions enabled — Gitea restarted to apply." \ + || { log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"; return 1; } + fi + + log_info "Generating a runner registration token..." + local RUNNER_TOKEN="" + RUNNER_TOKEN="$(docker exec -u git gitea gitea actions generate-runner-token 2>/dev/null | tail -1)" + if [[ -z "$RUNNER_TOKEN" ]]; then + log_warning "Couldn't generate a runner token automatically (older Gitea image?). Generate one by hand:" + log_warning " Gitea -> Site Administration -> Actions -> Runners -> Create new Runner" + log_warning " then add an act_runner container yourself using that token — see" + log_warning " https://docs.gitea.com/usage/actions/quickstart for the compose snippet." + return 1 + fi + + mkdir -p "$DIR/runner-data" + cat >> "$DIR/docker-compose.yml" << EOF + + act_runner: + image: gitea/act_runner:latest + container_name: gitea-runner + restart: unless-stopped + environment: + - GITEA_INSTANCE_URL=http://gitea:3000 + - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN} + - GITEA_RUNNER_NAME=gitea-runner + volumes: + - ./runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - gitea +EOF + + _gitea_fix_ownership "$DIR" + (cd "$DIR" && docker compose up -d act_runner) \ + && log_success "Actions runner started — .gitea/workflows/*.yml will now run automatically on push." \ + || log_warning "Runner failed to start — check: docker compose -f $DIR/docker-compose.yml logs act_runner" +} + # ── Own systemd timer, not gitea-github-sync.sh's built-in --install-timer ── # The vendor script's own timer installer always runs the script bare (no # --pull-only/--push-only), i.e. always both directions — there's no way to @@ -307,6 +438,8 @@ install_gitea() { echo "[DRY-RUN] Would ask sync direction (GitHub->Gitea / Gitea->GitHub / both) and whether" echo "[DRY-RUN] to install a systemd timer for automatic sync, or print manual instructions" echo "[DRY-RUN] Would offer to run a sync now (dry-run preview or for real), off-schedule" + echo "[DRY-RUN] Would offer \"Sign in with Authelia\" (OIDC) if Authelia is installed" + echo "[DRY-RUN] Would offer to enable Gitea Actions (CI) with a local act_runner container" echo "[DRY-RUN] Would write $DIR/README.md" return 0 fi @@ -333,6 +466,8 @@ install_gitea() { && log_success "Gitea refreshed and restarted." \ || log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs" _gitea_run_sync_direction_step "$DIR" + _gitea_offer_authelia_sso "$DIR" + _gitea_offer_actions_runner "$DIR" log_success "Existing .env (tokens) and web/SSH ports were left untouched." return 0 ;; @@ -494,9 +629,16 @@ ENV _gitea_run_sync_direction_step "$DIR" - # ── Caddy (Gitea has its own built-in login — no Authelia needed) ────── + # ── Caddy — no forward_auth gate here. Gitea has its own built-in login, + # unlike the no-auth-at-all apps elsewhere in this repo that need Caddy + # to gate them via Authelia. Optional "Sign in with Authelia" (OIDC) is + # offered separately below, as an addition to Gitea's own login, not a + # replacement requiring Caddy involvement. ───────────────────────────── configure_caddy_for_service "Gitea" "host.docker.internal:${WEB_PORT}" "git" + _gitea_offer_authelia_sso "$DIR" + _gitea_offer_actions_runner "$DIR" + write_readme "$DIR" << MD # Gitea @@ -528,6 +670,27 @@ Config (which repos, private/forks handling) lives at \`~/.config/gitea-github-sync/config\` — edit directly, or re-run \`bash gitea-github-sync.sh --init\` to redo it interactively. +## Sign in with Authelia (optional) + +If Authelia is installed, re-run \`sudo ./setup.sh gitea\` (Update mode is +fine — this doesn't touch tokens or anything else) and answer yes to +"Add \"Sign in with Authelia\"?" to add it as an extra OAuth2 login option +on Gitea's own login page. Local admin login keeps working exactly as +before — this is additive, not a replacement. Managed in Gitea under +Site Administration -> Authentication Sources (source name: \`authelia\`). + +## Gitea Actions (CI) — optional local runner + +Re-run \`sudo ./setup.sh gitea\` (Update mode is fine) and answer yes to +"Enable Gitea Actions?" to run \`.gitea/workflows/*.yml\` here the same way +GitHub Actions runs \`.github/workflows/*.yml\` — mainly useful as a fallback +so builds still work if GitHub is ever unreachable. Adds an \`act_runner\` +container (\`docker compose ps\` will show \`gitea-runner\`) that polls this +Gitea instance for jobs and launches a fresh container per job using this +host's own Docker socket — same pattern already used by this repo's +portainer/watchtower/uptimekuma services, not something new. Manage runners +under Site Administration -> Actions -> Runners. + ## Manage \`\`\`bash