diff --git a/.env.docker.example b/.env.docker.example index 1facb56..325a5f6 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -54,7 +54,7 @@ BCRYPT_ROUNDS=12 # --- Admin user (seeded on first start) -------------------------------------- ADMIN_EMAIL=admin@example.com -ADMIN_PASSWORD=Admin123! +ADMIN_PASSWORD=CHANGE_ME_generate_a_strong_password # --- MAIL_ENCRYPTION_KEY (REQUIRED) ------------------------------------------- # AES-256 encryption key for mail account passwords (Fernet). diff --git a/.env.example b/.env.example index 4ece17a..631c183 100644 --- a/.env.example +++ b/.env.example @@ -134,4 +134,4 @@ API_GIT_BRANCH=main # === Admin User (auto-seeded on first start) === ADMIN_EMAIL=admin@media-on.de -ADMIN_PASSWORD=Admin123! +ADMIN_PASSWORD=CHANGE_ME_generate_a_strong_password diff --git a/docker-compose.yaml b/docker-compose.yaml index ccacb8a..0aed3ca 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -92,7 +92,7 @@ services: SMTP_TLS: ${SMTP_TLS:-true} BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12} ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@media-on.de} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:-Admin123!} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD must be set — no default credentials (Astra F30)} MAIL_ENCRYPTION_KEY: ${MAIL_ENCRYPTION_KEY:?MAIL_ENCRYPTION_KEY is required} volumes: - storage:/data/storage diff --git a/scripts/seed_admin.py b/scripts/seed_admin.py index e600d6d..15215bc 100644 --- a/scripts/seed_admin.py +++ b/scripts/seed_admin.py @@ -16,20 +16,21 @@ for initial bootstrap on a fresh database. """ import asyncio -import sys import os +import sys # Ensure app is importable sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from app.core.db import get_migration_engine, set_tenant_context -from app.core.auth import hash_password -from app.models.tenant import Tenant -from app.models.user import User, UserTenant -from app.models.role import Role from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker +from app.core.auth import hash_password +from app.core.db import get_migration_engine, set_tenant_context +from app.models.role import Role +from app.models.tenant import Tenant +from app.models.user import User, UserTenant + async def seed(): # Use migration engine to bypass RLS for bootstrap @@ -73,10 +74,34 @@ async def seed(): user = result.scalar_one_or_none() if user is None: + # F30 (Astra P1): no known default password on fresh installs. + # - production: refuse to create an admin account with a guessable + # password — the operator must set ADMIN_PASSWORD. + # - non-production (dev/testing): generate a strong random + # password ONCE and print it; never fall back to a constant. + admin_password = os.environ.get("ADMIN_PASSWORD", "").strip() + from app.config import get_settings + + if not admin_password: + if get_settings().environment == "production": + print( + "ERROR: ADMIN_PASSWORD is not set. Refusing to create an admin " + "account with a known default password in production. Set the " + "ADMIN_PASSWORD environment variable and restart.", + file=sys.stderr, + ) + raise SystemExit(1) + import secrets + + admin_password = secrets.token_urlsafe(16) + print( + "NOTICE: ADMIN_PASSWORD not set — generated a one-time random " + f"password for the new admin account: {admin_password}", + ) user = User( email=os.environ.get("ADMIN_EMAIL", "admin@media-on.de"), name="Administrator", - password_hash=hash_password(os.environ.get("ADMIN_PASSWORD", "Admin123!")), + password_hash=hash_password(admin_password), is_active=True, is_system_admin=True, preferences={}, @@ -95,7 +120,7 @@ async def seed(): ) db.add(ut) await db.flush() - print(f"Created user_tenant link with admin role") + print("Created user_tenant link with admin role") else: print(f"User exists: {user.email} (id: {user.id})") # Ensure existing admin has is_system_admin=True