- Bake version info (commit, branch, date) into /app/version.json at build time
via Docker ARG GIT_COMMIT/GIT_BRANCH/GIT_COMMIT_DATE
- Mount source directory as /app-source for in-container git operations
- Add git config safe.directory for /app-source (ownership mismatch fix)
- Add SystemConfig fields: git_repo_url, git_branch, git_token_encrypted
- Add DB migrations for the three new columns
- Add git_token encryption in update_settings() handler
- New endpoints:
GET /api/settings/version — current version + latest from Gitea API
POST /api/settings/update — DB backup + git pull + docker compose rebuild
- New service: app/services/update_service.py
get_current_version() — reads /app/version.json
check_for_updates() — queries Gitea API for latest commit on branch
backup_database() — timestamped SQLite copy to /app/backups/
trigger_update() — git pull + fire-and-forget compose rebuild
- New script: update.sh — SSH-based manual update with health check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.9 KiB
Python
164 lines
5.9 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
|
|
# Windows DNS
|
|
dns_enabled: bool = False
|
|
dns_server: str = ""
|
|
dns_username: str = ""
|
|
dns_password: str = "" # decrypted
|
|
dns_zone: str = ""
|
|
dns_record_ip: str = ""
|
|
# LDAP
|
|
ldap_enabled: bool = False
|
|
ldap_server: str = ""
|
|
ldap_port: int = 389
|
|
ldap_use_ssl: bool = False
|
|
ldap_bind_dn: str = ""
|
|
ldap_bind_password: str = "" # decrypted
|
|
ldap_base_dn: str = ""
|
|
ldap_user_filter: str = "(sAMAccountName={username})"
|
|
ldap_group_dn: str = ""
|
|
# Update management
|
|
git_repo_url: str = ""
|
|
git_branch: str = "main"
|
|
git_token: str = "" # decrypted
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 = ""
|
|
try:
|
|
dns_password = decrypt_value(row.dns_password_encrypted) if row.dns_password_encrypted else ""
|
|
except Exception:
|
|
dns_password = ""
|
|
try:
|
|
ldap_bind_password = decrypt_value(row.ldap_bind_password_encrypted) if row.ldap_bind_password_encrypted else ""
|
|
except Exception:
|
|
ldap_bind_password = ""
|
|
try:
|
|
git_token = decrypt_value(row.git_token_encrypted) if row.git_token_encrypted else ""
|
|
except Exception:
|
|
git_token = ""
|
|
|
|
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),
|
|
dns_enabled=bool(getattr(row, "dns_enabled", False)),
|
|
dns_server=getattr(row, "dns_server", "") or "",
|
|
dns_username=getattr(row, "dns_username", "") or "",
|
|
dns_password=dns_password,
|
|
dns_zone=getattr(row, "dns_zone", "") or "",
|
|
dns_record_ip=getattr(row, "dns_record_ip", "") or "",
|
|
ldap_enabled=bool(getattr(row, "ldap_enabled", False)),
|
|
ldap_server=getattr(row, "ldap_server", "") or "",
|
|
ldap_port=getattr(row, "ldap_port", 389) or 389,
|
|
ldap_use_ssl=bool(getattr(row, "ldap_use_ssl", False)),
|
|
ldap_bind_dn=getattr(row, "ldap_bind_dn", "") or "",
|
|
ldap_bind_password=ldap_bind_password,
|
|
ldap_base_dn=getattr(row, "ldap_base_dn", "") or "",
|
|
ldap_user_filter=getattr(row, "ldap_user_filter", "(sAMAccountName={username})") or "(sAMAccountName={username})",
|
|
ldap_group_dn=getattr(row, "ldap_group_dn", "") or "",
|
|
git_repo_url=getattr(row, "git_repo_url", "") or "",
|
|
git_branch=getattr(row, "git_branch", "main") or "main",
|
|
git_token=git_token,
|
|
)
|