Files
site/apps/api/app/core/install_secrets.py
T
влад 12c983c0fc Шхуна не тонет: security, infra и доки на русском.
Безопасность довёл до ума — Cursor-генерацию переписал руками.
IDOR закрыл, CSRF задушил, refresh rotation теперь как надо.
HSTS на staging, ENABLE_DOCS=false, install.env recovery протестил.

Backend:
- jwt_denylist + auth_epoch: мгновенный revoke access JWT (logout/block/reset)
- auth/admin/users: bump epoch, logout с Bearer, forgot_password skip для blocked
- install_secrets: путь всегда apps/api/data/secrets/ (bootstrap из корня не ломает Docker)
- seed: SEED_DEMO_USERS=false на prod/staging
- тесты: jwt revoke, integration, coverage gate 90%

Frontend:
- logout шлёт Bearer, обработка TOKEN_REVOKED
- guards TypeScript fix
- E2E: blocked user → 401 сразу после block

Infra:
- staging/prod compose, TLS nginx, deploy-скрипты
- k6 §17.2, backup/health/smoke scripts

Docs:
- docs/ на русском: project, security, deploy, release (старые md слили)
- README короткий + план ТЗ + стандартные логины dev

Код готов к плаванию. Капитан может идти писать фронт.
2026-07-15 00:06:13 +03:00

193 lines
6.4 KiB
Python

