Rebuild gaps closed: Authentik bootstrap creds, Gitea install lock + admin user from .env, Syncthing GUI-auth-off fixup, plugin ensured on apply, portal built by install.sh; README rebuild section with the honest manual remainder
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
26
README.md
26
README.md
@@ -447,6 +447,32 @@ Turning it on: `DEPLOY_AUTO=1` in `.env`, `./configure.py --apply`, and
|
|||||||
CI: `.gitea/draft-workflows/ci.yml` (inert until moved to `.gitea/workflows/`) runs `check.sh` and the plugin
|
CI: `.gitea/draft-workflows/ci.yml` (inert until moved to `.gitea/workflows/`) runs `check.sh` and the plugin
|
||||||
tests/build on the Actions runner. The runner never deploys — it must not touch the host.
|
tests/build on the Actions runner. The runner never deploys — it must not touch the host.
|
||||||
|
|
||||||
|
## Rebuild from scratch (what is and isn't automated)
|
||||||
|
|
||||||
|
Server side, scripted:
|
||||||
|
|
||||||
|
git clone ssh://git@git.HOST:2222/techy/Media-Server-Setup.git ~/vanguard-stream # or scp the folder
|
||||||
|
cd ~/vanguard-stream && sudo bash install.sh # packages, docker, folders, host scripts, timers, firewall, portal build
|
||||||
|
./configure.py --full # hostnames, secrets, configs, containers, post-start fixups
|
||||||
|
|
||||||
|
Restore first if rebuilding a known box: `.env` (all secrets — without it every password is regenerated and
|
||||||
|
the databases won't match), `/srv/appdata` (Authentik DB, Jellyfin, Gitea, MariaDB, Syncthing config),
|
||||||
|
`/srv/media` (the footage), `/etc/vanguard/clip-quotas.json` if used. With those restored, nothing below is
|
||||||
|
needed except DNS.
|
||||||
|
|
||||||
|
Post-start fixups `configure.py` performs on every apply (idempotent): Syncthing GUI auth off, Gitea admin
|
||||||
|
user created from `.env` (`GITEA_ADMIN_USER` / `GITEA_ADMIN_PASSWORD`; the install page is locked), Jellyfin plugin
|
||||||
|
installed from the committed zip. Authentik's `akadmin` password is set on first start from
|
||||||
|
`AUTHENTIK_BOOTSTRAP_PASSWORD` in `.env` (no initial-setup page).
|
||||||
|
|
||||||
|
Still manual on a truly fresh box (not in the repo; documented in §5c–5g):
|
||||||
|
- Authentik: Discord source, identification-stage source, enrollment user type, groups, the two policies,
|
||||||
|
the domain-level proxy provider + outpost host, the Jellyfin and Gitea OIDC providers, application bindings
|
||||||
|
- Jellyfin: first-run wizard, libraries (Shows + Vanguard provider), SSO plugin + provider, branding
|
||||||
|
- Gitea: authentication source (authentik), deploy key, webhook; `./registerGiteaRunner.sh`
|
||||||
|
- Discord developer portal: application, redirect URIs, bot; DNS records
|
||||||
|
A `.env`-driven Authentik blueprint would remove the first line (planned).
|
||||||
|
|
||||||
## Layout on the server
|
## Layout on the server
|
||||||
|
|
||||||
/srv/media/streams/<user>/<Title>/.live/<ts>.mp4 recording in progress (hidden from Jellyfin/copyparty)
|
/srv/media/streams/<user>/<Title>/.live/<ts>.mp4 recording in progress (hidden from Jellyfin/copyparty)
|
||||||
|
|||||||
47
configure.py
47
configure.py
@@ -318,6 +318,10 @@ def write_env(watch_host, discord_token=None, discord_guild=None, git_host="", b
|
|||||||
env.setdefault("APEX_REDIRECT", "") # where the bare domain sends people, e.g. https://watch.HOST/
|
env.setdefault("APEX_REDIRECT", "") # where the bare domain sends people, e.g. https://watch.HOST/
|
||||||
env.setdefault("SYNC_REQUIRE_GROUP", "media-admins") # Caddy gate for /sync on the main host
|
env.setdefault("SYNC_REQUIRE_GROUP", "media-admins") # Caddy gate for /sync on the main host
|
||||||
env.setdefault("STREAM_TOKEN_SECRET", secrets.token_urlsafe(48)) # signs publish tokens (dumpd)
|
env.setdefault("STREAM_TOKEN_SECRET", secrets.token_urlsafe(48)) # signs publish tokens (dumpd)
|
||||||
|
env.setdefault("AUTHENTIK_BOOTSTRAP_PASSWORD", "".join(c for c in secrets.token_urlsafe(30) if c.isalnum())[:20]) # akadmin on first start
|
||||||
|
env.setdefault("AUTHENTIK_BOOTSTRAP_TOKEN", secrets.token_urlsafe(40))
|
||||||
|
env.setdefault("GITEA_ADMIN_USER", "vanguard")
|
||||||
|
env.setdefault("GITEA_ADMIN_PASSWORD", "".join(c for c in secrets.token_urlsafe(30) if c.isalnum())[:20])
|
||||||
env.setdefault("DEPLOY_AUTO", "0") # 1 = Gitea push webhook runs deploy.sh (off until trusted)
|
env.setdefault("DEPLOY_AUTO", "0") # 1 = Gitea push webhook runs deploy.sh (off until trusted)
|
||||||
env.setdefault("DEPLOY_WEBHOOK_SECRET", secrets.token_urlsafe(32)) # Gitea webhook secret (HMAC)
|
env.setdefault("DEPLOY_WEBHOOK_SECRET", secrets.token_urlsafe(32)) # Gitea webhook secret (HMAC)
|
||||||
env.setdefault("DEPLOY_BRANCH", "main")
|
env.setdefault("DEPLOY_BRANCH", "main")
|
||||||
@@ -507,18 +511,57 @@ def apply(domain, admin_pw, streamers, editors, auth_host="", watch_host="", dis
|
|||||||
run(dc + ["up", "-d", "--build"]) # recreates only services whose compose definition changed
|
run(dc + ["up", "-d", "--build"]) # recreates only services whose compose definition changed
|
||||||
run(dc + ["restart", "copyparty"]) # copyparty re-reads its config on start
|
run(dc + ["restart", "copyparty"]) # copyparty re-reads its config on start
|
||||||
run(dc + ["exec", "caddy", "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]) # zero-downtime
|
run(dc + ["exec", "caddy", "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]) # zero-downtime
|
||||||
|
post_start_fixups(dc)
|
||||||
print(" mediamtx: config changes are NOT picked up live on this setup (bind-mounted file). If mediamtx.yml changed,")
|
print(" mediamtx: config changes are NOT picked up live on this setup (bind-mounted file). If mediamtx.yml changed,")
|
||||||
print(" run docker compose restart mediamtx between streams (cuts a live recording). Adding/removing")
|
print(" run docker compose restart mediamtx between streams (cuts a live recording). Adding/removing")
|
||||||
print(" streamers or tokens never touches mediamtx.yml, so day-to-day nothing restarts.")
|
print(" streamers or tokens never touches mediamtx.yml, so day-to-day nothing restarts.")
|
||||||
|
|
||||||
|
def post_start_fixups(dc):
|
||||||
|
"""Idempotent first-run state that otherwise needs clicking: Syncthing GUI auth off, Gitea admin user, Jellyfin plugin."""
|
||||||
|
env = read_env()
|
||||||
|
print("== post-start fixups (idempotent)")
|
||||||
|
# Syncthing: no GUI login (the Authentik gate is the login); needs insecureAdminAccess since Caddy is not "localhost" to it
|
||||||
|
cfg = Path("/srv/media/.syncthing/config.xml")
|
||||||
|
if not DRY and cfg.exists():
|
||||||
|
import time as _t
|
||||||
|
for _ in range(20):
|
||||||
|
if "<gui" in cfg.read_text(): break
|
||||||
|
_t.sleep(2)
|
||||||
|
x = cfg.read_text()
|
||||||
|
if "<gui" in x and ('insecureAdminAccess="true"' not in x or "<user>" in x or "<password>" in x):
|
||||||
|
x = re.sub(r"^\s*<user>.*</user>\s*$\n?", "", x, flags=re.M); x = re.sub(r"^\s*<password>.*</password>\s*$\n?", "", x, flags=re.M)
|
||||||
|
x = re.sub(r'<gui ([^>]*?)insecureAdminAccess="false"', r'<gui \1insecureAdminAccess="true"', x)
|
||||||
|
if 'insecureAdminAccess=' not in x: x = x.replace("<gui ", '<gui insecureAdminAccess="true" ', 1)
|
||||||
|
subprocess.run(["sudo", "tee", str(cfg)], input=x, text=True, capture_output=True)
|
||||||
|
run(dc + ["restart", "syncthing"]); print(" syncthing: GUI auth disabled, insecureAdminAccess on")
|
||||||
|
else: print(" syncthing: ok")
|
||||||
|
elif DRY: print(" (dry) syncthing config patch")
|
||||||
|
# Gitea: create the admin user once (INSTALL_LOCK skips the install page)
|
||||||
|
if not DRY:
|
||||||
|
import time as _t
|
||||||
|
for _ in range(30):
|
||||||
|
r = subprocess.run(dc + ["exec", "-T", "-u", "1000", "gitea", "gitea", "admin", "user", "list", "--config", "/data/gitea/conf/app.ini"], capture_output=True, text=True)
|
||||||
|
if r.returncode == 0: break
|
||||||
|
_t.sleep(3)
|
||||||
|
if r.returncode == 0 and env.get("GITEA_ADMIN_USER") and env["GITEA_ADMIN_USER"] not in r.stdout:
|
||||||
|
subprocess.run(dc + ["exec", "-T", "-u", "1000", "gitea", "gitea", "admin", "user", "create", "--config", "/data/gitea/conf/app.ini", "--admin",
|
||||||
|
"--username", env["GITEA_ADMIN_USER"], "--password", env["GITEA_ADMIN_PASSWORD"], "--email", f"{env['GITEA_ADMIN_USER']}@{env.get('MAIN_HOST','localhost')}", "--must-change-password=false"], capture_output=True, text=True)
|
||||||
|
print(f" gitea: admin user {env['GITEA_ADMIN_USER']} created (password in .env)")
|
||||||
|
else: print(" gitea: ok" if r.returncode == 0 else " gitea: not ready (install page or db?) — run ./configure.py --apply again later")
|
||||||
|
else: print(" (dry) gitea admin user")
|
||||||
|
# Jellyfin plugin from the committed zip
|
||||||
|
if Path("installJellyfinPlugin.sh").exists():
|
||||||
|
if DRY: print(" (dry) installJellyfinPlugin.sh")
|
||||||
|
else: subprocess.run(["bash", "installJellyfinPlugin.sh"], capture_output=True, text=True); print(" jellyfin plugin: ensured")
|
||||||
|
|
||||||
def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
def show(domain, admin_pw, streamers, editors, auth_host="", watch_host="", git_host="", bot_host="", live_host=""):
|
||||||
print(f"\n== domain : {domain or '(not set)'}")
|
print(f"\n== domain : {domain or '(not set)'}")
|
||||||
_e = read_env()
|
_e = read_env()
|
||||||
if _e.get("APEX_HOST") and _e.get("APEX_REDIRECT"): print(f"== apex : https://{_e['APEX_HOST']}/ -> {_e['APEX_REDIRECT']}")
|
if _e.get("APEX_HOST") and _e.get("APEX_REDIRECT"): print(f"== apex : https://{_e['APEX_HOST']}/ -> {_e['APEX_REDIRECT']}")
|
||||||
print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)")
|
print(f"== live : https://{live_host}/ (behind Authentik forward-auth; deploy live/dist -> /srv/live; WebRTC needs 8189 open)" if live_host else "== live : (not set — run --full)")
|
||||||
print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)")
|
print(f"== bot web : https://{bot_host}/ (Authentik forward-auth; Caddy requires group '{read_env().get('BOT_REQUIRE_GROUP','bot-users') or 'any'}')" if bot_host else "== bot web : (not set — run --full)")
|
||||||
print(f"== gitea : https://{git_host}/ ssh port 2222 (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)")
|
print(f"== gitea : https://{git_host}/ ssh port 2222 admin {read_env().get('GITEA_ADMIN_USER','vanguard')} / {read_env().get('GITEA_ADMIN_PASSWORD','?')} (runner: ./registerGiteaRunner.sh)" if git_host else "== gitea : (not set — run --full)")
|
||||||
print(f"== authentik: https://{auth_host}/ (first run: https://{auth_host}/if/flow/initial-setup/)" if auth_host else "== authentik: (not set — run --full)")
|
print(f"== authentik: https://{auth_host}/ akadmin password = AUTHENTIK_BOOTSTRAP_PASSWORD in .env (set on first start; change it in the UI)" if auth_host else "== authentik: (not set — run --full)")
|
||||||
print(f"== jellyfin : https://{watch_host}/" if watch_host else "== jellyfin : (not set — run --full)")
|
print(f"== jellyfin : https://{watch_host}/" if watch_host else "== jellyfin : (not set — run --full)")
|
||||||
env = read_env()
|
env = read_env()
|
||||||
tok = env.get("DISCORD_BOT_TOKEN", "")
|
tok = env.get("DISCORD_BOT_TOKEN", "")
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ services:
|
|||||||
command: server
|
command: server
|
||||||
environment: &authentik_env
|
environment: &authentik_env
|
||||||
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
||||||
|
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-} # akadmin password on FIRST start (no initial-setup page)
|
||||||
|
AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN:-} # API token for akadmin, created on first start (blueprints/automation)
|
||||||
AUTHENTIK_REDIS__HOST: authentik-redis
|
AUTHENTIK_REDIS__HOST: authentik-redis
|
||||||
AUTHENTIK_POSTGRESQL__HOST: authentik-db
|
AUTHENTIK_POSTGRESQL__HOST: authentik-db
|
||||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||||
@@ -234,6 +236,7 @@ services:
|
|||||||
GITEA__actions__ENABLED: "true"
|
GITEA__actions__ENABLED: "true"
|
||||||
GITEA__actions__DEFAULT_ACTIONS_URL: github
|
GITEA__actions__DEFAULT_ACTIONS_URL: github
|
||||||
GITEA__webhook__ALLOWED_HOST_LIST: private,loopback,external
|
GITEA__webhook__ALLOWED_HOST_LIST: private,loopback,external
|
||||||
|
GITEA__security__INSTALL_LOCK: "true" # no install page: db/settings come from env; admin user via configure.py
|
||||||
GITEA__time__DEFAULT_UI_LOCATION: America/New_York
|
GITEA__time__DEFAULT_UI_LOCATION: America/New_York
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:3000:3000"
|
- "127.0.0.1:3000:3000"
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ echo "== compose images (the stack is started by ./configure.py --full, which ge
|
|||||||
chown -R "$OWNER:" .
|
chown -R "$OWNER:" .
|
||||||
docker compose pull -q --ignore-buildable 2>/dev/null || docker compose pull -q || true
|
docker compose pull -q --ignore-buildable 2>/dev/null || docker compose pull -q || true
|
||||||
|
|
||||||
|
echo "== live portal: build into /srv/live (node)"
|
||||||
|
if [[ -d live && -f live/package.json ]]; then
|
||||||
|
( cd live && sudo -u "$OWNER" -H npm install --no-audit --no-fund --loglevel=error && sudo -u "$OWNER" -H npm run build ) \
|
||||||
|
&& install -d -o "$OWNER" /srv/live && rsync -a --delete live/dist/ /srv/live/ && echo " built" || echo " portal build failed (run ./deployLive.sh from a dev box later)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "== host firewall (nftables; own table, input only; keeps this SSH session alive)"
|
echo "== host firewall (nftables; own table, input only; keeps this SSH session alive)"
|
||||||
if [[ -f firewall/firewall.sh ]]; then
|
if [[ -f firewall/firewall.sh ]]; then
|
||||||
bash firewall/firewall.sh
|
bash firewall/firewall.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user