Commit Graph
13 Commits
Author SHA1 Message Date
Claude be60f475c3 Add zero-click Authelia login to Gitea and Uptime Kuma
Follow-up to Frigate's Authelia integration: both of these can also skip
their own login entirely once Authelia is doing the gating, each with a
different trust model appropriate to what the app actually supports.

- gitea: new _gitea_offer_reverse_proxy_auth(), a second Authelia
  integration alongside the existing OIDC "Sign in with Authelia" button.
  Enables Gitea's own ENABLE_REVERSE_PROXY_AUTHENTICATION so it auto-logs
  in from a trusted Remote-User header — no click, no separate Gitea
  session to expire on its own. Trust is IP-range based
  (REVERSE_PROXY_TRUSTED_PROXIES), computed from caddy_net's real subnet
  the same way ufw_allow_from_caddy_net does; refuses to enable the
  feature at all if that can't be determined rather than fall back to a
  permissive default — Gitea's own Docker image has shipped an unscoped
  default before (GHSA-f75j-4cw6-rmx4, any IP could impersonate any user).
  Rewires Gitea onto caddy_net and re-points Caddy at gitea:3000, since it
  previously only reached Caddy via its published host port. Gitea's own
  login stays available as a fallback, so unlike Frigate there's no
  "native login off with nothing gating it" state to guard against.

- uptimekuma: sets DISABLE_AUTH=true only once Caddy's "import authelia"
  gate is confirmed in front of it. Uptime Kuma already joined caddy_net
  unconditionally, so this only needed the env var plus moving the
  Authelia-gated Caddy call earlier (before docker-compose.yml is
  written); the existing unconditional call at the end now only runs as a
  fallback when the Authelia path wasn't used or wasn't completed. Kuma's
  DISABLE_AUTH has no IP-scoping or secret check left once set — the
  strictest of the three to get the ordering right on, since a mistake
  here means wide open, not just spoofable.

Verified with a local test harness (fake Authelia/Caddy/docker-network
state): both the happy path and the "Caddy declined" safety fallback
produce the expected docker-compose.yml/.env/Caddyfile output for each
service, and Gitea's subnet-detection refusal + idempotent-rerun guard
were exercised directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpKTLpwAgZNooTacWeQLuc
2026-08-21 21:07:02 +00:00
Claude fc3b85f970 authelia: stop hardcoding "auth." as the portal subdomain for OIDC discovery
_authelia_provision_oidc_client() gained a new out-param,
OIDC_AUTHELIA_PORTAL_URL, read back from the instance's own
configuration.yml (session.cookies[].authelia_url) — the actual source of
truth for where the portal lives — instead of every caller separately
assuming "https://auth.$domain".