from __future__ import annotations
import os
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from urllib.parse import ParseResult, urlparse, urlunparse
from uuid import uuid4
from app.core.crypto import generate_install_bundle
_API_ROOT = Path(__file__).resolve().parents[2]
INSTALL_SECRETS_DIR = _API_ROOT / "data" / "secrets"
INSTALL_SECRETS_FILE = INSTALL_SECRETS_DIR / "install.env"
INSTALL_SECRETS_META_FILE = INSTALL_SECRETS_DIR / "install.meta.json"
REQUIRED_KEYS = (
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_DB",
"DATABASE_URL",
"JWT_ACCESS_SECRET",
"JWT_REFRESH_PEPPER",
"S3_ACCESS_KEY",
"S3_SECRET_KEY",
"MINIO_ROOT_USER",
"MINIO_ROOT_PASSWORD",
)
@dataclass
class InstallSecretsStatus:
initialized: bool
locked: bool
path: str
created: bool
def _parse_env_text(raw: str) -> dict[str, str]:
values: dict[str, str] = {}
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip()
return values
def _render_env(values: dict[str, str]) -> str:
ordered = [f"{key}={values[key]}" for key in sorted(values.keys())]
return "\n".join(ordered) + "\n"
def read_install_secrets() -> dict[str, str]:
if not INSTALL_SECRETS_FILE.exists():
return {}
return _parse_env_text(INSTALL_SECRETS_FILE.read_text(encoding="utf-8"))
def _write_install_secrets(values: dict[str, str]) -> None:
INSTALL_SECRETS_DIR.mkdir(parents=True, exist_ok=True)
INSTALL_SECRETS_FILE.write_text(_render_env(values), encoding="utf-8")
def _write_meta() -> None:
payload = (
"{\n"
f' "install_id": "{uuid4()}",\n'
f' "locked_at": "{datetime.now(UTC).isoformat()}"\n'
"}\n"
)
INSTALL_SECRETS_META_FILE.write_text(payload, encoding="utf-8")
def _adopt_from_environment() -> dict[str, str]:
env_values = {key: os.getenv(key, "") for key in REQUIRED_KEYS}
db_url = os.getenv("DATABASE_URL", "")
if db_url:
parsed = urlparse(db_url)
if parsed.username:
env_values["POSTGRES_USER"] = parsed.username
if parsed.password:
env_values["POSTGRES_PASSWORD"] = parsed.password
if parsed.path and parsed.path != "/":
env_values["POSTGRES_DB"] = parsed.path.lstrip("/")
return {key: value for key, value in env_values.items() if value}
def _sync_minio_s3_secrets(values: dict[str, str]) -> dict[str, str]:
"""MinIO root credentials are the S3 access key pair — keep them aligned."""
if (
values.get("S3_ACCESS_KEY") == values.get("MINIO_ROOT_USER")
and values.get("MINIO_ROOT_PASSWORD")
and values.get("S3_SECRET_KEY") != values["MINIO_ROOT_PASSWORD"]
):
values = dict(values)
values["S3_SECRET_KEY"] = values["MINIO_ROOT_PASSWORD"]
return values
def load_install_secrets_to_env() -> None:
values = _sync_minio_s3_secrets(read_install_secrets())
for key, value in values.items():
os.environ[key] = value
def ensure_install_secrets() -> InstallSecretsStatus:
existing = read_install_secrets()
if existing:
synced = _sync_minio_s3_secrets(existing)
if synced != existing:
synced["SECRETS_LOCKED"] = existing.get("SECRETS_LOCKED", "true")
_write_install_secrets(synced)
existing = synced
load_install_secrets_to_env()
return InstallSecretsStatus(True, existing.get("SECRETS_LOCKED", "false") == "true", str(INSTALL_SECRETS_FILE), False)
adopted = _adopt_from_environment()
generated = generate_install_bundle()
values = generated | adopted
values["SECRETS_LOCKED"] = "true"
_write_install_secrets(values)
_write_meta()
load_install_secrets_to_env()
return InstallSecretsStatus(True, True, str(INSTALL_SECRETS_FILE), True)
def masked_database_url(database_url: str) -> str:
parsed = urlparse(database_url)
if not parsed.username:
return database_url
password = "***" if parsed.password else ""
credentials = f"{parsed.username}:{password}" if password else parsed.username
host = parsed.hostname or ""
if parsed.port:
host = f"{host}:{parsed.port}"
netloc = f"{credentials}@{host}"
sanitized = ParseResult(
scheme=parsed.scheme,
netloc=netloc,
path=parsed.path,
params=parsed.params,
query=parsed.query,
fragment=parsed.fragment,
)
return urlunparse(sanitized)
def install_secrets_payload() -> dict:
values = read_install_secrets()
db = urlparse(values.get("DATABASE_URL", ""))
return {
"initialized": bool(values),
"locked": values.get("SECRETS_LOCKED") == "true",
"secrets_path": str(INSTALL_SECRETS_FILE),
"database": {
"host": db.hostname,
"port": db.port,
"database": db.path.lstrip("/") if db.path else "",
"user": db.username,
"password_configured": bool(values.get("POSTGRES_PASSWORD")),
},
"connection_string_masked": masked_database_url(values.get("DATABASE_URL", "")),
"secrets_status": {
"jwt_access_secret": "configured" if bool(values.get("JWT_ACCESS_SECRET")) else "missing",
"jwt_refresh_pepper": "configured" if bool(values.get("JWT_REFRESH_PEPPER")) else "missing",
"postgres_password": "configured" if bool(values.get("POSTGRES_PASSWORD")) else "missing",
"s3_secret_key": "configured" if bool(values.get("S3_SECRET_KEY")) else "missing",
"password_bcrypt_salt": "per_user_in_db",
},
}
def reveal_install_secret(key: str) -> str:
mapping = {
"database_password": "POSTGRES_PASSWORD",
"jwt_access_secret": "JWT_ACCESS_SECRET",
"jwt_refresh_pepper": "JWT_REFRESH_PEPPER",
"s3_secret_key": "S3_SECRET_KEY",
}
env_key = mapping.get(key)
if not env_key:
raise ValueError("UNSUPPORTED_SECRET_KEY")
values = read_install_secrets()
if env_key in values:
return values[env_key]
if env_key == "POSTGRES_PASSWORD":
database_url = values.get("DATABASE_URL") or os.getenv("DATABASE_URL", "")
parsed = urlparse(database_url)
return parsed.password or ""
return os.getenv(env_key, "")