111 lines
4.2 KiB
Python
111 lines
4.2 KiB
Python
"""Validate a prod `.env` file before (re)deploying.
|
|
|
|
Usage (from backend/ or repo root):
|
|
uv run python scripts/check_prod.py --env ../.env
|
|
uv run python scripts/check_prod.py # defaults to repo-root .env
|
|
|
|
Fails (exit 1) on missing/weak secrets and insecure modes; warns on
|
|
soft misconfigurations. Safe to run anywhere: reads one file, no imports
|
|
from app (stdlib only), prints no secret values.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
BACKEND = Path(__file__).resolve().parent.parent
|
|
REPO_ROOT = BACKEND.parent
|
|
|
|
MIN_TOKEN = 32
|
|
MIN_PASSWORD = 16
|
|
MIN_API_KEY = 16
|
|
|
|
|
|
def parse_env(path: Path) -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
for line in path.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
key = key.strip()
|
|
value = value.strip().strip("'\"")
|
|
if key.startswith("export "):
|
|
key = key[len("export ") :].strip()
|
|
values[key] = value
|
|
return values
|
|
|
|
|
|
def check(values: dict[str, str]) -> tuple[list[str], list[str]]:
|
|
"""(failures, warnings) for a parsed env file."""
|
|
failures: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
def secret(name: str, minimum: int, what: str) -> None:
|
|
value = values.get(name, "")
|
|
if not value:
|
|
failures.append(f"{name} is empty — generate one: openssl rand -hex 32")
|
|
elif len(value) < minimum:
|
|
failures.append(f"{name} too short ({len(value)} < {minimum} chars)")
|
|
|
|
def placeholder(name: str, prefixes: tuple[str, ...]) -> None:
|
|
value = values.get(name, "")
|
|
if value and any(value == p or value.startswith(p) for p in prefixes):
|
|
failures.append(f"{name} still holds an example placeholder")
|
|
|
|
# Shared + service API keys.
|
|
secret("CALLBACK_TOKEN", MIN_TOKEN, "callback token")
|
|
secret("LANGFLOW_API_KEY", MIN_API_KEY, "langflow api key")
|
|
placeholder("LANGFLOW_API_KEY", ("sk-",))
|
|
secret("LFX_API_KEY", MIN_API_KEY, "lfx api key")
|
|
placeholder("LFX_API_KEY", ("lfx-",))
|
|
# Langflow's own auth (internet-facing authoring UI).
|
|
secret("LANGFLOW_SUPERUSER_PASSWORD", MIN_PASSWORD, "langflow superuser password")
|
|
secret("LANGFLOW_SECRET_KEY", MIN_TOKEN, "langflow secret key")
|
|
|
|
# Insecure modes.
|
|
if values.get("FRONTEND_MODE", "preview") == "dev":
|
|
failures.append("FRONTEND_MODE=dev serves the Vite dev server — use preview in prod")
|
|
if values.get("APP_ENV", "prod") == "dev":
|
|
failures.append("APP_ENV=dev enables autoreload — unset it in prod")
|
|
if values.get("LANGFLOW_AUTO_LOGIN", "false").lower() == "true":
|
|
failures.append("LANGFLOW_AUTO_LOGIN=true allows passwordless superuser access")
|
|
|
|
# Soft misconfigurations.
|
|
if not values.get("ADMIN_EMAILS", ""):
|
|
warnings.append("ADMIN_EMAILS empty — nobody can bootstrap an admin account")
|
|
if values.get("SMTP_HOST", "") and not values.get("SMTP_PASSWORD", ""):
|
|
warnings.append("SMTP_HOST without SMTP_PASSWORD — magic-link mail will fail")
|
|
if not values.get("CONTACT_TO", ""):
|
|
warnings.append("CONTACT_TO empty — access-request notifications go nowhere")
|
|
frontend_url = values.get("FRONTEND_URL", "")
|
|
if frontend_url and not frontend_url.startswith("https://"):
|
|
warnings.append("FRONTEND_URL is not https — cookies and tokens travel in cleartext")
|
|
|
|
return failures, warnings
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--env", default=str(REPO_ROOT / ".env"))
|
|
args = parser.parse_args()
|
|
path = Path(args.env)
|
|
if not path.is_file():
|
|
print(f"no env file at {path}")
|
|
return 1
|
|
failures, warnings = check(parse_env(path))
|
|
for message in warnings:
|
|
print(f"WARN: {message}")
|
|
for message in failures:
|
|
print(f"FAIL: {message}")
|
|
if failures:
|
|
print(f"{len(failures)} blocking issue(s) — fix before deploying")
|
|
return 1
|
|
print("prod env OK" + (f" ({len(warnings)} warning(s))" if warnings else ""))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|