install_authelia() and add_authelia_domain() both still default new
instances to "auth." as before (unchanged), but that's just a default, not
a guarantee: it's plain text in configuration.yml and gets hand-edited on
some boxes (e.g. a dedicated instance renamed to "authelia." to avoid
colliding with another instance's "auth." on a different machine). Mealie,
ActualBudget, and Gitea's native-OIDC wiring all independently hardcoded
"auth." when building their discovery URL, so a renamed portal silently
produced a discovery URL pointing at a host that doesn't serve Authelia —
surfacing as an opaque 500 during the OIDC token exchange with no useful
client-side error.

Verified the new awk parse against both a default ("auth.") and a renamed
("authelia.") cookies block before trusting it.
2026-08-20 20:17:58 +00:00
Claude accfd7a5bb authelia/mealie/actualbudget/gitea: no more silent no-op reruns
Confirmed live: re-running ActualBudget's Update path produced zero
output for the Authelia SSO step — no prompt, no message, straight back
to the shell. Root cause: the idempotency guards in
_actualbudget_offer_authelia_oidc / _mealie_offer_authelia_oidc /
_gitea_offer_actions_runner were plain `grep -q ... && return 0` — silent
by construction. Indistinguishable from the step not running at all,
which is exactly what it looked like.

ActualBudget and Mealie's OIDC offers now explain what they found and
ask whether to reconfigure (registers a fresh Authelia client + secret,
clearing the old env vars first) instead of silently bailing. Gitea's
Actions-runner offer explains what it found and how to check its status
(reconfiguring that one means editing a docker-compose service block,
not just a couple of env vars, so it just informs rather than offering
to redo it).

Also: _authelia_scope_access now shows existing Authelia usernames as a
numbered list before asking who should have access — picking by number
works alongside typing new names directly (mix freely, e.g. "1 3
newperson"), rather than requiring exact usernames typed from memory
with no reference and no protection against a typo silently creating a
duplicate account.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-20 18:49:43 +00:00
Claude ab6b554cd8 authelia: add reusable per-service access scoping (universal vs specific users)
Every domain with an access_control rule was reachable by any Authelia
user by default (the existing catch-all *.${AUTHELIA_DOMAIN} rule) — no
way to restrict a specific service to a subset of users without hand-
editing configuration.yml and users.yml directly.

_authelia_scope_access(SERVICE_ID, DOMAIN) is a new generic, reusable
helper: call it after any service finishes being protected by Authelia
(forward_auth gate or native OIDC alike — it only cares about the domain).
Offers universal vs. specific-users access; if scoped, creates a
"<service_id>-only" group, adds every listed username to it (creating
accounts on the fly for names that don't exist yet, via the new
_authelia_create_user_noninteractive — a non-interactive sibling to
add_authelia_user, same extraction pattern already used for
_authelia_provision_oidc_client), and inserts an allow+deny rule pair
above the general catch-all. Idempotent on rerun.

_authelia_report_access_scope() (new menu option 6) is the read side —
lists who has universal vs. service-scoped access, and offers to promote
a scoped user back to universal by removing their "-only" group
membership.

services/gitea.sh's _gitea_offer_authelia_sso() is the reference
integration, calling _authelia_scope_access after successfully wiring up
Gitea's OIDC login. The other services with a plain "Protect X with
Authelia?" prompt (magicmirror, wolf-pair, js99er, drum-rhythm-game,
iopaint, paintplus, stirling-pdf, wolf) are natural follow-ups once this
is confirmed working live — each just needs one added call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-19 18:29:36 +00:00
Claude d3d2a411b0 gitea: add optional Gitea Actions (CI) with a local runner
Gitea Actions is Gitea's own CI, largely GitHub-Actions-workflow-compatible
(.gitea/workflows/*.yml). Off by default; this Gitea install is otherwise
just a passive GitHub pull mirror, so the main value here is resilience —
.gitea/workflows/*.yml can still run something like a GitHub Actions build
if GitHub itself is ever unreachable.

_gitea_offer_actions_runner(), offered on fresh installs and Update reruns
(idempotent — no-ops if already set up):
  - Enables GITEA__actions__ENABLED / DEFAULT_ACTIONS_URL in the compose
    file's environment, restarts to apply
  - Generates a runner registration token via `gitea actions
    generate-runner-token`
  - Appends an act_runner service to the same docker-compose.yml, using
    the host's Docker socket to launch a fresh container per job — the
    same pattern this repo already uses for portainer/watchtower/
    uptimekuma/beszel/traccar's autoheal
  - Falls back to printing manual setup instructions if token generation
    fails, rather than losing the attempt silently

_gitea_fix_ownership()'s data/-exclusion (added when we fixed the earlier
SQLite readonly-database bug) now also skips runner-data/, so a future
reinstall/update doesn't clobber the runner's own state the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 21:32:35 +00:00
Claude deb38b3b1d gitea: add optional "Sign in with Authelia" (OIDC) to the login page
Gitea has its own built-in login, so it was never wired into the
forward_auth/Caddy pattern the rest of this repo uses to gate apps with
no auth of their own — that's still correct and unchanged. But Gitea
also supports adding an OAuth2/OpenID Connect authentication source
natively, and Authelia can act as an OIDC provider — a genuinely
different, additive integration: an extra "Sign in with Authelia" button
on Gitea's own login page, alongside local login, not a Caddy-level gate.

Refactored services/authelia.sh's _authelia_add_oidc_client() to split
out its non-interactive core as _authelia_provision_oidc_client() — same
behavior for the existing ActualBudget/Vaultwarden/Immich/custom-app menu
flow, but now callable directly by other services with explicit args
instead of walking a human through the menu, returning the plaintext
secret and Authelia's domain via out-params.

services/gitea.sh's new _gitea_offer_authelia_sso() uses that to fully
automate both sides when accepted: registers Gitea as an OIDC client in
Authelia, 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 how this installer already avoids manual wizards for the admin
account/token. Falls back to printing the values for a manual add if the
Gitea-side CLI call fails. Offered on fresh installs and on Update
reruns (default no, so a plain Update stays silent), so it can be added
later without a full reinstall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 18:54:41 +00:00
Claude da6a6ab59a gitea: stop clobbering the live data/ directory's ownership on reinstall
Root cause of "attempt to write a readonly database (1544)" on repo
creation: Gitea's container always runs internally as UID 1000
(USER_UID/USER_GID are fixed in docker-compose.yml, independent of
whoever's running this installer) — the image chowns /data to that UID
itself at startup. install_gitea()'s three ensure_docker_dir_ownership
calls recursively chown the *entire* service directory, data/ included,
to $ACTUAL_USER. On a box where the installer runs as root directly
(ACTUAL_USER=root), that resets a live data/ back to UID 0. If the
container doesn't happen to restart right after — confirmed live: Update
mode against an already-running container just no-ops instead of
restarting — nothing ever re-fixes it, and every subsequent write to
Gitea's own SQLite DB fails.

Added _gitea_fix_ownership(), which chowns everything in the service
directory except data/, and swapped it in at all three call sites. The
container continues to own data/'s permissions exclusively, as it always
has on first boot; this installer no longer fights it on every rerun.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 18:02:53 +00:00
Claude 67ee2fc28b gitea: don't leave the admin account locked behind a password-change wall
Root cause of the "Failed to reach Gitea API" / 403 errors on every retry:
`gitea admin user change-password` (used in the already-exists branch to
sync the account's password to what the user just entered) defaults to
setting must_change_password=true, unlike `user create` which was already
pinned to --must-change-password=false. Once set, Gitea rejects every API
call — including the sync script's own token-authenticated calls — with
403 "You must change your password", even though the token itself and
GITEA_URL were both completely correct. Confirmed live via a direct curl
against /api/v1/user.

Pin the same flag on change-password that create already used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:41:02 +00:00
Claude f0ef647f60 gitea: retry token prompts instead of accepting one empty answer
Live symptom: pasting the GitHub PAT over SSH showed the token text
landing on the terminal *after* "No GitHub token entered" had already
printed — the prompt's read() returned empty a beat before the paste
actually arrived (a paste/Enter race that isn't specific to this box,
just common over higher-latency SSH sessions). A single empty answer
was treated as "user has no token" and the install moved on silently.

Both token prompts (GitHub token, and the Gitea-token manual fallback)
now retry up to 3 times interactively before giving up, and strip
whitespace from what was captured in case the paste carried a stray
leading/trailing newline. Unattended installs still take one shot, same
as before, since nobody's there to retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:27:56 +00:00
Claude dcf643cde4 gitea: offer to run the sync immediately after configuring it
The sync direction step only ever set up the timer (or printed manual
instructions) — there was no way to actually confirm tokens/config work
without waiting for the first scheduled run or invoking the script by
hand afterward. Add a post-configure prompt: dry-run preview (--list),
run for real right now, or skip. Defaults to dry-run interactively;
defaults to skip under UNATTENDED so a headless install with no GitHub
token configured doesn't spam preflight errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:22:27 +00:00
Claude def70ab4e2 gitea: fix token-name collision that swallowed the GitHub token prompt
generate-access-token used a fixed --token-name "sync", which Gitea
rejects on a second call for the same user (e.g. a retry against an
already-existing admin account, now common after the readiness-wait
fix). The failure was silent: it fell through to a manually-labeled
"Paste the Gitea token here" prompt appearing immediately before the
real "GitHub token:" prompt, so a pasted GitHub PAT could land on the
wrong prompt and leave GITHUB_TOKEN empty with no clear reason why.

Token name now includes a timestamp so it's always unique, and the
fallback prompt is relabeled to make clear it wants a Gitea token, not
the GitHub one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 17:19:01 +00:00
Claude 240fc78244 gitea: prompt for admin username/password, make readiness wait resilient
Previously the admin account was silently auto-generated (username =
ACTUAL_USER, random password) and only created after a fixed 60s
readiness probe — a slow first boot (SQLite init on a slower disk) timed
the whole install out with no account ever created, leaving the user to
create one by hand with a raw docker exec.

Now the install prompts for admin username/password up front, then folds
account creation into the same retry loop used to detect readiness (up to
2 minutes), so a slow-but-eventually-successful boot no longer dead-ends
the install. A retry against a partially-completed prior run (account
already exists) is treated as success and syncs the password instead of
failing outright.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
2026-08-17 16:55:50 +00:00
Claude eca4fc4d4e Add gitea: standalone self-hosted Git server with GitHub mirror sync
Gitea previously only existed bundled inside the full ai-stack service
(Ollama/ComfyUI/InvokeAI/etc. all together) — no way to get just a git
server without the rest of that heavy stack. This adds it as its own
lightweight service, reusing the vendored gitea-github-sync.sh but not
any of ai-stack's other components.

- Auto-creates a Gitea admin account and API token via the container's
  own CLI (no manual web setup wizard).
- Asks GitHub token, sync direction (GitHub->Gitea / Gitea->GitHub /
  both), and whether to install a systemd timer for automatic sync —
  prints manual commands instead if declined.
- Own systemd unit for the timer rather than the vendor script's
  built-in --install-timer, since that always runs both directions
  with no way to pin a single direction.
2026-08-17 15:26:53 +00:00