feat(release): publish minimal self-contained Magent source
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user