Files
NetBirdMSP-Appliance/app/utils/config.py
twothatit 72bad11129 security: apply four immediate security fixes
Fix #1 - SECRET_KEY startup validation (config.py, .env):
- App refuses to start if SECRET_KEY is missing, shorter than 32 chars,
  or matches a known insecure default value
- .env: replaced hardcoded test key with placeholder + generation hint

Fix #2 - Docker socket proxy (docker-compose.yml):
- Add tecnativa/docker-socket-proxy sidecar
- Only expose required Docker API endpoints (CONTAINERS, IMAGES,
  NETWORKS, POST, EXEC); dangerous endpoints explicitly blocked
- Remove direct /var/run/docker.sock mount from main container
- Route Docker API via DOCKER_HOST=tcp://docker-socket-proxy:2375

Fix #3 - Azure AD group whitelist (auth.py, models.py, validators.py):
- New azure_allowed_group_id field in SystemConfig
- After token exchange, verify group membership via Graph API /me/memberOf
- Deny login with HTTP 403 if user is not in the required group
- New Azure AD users now get role 'viewer' instead of 'admin'

Fix #4 - Rate limiting on login (main.py, auth.py, requirements.txt):
- Add slowapi==0.1.9 dependency
- Initialize SlowAPI limiter in main.py with 429 exception handler
- Apply 10 requests/minute limit per IP on /login and /mfa/verify
2026-02-18 21:28:49 +01:00

113 lines
3.7 KiB
Python

"""Configuration management — loads all settings from the database (system_config table).
There is NO .env file for application config. The install.sh script collects values
interactively and seeds them into the database. The Web UI settings page allows
editing every value at runtime.
"""
import os
from dataclasses import dataclass
from typing import Optional
from sqlalchemy.orm import Session
from app.utils.security import decrypt_value
@dataclass
class AppConfig:
"""In-memory snapshot of system configuration."""
base_domain: str
admin_email: str
npm_api_url: str
npm_api_email: str # decrypted — NPM login email
npm_api_password: str # decrypted — NPM login password
netbird_management_image: str
netbird_signal_image: str
netbird_relay_image: str
netbird_dashboard_image: str
data_dir: str
docker_network: str
relay_base_port: int
dashboard_base_port: int
ssl_mode: str
wildcard_cert_id: int | None
# ---------------------------------------------------------------------------
# Environment-level settings (not stored in DB)
# ---------------------------------------------------------------------------
# Known insecure default values that must never be used in production.
_INSECURE_KEY_VALUES: set[str] = {
"change-me-in-production",
"local-test-secret-key-not-for-production-1234",
"secret",
"changeme",
"",
}
SECRET_KEY: str = os.environ.get("SECRET_KEY", "")
# --- Startup security gate ---
# Abort immediately if the key is missing, too short, or a known default.
_MIN_KEY_LENGTH = 32
if SECRET_KEY in _INSECURE_KEY_VALUES or len(SECRET_KEY) < _MIN_KEY_LENGTH:
raise RuntimeError(
"FATAL: SECRET_KEY is insecure, missing, or too short.\n"
f" Current length : {len(SECRET_KEY)} characters (minimum: {_MIN_KEY_LENGTH})\n"
" The key must be at least 32 random characters and must not be a known default value.\n"
" Generate a secure key with:\n"
" python3 -c \"import secrets; print(secrets.token_hex(32))\"\n"
" Then set it in your .env file as: SECRET_KEY=<generated-value>"
)
DATABASE_PATH: str = os.environ.get("DATABASE_PATH", "/app/data/netbird_msp.db")
LOG_LEVEL: str = os.environ.get("LOG_LEVEL", "INFO")
JWT_ALGORITHM: str = "HS256"
JWT_EXPIRE_MINUTES: int = 480 # 8 hours
def get_system_config(db: Session) -> Optional[AppConfig]:
"""Load the singleton SystemConfig row and return an AppConfig dataclass.
Args:
db: Active SQLAlchemy session.
Returns:
AppConfig instance or None if the system_config row does not exist yet.
"""
from app.models import SystemConfig
row = db.query(SystemConfig).filter(SystemConfig.id == 1).first()
if row is None:
return None
try:
npm_email = decrypt_value(row.npm_api_email_encrypted)
except Exception:
npm_email = ""
try:
npm_password = decrypt_value(row.npm_api_password_encrypted)
except Exception:
npm_password = ""
return AppConfig(
base_domain=row.base_domain,
admin_email=row.admin_email,
npm_api_url=row.npm_api_url,
npm_api_email=npm_email,
npm_api_password=npm_password,
netbird_management_image=row.netbird_management_image,
netbird_signal_image=row.netbird_signal_image,
netbird_relay_image=row.netbird_relay_image,
netbird_dashboard_image=row.netbird_dashboard_image,
data_dir=row.data_dir,
docker_network=row.docker_network,
relay_base_port=row.relay_base_port,
dashboard_base_port=getattr(row, "dashboard_base_port", 9000) or 9000,
ssl_mode=getattr(row, "ssl_mode", "letsencrypt") or "letsencrypt",
wildcard_cert_id=getattr(row, "wildcard_cert_id", None),
)