Security hardening and first-run/portability improvements
Security (P0): - Remove committed session-secret default; auto-generate and persist a random secret to the config volume when SESSION_SECRET is unset (prevents forgeable session cookies / auth bypass). - Validate playlist names to a safe charset and render sldl configs via literal Python substitution instead of sed (closes a command-injection and path-traversal path through playlist names). - shlex-quote credential values written to shell-sourced env files, and strip newlines from values patched into .conf files. - Render playlist .conf files 0600; warn at startup if the master key is co-located with the config volume; document keeping it separate. Portability: - Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton. - Remove personal defaults (navidrome user "andrew", ephemeral.club URLs). - Ship generic example seeds; move the cross-album dedup keep-list and the legacy playlist import to editable config files; drop the personal _upgrade.csv. - Generic VPN reference in docker-compose.snippet.yml. First-run experience: - Redirect to /setup instead of 500 when OIDC is unconfigured; surface a missing master key inline; entrypoint exits with an actionable message when the config folder is not writable. - Add unauthenticated /health (JSON) and /setup (checklist) diagnostics. Docs: - Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced but missing); expand README with ownership, backups, advanced settings, and migration guidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
import shlex
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -30,7 +31,7 @@ SCOPE_DESCRIPTIONS = {
|
||||
"navidrome": "Triggers a library rescan after downloads/imports/edits, and lets the daily status report check Navidrome's scan health. Required.",
|
||||
"bandcamp": "Pulls new purchases from your Bandcamp collection, imports them automatically, and stamps a buy link into the file tag.",
|
||||
"qobuz": "A buy-link source (alongside Bandcamp) for tracks you didn't purchase there -- DRM-free hi-res links when available, written directly into the file tag.",
|
||||
"azuracast": "Not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file tag) -- this lets alembic tell AzuraCast (ephemeral.club's backend) to immediately reprocess touched files or fix a playlist assignment, instead of waiting for its periodic scan.",
|
||||
"azuracast": "Only relevant if you run an AzuraCast radio station off the same library. Not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file tag) -- this lets alembic tell AzuraCast to immediately reprocess touched files or fix a playlist assignment, instead of waiting for its periodic scan.",
|
||||
"telegram": "Sends the daily pipeline health digest to a chat instead of you having to check the dashboard.",
|
||||
}
|
||||
|
||||
@@ -99,6 +100,9 @@ def _patch_conf_field(path: Path, key: str, value: str) -> None:
|
||||
every other line (including PLAYLIST_NAME/SPOTIFY_URL substitutions
|
||||
regen.sh already applied) untouched."""
|
||||
text = path.read_text()
|
||||
# A newline in the value would inject an extra `key = value` line into the
|
||||
# sldl config. Credentials never legitimately contain one, so strip any.
|
||||
value = value.replace("\r", "").replace("\n", "")
|
||||
pattern = re.compile(rf"^{re.escape(key)}\s*=.*$", re.MULTILINE)
|
||||
new_line = f"{key} = {value}"
|
||||
if pattern.search(text):
|
||||
@@ -114,7 +118,11 @@ def _every_playlist_conf() -> list[Path]:
|
||||
|
||||
def _write_env_file(path: Path, values: dict[str, str]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [f"{k}='{v}'" for k, v in values.items()]
|
||||
# shlex.quote so a credential value containing a quote, space, or shell
|
||||
# metacharacter (e.g. a password like `p'a$(id)ss`) stays a single literal
|
||||
# value when a pipeline script `source`s this file, instead of breaking out
|
||||
# and executing. shlex.quote also handles the empty string correctly ('').
|
||||
lines = [f"{k}={shlex.quote(v)}" for k, v in values.items()]
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
path.chmod(0o600)
|
||||
|
||||
@@ -147,7 +155,7 @@ def render_scope(db: Session, scope: str) -> None:
|
||||
settings.pipeline_config_dir / "navidrome" / "admin.env",
|
||||
{
|
||||
"ND_BASE": values.get("base_url", "http://navidrome:4533"),
|
||||
"ND_USER": values.get("admin_user", "andrew"),
|
||||
"ND_USER": values.get("admin_user", ""),
|
||||
"ND_PASS": values.get("admin_pass", ""),
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user