Compare commits
6 Commits
7e935c31d1
...
7213460c10
| Author | SHA1 | Date | |
|---|---|---|---|
| 7213460c10 | |||
| 68c2866a28 | |||
| 446fd32c81 | |||
| 97d1ba97a9 | |||
| 52c065b753 | |||
| 2ab0a4e837 |
@@ -246,7 +246,7 @@ Checklist:
|
||||
publishes the plugin zip is the obvious first pipeline
|
||||
- risk: the runner's Docker socket is root on the host for anyone who can push a workflow
|
||||
|
||||
### Phase 10 — Gitea login via Authentik (OIDC) (compose done; Authentik + Gitea UI steps pending)
|
||||
### Phase 10 — Gitea login via Authentik (OIDC) ✅ live, verified
|
||||
Same pattern as Jellyfin: Authentik is the identity, Gitea trusts it.
|
||||
- [ ] Authentik → Applications → Create with provider → OAuth2/OpenID: name `gitea`, redirect
|
||||
`https://git.HOST/user/oauth2/authentik/callback`, authorization flow implicit-consent, scopes
|
||||
@@ -258,7 +258,8 @@ Same pattern as Jellyfin: Authentik is the identity, Gitea trusts it.
|
||||
- [x] Gitea env: `DISABLE_REGISTRATION=false` + `ALLOW_ONLY_EXTERNAL_REGISTRATION=true` (local self-signup stays
|
||||
off), `ENABLE_AUTO_REGISTRATION`, `ACCOUNT_LINKING=auto`, `USERNAME=nickname`, `UPDATE_AVATAR`
|
||||
- [ ] optional: hide the password form on the login page (custom template) so Discord is the only visible way in
|
||||
- [ ] Authentik group `git-users` (from a Discord role) bound on the `Gitea` application (+ `admins`); only they can log in
|
||||
- [ ] Authentik groups `git-users` and `git-admins` bound on the `Gitea` application; Gitea admin claim = `git-admins`;
|
||||
nesting `authentik Admins` → `admins` → `git-admins` so admins inherit it
|
||||
- [ ] test with a non-admin Discord user; confirm admin mapping via `admins` group
|
||||
|
||||
### Phase 11 — MediaMTX auth through Authentik (no stream keys anywhere) ✅ live
|
||||
|
||||
22
README.md
22
README.md
@@ -80,8 +80,14 @@ Everything else is dropped and a trickle is logged (`journalctl -k | grep vg-dro
|
||||
Jellyfin (8096), copyparty, the Syncthing UI and dumpd bind to 127.0.0.1 only and are reached via Caddy
|
||||
or an ssh tunnel, so they are deliberately not opened. MariaDB runs on the host network (not a Docker
|
||||
published port) precisely so the firewall can govern it.
|
||||
Syncthing's admin UI is proxied at `https://HOST/sync/` behind Syncthing's own GUI login (set it in
|
||||
Actions → Settings → GUI before relying on this). The ssh tunnel to 8384 still works as a fallback.
|
||||
Syncthing's admin UI is proxied at `https://HOST/sync/` behind the Authentik gate + group `media-admins`; its
|
||||
own GUI login is disabled (no user/password, `insecureAdminAccess=true`) so Discord is the only login:
|
||||
|
||||
docker exec syncthing syncthing cli --home /var/syncthing/config config gui insecure-admin-access set true
|
||||
docker exec syncthing syncthing cli --home /var/syncthing/config config gui user set ""
|
||||
docker exec syncthing syncthing cli --home /var/syncthing/config config gui password set ""
|
||||
|
||||
The ssh tunnel to 8384 still works as an admin fallback (no login at all — ssh is the gate there).
|
||||
|
||||
To change rules: edit `/etc/nftables.conf`, then `sudo nft -f /etc/nftables.conf` or
|
||||
`sudo systemctl reload nftables`. **Never `systemctl stop|restart nftables`** — Debian's unit flushes
|
||||
@@ -212,11 +218,13 @@ Keep repo write access to trusted people.
|
||||
2. Gitea → Site Administration → Identity & Access → Authentication Sources → Add: type **OAuth2**, name
|
||||
**`authentik`** (must match the callback path), provider **OpenID Connect**, client ID/secret, OpenID Connect
|
||||
Auto Discovery URL `https://auth.HOST/application/o/gitea/.well-known/openid-configuration`, tick "Skip local
|
||||
2FA", claim name for groups `groups`, administrator group claim value `admins`. Save.
|
||||
2FA", claim name for groups `groups`, administrator group claim value **`git-admins`**. Save.
|
||||
Authentik group nesting makes the admins inherit it: `authentik Admins` → parent `admins` → parent `git-admins`
|
||||
(members of a child group count as members of the parent, in the `groups` claim and in Caddy's header checks).
|
||||
3. compose (already set): registration only via external sources, auto-registration + account linking on,
|
||||
username from `preferred_username`. A first Discord login creates the Gitea user with no form.
|
||||
4. Access = Authentik group **`git-users`**: Directory → Groups → create it (add a Discord role to it in the
|
||||
roles→groups table); Applications → `Gitea` → Policy / Group / User Bindings → bind `git-users` (and `admins`).
|
||||
roles→groups table); Applications → `Gitea` → Policy / Group / User Bindings → bind `git-users` and `git-admins`.
|
||||
Non-members are refused by Authentik before Gitea sees them.
|
||||
5. Test in a private window: https://git.HOST/user/login → "Sign in with authentik" → Discord → logged in.
|
||||
Members of `admins` become Gitea site admins on login. The local admin account stays as a fallback.
|
||||
@@ -262,11 +270,13 @@ Design: **Authentik authenticates once for the whole domain; Caddy authorizes pe
|
||||
`discord-roles-to-groups` from `authentik/discord-roles-to-groups.py` (fill in the server ID and the
|
||||
role-id → group table). Bind it to `default-source-enrollment` **and** `default-source-authentication`
|
||||
(engine mode `all`, after `discord-guild-member`). From then on giving someone a Discord role gives them
|
||||
the matching access on next login; removing it removes it.
|
||||
the matching access on next login; removing it removes it. Memberships added by hand in Authentik are left
|
||||
alone: the policy only revokes what it granted itself (tracked in the user attribute `discord_role_groups`).
|
||||
copyparty additionally requires a header with a secret name that only Caddy adds (`idp-h-key`,
|
||||
`COPYPARTY_IDP_KEY` in `.env`), so identity headers cannot be forged even if copyparty were reached directly.
|
||||
Logout: copyparty's logout link, the live portal's "Sign out" and the dock's "sign out" go through
|
||||
`/outpost.goauthentik.io/sign_out`. Set the provider's **Invalidation flow** to
|
||||
`/outpost.goauthentik.io/sign_out`; `https://<any gated host>/logout` does the same from anywhere
|
||||
(e.g. from the Syncthing UI, which has no logout of its own). Set the provider's **Invalidation flow** to
|
||||
`default-provider-invalidation-flow` so that also ends the Authentik login: every logout then forces a full
|
||||
Discord re-login on the next visit (and picks up group changes). Discord itself may approve silently if
|
||||
you are still logged into Discord in that browser.
|
||||
|
||||
12
STATUS.md
12
STATUS.md
@@ -33,10 +33,10 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|
||||
| Anyone in the Discord server | Watch recordings and clips by streamer and season | Jellyfin `https://watch.…/` | Discord (Authentik) |
|
||||
| Anyone in the Discord server | Watch live: all active streams in dockable windows, audio follows focus, per-window mute/volume | Live portal `https://live.…/` | Discord (Authentik, domain-level) — **live, shows a real stream** |
|
||||
| Streamer (Windows) | **Go Live** from the browser: pick the WarDogs window (+audio on Windows Chrome 141+; Linux/Mac video only), streams as `<user>/HelmetCam-<date>`; no OBS needed | Live portal → Go Live | Discord; key fetched for the logged-in user — **verified end to end** (publish → `.live` → sweep → title folder) |
|
||||
| Devs | Host code, review, CI | Gitea `https://git.…/`, ssh 2222 | **local account** (Discord later) |
|
||||
| Devs | Host code, review, CI | Gitea `https://git.…/`, ssh 2222 | Discord (Authentik), group `git-users`; `git-admins` = site admin |
|
||||
| Bot dev | Push code, restart container, read logs; web UI at `https://bot.…/` | Gitea + compose; `bot/AGENT.md` | web UI behind Discord (Authentik), once its provider exists |
|
||||
| Admin | Hostnames, bot token/guild, webhook; streamers/editors are Discord roles → Authentik groups | `./configure.py` | ssh |
|
||||
| Admin | Pair a gamer's device, approve their folder | Syncthing UI `https://ovh1…/sync/` | Discord + group `media-admins`, then Syncthing's GUI password |
|
||||
| Admin | Pair a gamer's device, approve their folder | Syncthing UI `https://ovh1…/sync/` | Discord + group `media-admins` (Syncthing's own login disabled) |
|
||||
| Admin | Groups, who may log in, app access | Authentik `https://auth.…/` | Discord or akadmin |
|
||||
| Admin | Install/upgrade the Jellyfin plugin | `./installJellyfinPlugin.sh` | ssh |
|
||||
| Admin | Register the CI runner | `./registerGiteaRunner.sh` | ssh |
|
||||
@@ -49,7 +49,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|
||||
| copyparty | browse/scrub/download/finals upload | behind Authentik, group permissions (`editors`, `media-admins`), Discord-only, no WebDAV | live |
|
||||
| dumpd + dock | dump, stream tokens, MediaMTX auth hook, `/time`; OBS dock with timecode-band setup | identity from Authentik headers | live |
|
||||
| Timecode | `bin/vgstrip.py` decoder, `tmcd` + sidecar in the sweep, `/dock/clock/` strip page, Go Live band composite, portal crop | server clock is the reference | **live, verified** (OBS: frame-accurate, 0 gaps) |
|
||||
| Syncthing | clip intake, UI at `/sync/` | UI behind Authentik + `media-admins` | live |
|
||||
| Syncthing | clip intake, UI at `/sync/` | UI behind Authentik + `media-admins`, own GUI auth off | live |
|
||||
| normalize sweep | editing standard, one copy per clip | live |
|
||||
| nftables | host firewall (22, 80/443, 1935, 2222, 3306, 8890/udp, 22000) | live |
|
||||
| Caddy | TLS + routing for all hostnames | live |
|
||||
@@ -57,7 +57,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|
||||
| Jellyfin | watch page, SSO via Authentik, direct play only | live |
|
||||
| Vanguard Metadata plugin | seasons by tag, Discord names/avatars/events, feed, playback policy | **0.1.3 built** (flat `<Title>__<ts>` names, policy task, feed wording) — install pending; server has 0.1.0 or 0.1.2 |
|
||||
| MariaDB | databases for the bot and Gitea; public :3306, root local-only | live |
|
||||
| Gitea | git hosting on MariaDB, Actions enabled; holds this repo | live, local admin; Discord login via Authentik OIDC **compose ready, UI steps pending** |
|
||||
| Gitea | git hosting on MariaDB, Actions enabled; holds this repo | live; Discord login via Authentik OIDC **verified**; access = `git-users`/`git-admins` groups; local admin as fallback |
|
||||
| act_runner | CI jobs in Docker | **not registered** |
|
||||
| discord-bot | Python runtime, idles until `bot/app/main.py` exists | live, no code yet |
|
||||
| `bot.` host | bot web UI behind Authentik forward-auth; Caddy requires group `bot-users` (from a Discord role) | Caddy block live; **`bot` CNAME still points at the wrong IP; roles→groups policy pending** |
|
||||
@@ -65,11 +65,9 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea
|
||||
|
||||
## Unfinished, in rough priority
|
||||
|
||||
0. `docker compose restart mediamtx` at a quiet moment so recording filenames gain microseconds (the arrival-time fallback); everything else in phase 12 is live.
|
||||
1. Authentik: `discord-roles-to-groups` policy so `streamers`/`editors`/`media-admins`/`bot-users` follow Discord roles (`bot-users` group needed for the bot host). Set the provider's Invalidation flow to `default-provider-invalidation-flow` so every logout forces a full re-login.
|
||||
1. Roles→groups policy: bound and updated (revokes only its own grants); will be proven by the next natural Discord logins (`/whoami` should show `bot-users`, `git-admins`). `akadmin` remains the recovery login if a policy misfires.
|
||||
2. Confirm plugin 0.1.3 under Jellyfin → Plugins and the Gitea runner registered.
|
||||
3. Portal viewer during a real stream: corner chip `webrtc`; Windows Go Live with WarDogs audio (first Windows streamer).
|
||||
4. Gitea login via Authentik: deploy the compose change, then the Authentik provider + Gitea auth source (README 5e).
|
||||
5. Housekeeping: pin `AUTHENTIK_TAG`, rotate the Discord client secret seen in a screenshot, nightly Postgres dump, Caddyfile formatting warning.
|
||||
6. Medal filename pattern in the plugin (need a sample filename).
|
||||
7. Home archive puller on the home box.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# guilds guilds.members.read ). Existing users re-consent once at Discord.
|
||||
# The Authentik groups must already exist (Directory -> Groups). Role IDs: Discord Developer Mode ->
|
||||
# Server Settings -> Roles -> right-click a role -> Copy Role ID.
|
||||
# Manual memberships are safe: the policy only removes groups it granted itself (user attribute discord_role_groups).
|
||||
|
||||
GUILD_ID = "1387203044084551690" # <- your Discord server ID
|
||||
ROLE_TO_GROUP = { # <- Discord role id : Authentik group name
|
||||
@@ -40,17 +41,29 @@ wanted = {g for r, g in ROLE_TO_GROUP.items() if r in roles}
|
||||
managed = set(ROLE_TO_GROUP.values())
|
||||
|
||||
# Authentication flow: the user exists -> edit memberships directly.
|
||||
# Only memberships THIS policy granted earlier are ever removed (tracked in user.attributes["discord_role_groups"]),
|
||||
# so a group someone was added to by hand in Authentik survives role changes and re-logins.
|
||||
user = context.get("pending_user") or (request.user if request.user.is_authenticated else None)
|
||||
if user and user.pk:
|
||||
attrs = user.attributes or {}
|
||||
granted = set(attrs.get("discord_role_groups", []))
|
||||
for group in Group.objects.filter(name__in=managed):
|
||||
if group.name in wanted:
|
||||
group.users.add(user)
|
||||
else:
|
||||
group.users.remove(user)
|
||||
granted.add(group.name)
|
||||
elif group.name in granted:
|
||||
group.users.remove(user) # role gone -> take back what the role had given
|
||||
granted.discard(group.name)
|
||||
attrs["discord_role_groups"] = sorted(granted)
|
||||
user.attributes = attrs
|
||||
user.save()
|
||||
return True
|
||||
|
||||
# Enrollment flow: the user is created later by the user-write stage; hand it the groups to attach.
|
||||
# Enrollment flow: the user is created later by the user-write stage; hand it the groups to attach and
|
||||
# record them as role-granted so a later role removal can revoke exactly these.
|
||||
plan = context.get("flow_plan")
|
||||
if plan is not None:
|
||||
plan.context["groups"] = list(Group.objects.filter(name__in=wanted))
|
||||
prompt = plan.context.setdefault("prompt_data", {})
|
||||
prompt.setdefault("attributes", {})["discord_role_groups"] = sorted(wanted)
|
||||
return True
|
||||
|
||||
@@ -127,7 +127,9 @@ def ask_name(kind, taken):
|
||||
return n
|
||||
|
||||
# ------------------------------------------------------------------ writers
|
||||
AUTHENTIK_FORWARD_AUTH = """ # Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the
|
||||
AUTHENTIK_FORWARD_AUTH = """ # /logout on every gated host = sign out of the Discord login (domain-wide session)
|
||||
redir /logout /outpost.goauthentik.io/sign_out 302
|
||||
# Authentik forward-auth (embedded outpost). Needs a Proxy Provider in Authentik — domain-level for the
|
||||
# whole domain (recommended) or single-application for this host — assigned to the embedded outpost.
|
||||
# Until then: 4xx, never open. Per-host group requirements are enforced below from X-Authentik-Groups.
|
||||
reverse_proxy /outpost.goauthentik.io/* 127.0.0.1:9000
|
||||
@@ -263,7 +265,7 @@ name: {{http.request.header.X-Authentik-Name}}"
|
||||
reverse_proxy 127.0.0.1:8080 # server clock for the timecode strip
|
||||
}}
|
||||
|
||||
# Syncthing admin UI: Authentik + group {sync_group or 'any'}; Syncthing's own GUI login stays as a second layer
|
||||
# Syncthing admin UI: Authentik + group {sync_group or 'any'} (Syncthing's own GUI login is disabled)
|
||||
{sync_gate} redir /sync /sync/ 308
|
||||
handle_path /sync/* {{
|
||||
reverse_proxy 127.0.0.1:8384 {{
|
||||
|
||||
Reference in New Issue
Block a user