fix(security): F30 (Astra P1) — kein bekanntes Admin-Standardpasswort mehr
Vorher: seed_admin.py und docker-compose.yaml enthielten einen festen
Passwort-Fallback (Admin123!) — ein frisches Volume erzeugte ein
nutzbares Konto mit bekanntem Zugang. Auch die laufende Produktion
nutzte diesen Default (im Container verifiziert).
Fix:
- seed_admin.py: Bei NEUER Admin-Anlage ohne gesetztes ADMIN_PASSWORD
bricht der Start in Produktion AB (vor Benutzeranlage); in Dev wird
ein einmaliges Zufallspasswort generiert und ausgegeben. Bestehende
Admin-Accounts werden uebersprungen (kein Passwortgebrauch) — der
naechste Deploy laeuft also auch ohne gesetzte Variable weiter.
- docker-compose.yaml: ${ADMIN_PASSWORD:-Admin123!} -> required
(${ADMIN_PASSWORD:?...}) — kein Default mehr.
- .env.example/.env.docker.example: Default durch CHANGE_ME-Hinweis
ersetzt.
Abnahme (Astra): Ein frisches Volume ohne gesetztes Geheimnis erzeugt
kein nutzbares Konto mit festem Standardpasswort — erfuellt.
This commit is contained in:
+1
-1
@@ -54,7 +54,7 @@ BCRYPT_ROUNDS=12
|
|||||||
|
|
||||||
# --- Admin user (seeded on first start) --------------------------------------
|
# --- Admin user (seeded on first start) --------------------------------------
|
||||||
ADMIN_EMAIL=admin@example.com
|
ADMIN_EMAIL=admin@example.com
|
||||||
ADMIN_PASSWORD=Admin123!
|
ADMIN_PASSWORD=CHANGE_ME_generate_a_strong_password
|
||||||
|
|
||||||
# --- MAIL_ENCRYPTION_KEY (REQUIRED) -------------------------------------------
|
# --- MAIL_ENCRYPTION_KEY (REQUIRED) -------------------------------------------
|
||||||
# AES-256 encryption key for mail account passwords (Fernet).
|
# AES-256 encryption key for mail account passwords (Fernet).
|
||||||
|
|||||||
+1
-1
@@ -134,4 +134,4 @@ API_GIT_BRANCH=main
|
|||||||
|
|
||||||
# === Admin User (auto-seeded on first start) ===
|
# === Admin User (auto-seeded on first start) ===
|
||||||
ADMIN_EMAIL=admin@media-on.de
|
ADMIN_EMAIL=admin@media-on.de
|
||||||
ADMIN_PASSWORD=Admin123!
|
ADMIN_PASSWORD=CHANGE_ME_generate_a_strong_password
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ services:
|
|||||||
SMTP_TLS: ${SMTP_TLS:-true}
|
SMTP_TLS: ${SMTP_TLS:-true}
|
||||||
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
|
BCRYPT_ROUNDS: ${BCRYPT_ROUNDS:-12}
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@media-on.de}
|
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}
|
MAIL_ENCRYPTION_KEY: ${MAIL_ENCRYPTION_KEY:?MAIL_ENCRYPTION_KEY is required}
|
||||||
volumes:
|
volumes:
|
||||||
- storage:/data/storage
|
- storage:/data/storage
|
||||||
|
|||||||
+33
-8
@@ -16,20 +16,21 @@ for initial bootstrap on a fresh database.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
# Ensure app is importable
|
# Ensure app is importable
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
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 import select
|
||||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
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():
|
async def seed():
|
||||||
# Use migration engine to bypass RLS for bootstrap
|
# Use migration engine to bypass RLS for bootstrap
|
||||||
@@ -73,10 +74,34 @@ async def seed():
|
|||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
if user is 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(
|
user = User(
|
||||||
email=os.environ.get("ADMIN_EMAIL", "admin@media-on.de"),
|
email=os.environ.get("ADMIN_EMAIL", "admin@media-on.de"),
|
||||||
name="Administrator",
|
name="Administrator",
|
||||||
password_hash=hash_password(os.environ.get("ADMIN_PASSWORD", "Admin123!")),
|
password_hash=hash_password(admin_password),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
is_system_admin=True,
|
is_system_admin=True,
|
||||||
preferences={},
|
preferences={},
|
||||||
@@ -95,7 +120,7 @@ async def seed():
|
|||||||
)
|
)
|
||||||
db.add(ut)
|
db.add(ut)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
print(f"Created user_tenant link with admin role")
|
print("Created user_tenant link with admin role")
|
||||||
else:
|
else:
|
||||||
print(f"User exists: {user.email} (id: {user.id})")
|
print(f"User exists: {user.email} (id: {user.id})")
|
||||||
# Ensure existing admin has is_system_admin=True
|
# Ensure existing admin has is_system_admin=True
|
||||||
|
|||||||
Reference in New Issue
Block a user