"""Persistent secrets for fresh image-only container installations. Runs before importing application settings. Existing environment-managed deployments are unchanged. Secrets are never printed during normal startup. """ import base64 import binascii from contextlib import closing import json import os from pathlib import Path import re import secrets import sqlite3 import stat import sys import tempfile from urllib.parse import urlsplit from .installation_origin import normalize_application_origin DATA_DIRECTORY = Path("/app/data") STATE_FILENAME = "bootstrap-secrets.json" SECRET_NAMES = ("JWT_SECRET", "SETTINGS_ENCRYPTION_KEY", "SETUP_TOKEN") MAX_STATE_BYTES = 4096 class BootstrapError(ValueError): """An operator-actionable error that never includes a secret value.""" def managed_mode(environment: dict) -> bool: value = environment.get("MAGENT_MANAGED_SECRETS", "false").strip().lower() if value == "auto": # Existing explicitly keyed installations retain their environment and # JWT-derived encryption behaviour. Fresh image-only installs opt in. return not bool(environment.get("JWT_SECRET", "").strip()) if value not in {"true", "false", "1", "0", "yes", "no", ""}: raise BootstrapError("MAGENT_MANAGED_SECRETS must be auto, true or false.") return value in {"true", "1", "yes"} def _data_paths(environment: dict, directory: Path) -> tuple[Path, Path]: directory = directory.absolute() if not directory.is_dir() or any(part.is_symlink() for part in (directory, *directory.parents)): raise BootstrapError("Managed installation requires a real, writable /app/data volume; symlinks are not allowed.") if os.name == "posix": metadata = directory.stat() if metadata.st_uid != os.geteuid() or stat.S_IMODE(metadata.st_mode) & 0o022: raise BootstrapError("Managed data volume must belong to the runtime user and not be writable by other users.") database = directory / "magent.db" configured = Path(environment.get("SQLITE_PATH") or str(database)).absolute() if configured != database: raise BootstrapError("Managed installation requires SQLITE_PATH=/app/data/magent.db; retain manual keys for custom paths.") if os.path.lexists(database) and (database.is_symlink() or not database.is_file()): raise BootstrapError("Managed database must be a regular file, not a symlink or directory.") return directory / STATE_FILENAME, database def _read_state(path: Path) -> dict: try: descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) with os.fdopen(descriptor, "rb") as handle: metadata = os.fstat(handle.fileno()) if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > MAX_STATE_BYTES: raise BootstrapError("Managed secrets file must be a small regular file.") if os.name == "posix" and ( metadata.st_uid != os.geteuid() or stat.S_IMODE(metadata.st_mode) != 0o600 ): raise BootstrapError("Managed secrets file must belong to the runtime user with permissions 0600.") state = json.loads(handle.read(MAX_STATE_BYTES + 1)) except FileNotFoundError: raise except (OSError, ValueError, UnicodeError) as exc: if isinstance(exc, BootstrapError): raise raise BootstrapError("Cannot read managed secrets. Restore the original file; keys will not be regenerated.") from None if not isinstance(state, dict) or set(state) != {"version", *SECRET_NAMES} or type(state["version"]) is not int or state["version"] != 1: raise BootstrapError("Invalid managed secrets format. Restore the original file; keys will not be regenerated.") for key in SECRET_NAMES: if not isinstance(state[key], str): raise BootstrapError("Invalid managed secret values. Restore the original file.") for key in ("JWT_SECRET", "SETUP_TOKEN"): if not re.fullmatch(r"[A-Za-z0-9_-]{64}", state[key]) or len(set(state[key])) < 2: raise BootstrapError("Invalid managed token. Restore the original file.") try: decoded = base64.b64decode(state["SETTINGS_ENCRYPTION_KEY"], altchars=b"-_", validate=True) except (ValueError, binascii.Error): raise BootstrapError("Invalid managed encryption key. Restore the original file.") from None if len(decoded) != 32 or base64.urlsafe_b64encode(decoded).decode() != state["SETTINGS_ENCRYPTION_KEY"]: raise BootstrapError("Invalid managed encryption key. Restore the original file.") if state["JWT_SECRET"] == state["SETUP_TOKEN"]: raise BootstrapError("Managed signing and setup tokens must be independent.") return state def _sync_directory(directory: Path) -> None: if os.name == "posix": descriptor = os.open(directory, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(descriptor) finally: os.close(descriptor) def _create_state(path: Path) -> dict: state = { "version": 1, "JWT_SECRET": secrets.token_urlsafe(48), "SETTINGS_ENCRYPTION_KEY": base64.urlsafe_b64encode(secrets.token_bytes(32)).decode(), "SETUP_TOKEN": secrets.token_urlsafe(48), } descriptor, temporary_name = tempfile.mkstemp(prefix=".magent-secrets-", dir=path.parent) temporary = Path(temporary_name) try: with os.fdopen(descriptor, "w", encoding="utf-8") as handle: json.dump(state, handle, separators=(",", ":")) handle.flush() os.fsync(handle.fileno()) try: # Publish an entirely written file without replacing another # initializer's state. Both callers subsequently read the winner. os.link(temporary, path) _sync_directory(path.parent) except FileExistsError: pass finally: temporary.unlink(missing_ok=True) return _read_state(path) def _saved_origin(database: Path) -> str: if not database.exists(): return "" try: with closing(sqlite3.connect(database.as_uri() + "?mode=ro", uri=True)) as connection: if not connection.execute("SELECT 1 FROM sqlite_master WHERE type='table' AND name='settings'").fetchone(): return "" row = connection.execute("SELECT value FROM settings WHERE key='magent_application_url'").fetchone() return str(row[0] or "") if row else "" except sqlite3.Error: raise BootstrapError("Cannot read the saved application address. Check the existing database; no keys were changed.") from None def _configure_origin(environment: dict, database: Path) -> None: value = environment.get("MAGENT_APPLICATION_URL", "") saved = _saved_origin(database) if saved: value = saved if not value: # No network address is trusted automatically. The token-authorized # first-admin transaction will save the explicitly confirmed origin. environment.setdefault("CORS_ALLOW_ORIGIN", "http://localhost:3000") environment.setdefault("AUTH_COOKIE_SECURE", "false") return try: parsed = urlsplit(value) valid = ( bool(value) and not any(c.isspace() or ord(c) < 33 or ord(c) == 127 for c in value) and parsed.scheme in {"http", "https"} and parsed.hostname and parsed.username is None and parsed.password is None and not parsed.path and "?" not in value and "#" not in value and "\\" not in value and "*" not in value and (parsed.port is None or 1 <= parsed.port <= 65535) ) except ValueError: valid = False if not valid: raise BootstrapError("Set MAGENT_APPLICATION_URL to the exact http(s) browser origin, with no path or trailing slash.") if not saved and environment.get("CORS_ALLOW_ORIGIN") not in (None, "", value): raise BootstrapError("CORS_ALLOW_ORIGIN must match MAGENT_APPLICATION_URL for a managed install.") value = normalize_application_origin(value) environment["MAGENT_APPLICATION_URL"] = value environment["CORS_ALLOW_ORIGIN"] = value secure = environment.get("AUTH_COOKIE_SECURE", "").strip().lower() if not secure: environment["AUTH_COOKIE_SECURE"] = str(parsed.scheme == "https").lower() elif secure not in {"true", "false", "1", "0"}: raise BootstrapError("AUTH_COOKIE_SECURE must be true or false.") elif parsed.scheme == "https" and secure in {"false", "0"}: raise BootstrapError("HTTPS managed installations require AUTH_COOKIE_SECURE=true.") elif parsed.scheme == "http" and secure in {"true", "1"}: raise BootstrapError("Secure cookies require an HTTPS application URL.") def prepare_environment(environment: dict, directory: Path = DATA_DIRECTORY) -> dict: prepared = dict(environment) if not managed_mode(prepared): return prepared if not prepared.get("JWT_SECRET", "").strip(): prepared.pop("JWT_SECRET", None) path, database = _data_paths(prepared, directory) _configure_origin(prepared, database) if prepared.get("API_DOCS_ENABLED", "false").strip().lower() not in {"", "false", "0"}: raise BootstrapError("API_DOCS_ENABLED is fixed to false for managed installations.") try: state = _read_state(path) except FileNotFoundError: # Never add independent encryption to an existing JWT-derived database # or invent replacement keys after a lost secrets file. if any(os.path.lexists(str(database) + suffix) for suffix in ("", "-wal", "-shm", "-journal")): raise BootstrapError("Existing database has no managed secrets file. Restore its original keys or use the existing manual deployment.") from None if any(prepared.get(key) for key in SECRET_NAMES): raise BootstrapError("Fresh managed installs generate their own keys. Remove manual key variables or disable managed mode.") from None state = _create_state(path) for key in SECRET_NAMES: if prepared.get(key) and prepared[key] != state[key]: raise BootstrapError(f"{key} conflicts with the persistent managed value. Keys will not be replaced.") prepared[key] = state[key] prepared["SQLITE_PATH"] = str(database) prepared["API_DOCS_ENABLED"] = "false" prepared["MAGENT_MANAGED_SECRETS"] = "true" prepared["MAGENT_RUNTIME_MANAGED"] = "1" return prepared def setup_token(environment: dict, directory: Path = DATA_DIRECTORY) -> str: if not managed_mode(environment): raise BootstrapError("Managed secrets are disabled. Use the SETUP_TOKEN from your deployment configuration.") path, database = _data_paths(environment, directory) state = _read_state(path) # This read-only command never generates keys. if database.is_symlink() or not database.is_file(): raise BootstrapError("Database is not initialized. Wait for the container to become healthy.") try: with closing(sqlite3.connect(database.as_uri() + "?mode=ro", uri=True)) as connection: row = connection.execute("SELECT completed FROM installation_setup WHERE id = 1").fetchone() admin = connection.execute("SELECT 1 FROM users WHERE LOWER(role) = 'admin' LIMIT 1").fetchone() except sqlite3.Error: raise BootstrapError("Cannot verify setup state. No setup token will be displayed.") from None if row is None or row[0] != 0 or admin is not None: raise BootstrapError("Initial administrator setup is no longer available. Sign in with the existing administrator.") return state["SETUP_TOKEN"] def main() -> int: try: if sys.argv[1:] == ["setup-token"]: print(setup_token(dict(os.environ))) return 0 if len(sys.argv) < 2: raise BootstrapError("Pass the container startup command, or setup-token from the operator console.") environment = prepare_environment(dict(os.environ)) if managed_mode(environment): print("Managed installation secrets loaded. For first setup, run in the container console: " "python -m app.container_bootstrap setup-token", flush=True) os.execvpe(sys.argv[1], sys.argv[1:], environment) except (BootstrapError, OSError): # Never include unexpected I/O details or environment values in logs. error = sys.exc_info()[1] message = str(error) if isinstance(error, BootstrapError) else "Cannot access managed installation files or start the runtime. Check volume permissions and original keys." print(f"Magent startup: {message}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())