790 lines
33 KiB
Python
790 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
sovereign-configure — interactive configurator for Sovereign group_vars/all.yml
|
|
|
|
Prompts for the handful of deployment-specific values (domain, org name,
|
|
server IP, admin email), auto-generates every password and cryptographic
|
|
secret, and writes a ready-to-use group_vars/all.yml.
|
|
|
|
Usage:
|
|
python3 configure.py # writes to inventories/production/group_vars/all.yml
|
|
python3 configure.py -o /path/to/all.yml # custom output path
|
|
python3 configure.py --stdout # print to stdout (no file written)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import os
|
|
import secrets
|
|
import string
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
|
|
BOLD = "\033[1m"
|
|
DIM = "\033[2m"
|
|
GREEN = "\033[32m"
|
|
CYAN = "\033[36m"
|
|
YELLOW = "\033[33m"
|
|
RED = "\033[31m"
|
|
RESET = "\033[0m"
|
|
|
|
|
|
def _strip_colour() -> None:
|
|
"""Disable all ANSI codes when stdout is not a TTY."""
|
|
global BOLD, DIM, GREEN, CYAN, YELLOW, RED, RESET
|
|
BOLD = DIM = GREEN = CYAN = YELLOW = RED = RESET = ""
|
|
|
|
|
|
# ── Crypto helpers ─────────────────────────────────────────────────────────────
|
|
|
|
_ALPHA = string.ascii_letters + string.digits
|
|
|
|
|
|
def gen_password(length: int = 24) -> str:
|
|
"""Alphanumeric password — safe to embed in YAML without escaping."""
|
|
return "".join(secrets.choice(_ALPHA) for _ in range(length))
|
|
|
|
|
|
def gen_secret(length: int = 50) -> str:
|
|
"""Alphanumeric + hyphens/underscores — for tokens and secret keys."""
|
|
return "".join(secrets.choice(_ALPHA + "-_") for _ in range(length))
|
|
|
|
|
|
def sha256(value: str) -> str:
|
|
return hashlib.sha256(value.encode()).hexdigest()
|
|
|
|
|
|
def htpasswd_sha1(username: str, password: str) -> str:
|
|
"""Return an htpasswd entry with SHA-1 digest (accepted by Traefik basicauth)."""
|
|
digest = base64.b64encode(hashlib.sha1(password.encode()).digest()).decode()
|
|
return f"{username}:{{SHA}}{digest}"
|
|
|
|
|
|
def yaml_str(value: str) -> str:
|
|
"""Escape a value for safe embedding inside a YAML double-quoted scalar."""
|
|
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
|
|
|
|
# ── UI helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
def _ui(*args, **kwargs) -> None:
|
|
"""Print to stderr so interactive UI is separate from --stdout YAML output."""
|
|
print(*args, file=sys.stderr, **kwargs)
|
|
|
|
|
|
def header(title: str) -> None:
|
|
width = 64
|
|
_ui(f"\n{CYAN}{'─' * width}{RESET}")
|
|
_ui(f"{CYAN} {BOLD}{title}{RESET}")
|
|
_ui(f"{CYAN}{'─' * width}{RESET}")
|
|
|
|
|
|
def info(msg: str) -> None:
|
|
_ui(f" {DIM}{msg}{RESET}")
|
|
|
|
|
|
def generated(label: str, value: str, sensitive: bool = False) -> None:
|
|
display = "···" if sensitive else f"{GREEN}{value}{RESET}"
|
|
_ui(f" {DIM}↳ auto-generated {label}:{RESET} {display}")
|
|
|
|
|
|
def prompt(
|
|
label: str,
|
|
default: str = "",
|
|
required: bool = False,
|
|
choices: list[str] | None = None,
|
|
) -> str:
|
|
"""Prompt for input; return default on empty Enter."""
|
|
if choices:
|
|
choice_hint = f"{DIM}({'/'.join(choices)}){RESET} "
|
|
else:
|
|
choice_hint = ""
|
|
|
|
if default:
|
|
default_hint = f" {DIM}[{default}]{RESET}"
|
|
elif required:
|
|
default_hint = f" {RED}(required){RESET}"
|
|
else:
|
|
default_hint = ""
|
|
|
|
sys.stderr.write(f" {choice_hint}{label}{default_hint}: ")
|
|
sys.stderr.flush()
|
|
|
|
try:
|
|
value = input().strip()
|
|
except (KeyboardInterrupt, EOFError):
|
|
_ui(f"\n{YELLOW}Aborted.{RESET}")
|
|
sys.exit(1)
|
|
|
|
if not value and required and not default:
|
|
_ui(f" {RED}✖ This field cannot be empty.{RESET}")
|
|
return prompt(label, default, required, choices)
|
|
|
|
if choices and value and value not in choices:
|
|
_ui(f" {RED}✖ Must be one of: {', '.join(choices)}{RESET}")
|
|
return prompt(label, default, required, choices)
|
|
|
|
return value or default
|
|
|
|
|
|
# ── Configuration dataclass ───────────────────────────────────────────────────
|
|
|
|
@dataclass
|
|
class Config:
|
|
# Core
|
|
base_domain: str
|
|
tenant_name: str
|
|
tenant_logo_local_path: str
|
|
tenant_primary_color: str
|
|
tenant_accent_color: str
|
|
sovereign_base_dir: str
|
|
|
|
# Traefik
|
|
traefik_acme_email: str
|
|
traefik_dashboard_user: str
|
|
traefik_dashboard_password_plain: str
|
|
traefik_dashboard_htpasswd: str
|
|
|
|
# Authentik
|
|
authentik_version: str
|
|
authentik_secret_key: str
|
|
authentik_db_password: str
|
|
authentik_admin_email: str
|
|
authentik_admin_password: str
|
|
|
|
# Graylog
|
|
graylog_version: str
|
|
graylog_password_secret: str
|
|
graylog_admin_password: str
|
|
graylog_root_password_sha2: str
|
|
graylog_host: str
|
|
|
|
# Stalwart
|
|
stalwart_admin_password: str
|
|
|
|
# Roundcube
|
|
roundcube_db_password: str
|
|
roundcube_des_key: str
|
|
|
|
# Wazuh
|
|
wazuh_admin_password: str
|
|
wazuh_api_password: str
|
|
|
|
# Headscale / WireGuard
|
|
wireguard_port: int
|
|
|
|
# Matrix
|
|
matrix_version: str
|
|
matrix_registration_secret: str
|
|
matrix_db_password: str
|
|
|
|
# Jitsi
|
|
jitsi_version: str
|
|
jitsi_jicofo_auth_password: str
|
|
jitsi_jvb_auth_password: str
|
|
jitsi_jibri_recorder_password: str
|
|
jitsi_jibri_xmpp_password: str
|
|
jitsi_turn_secret: str
|
|
|
|
# MinIO
|
|
minio_root_user: str
|
|
minio_root_password: str
|
|
minio_nextcloud_access_key: str
|
|
minio_nextcloud_secret_key: str
|
|
|
|
# Nextcloud
|
|
nextcloud_version: str
|
|
nextcloud_admin_user: str
|
|
nextcloud_admin_password: str
|
|
nextcloud_db_password: str
|
|
nextcloud_db_root_password: str
|
|
|
|
# Vaultwarden
|
|
vaultwarden_admin_token: str
|
|
vaultwarden_db_password: str
|
|
|
|
# Forgejo
|
|
forgejo_admin_user: str
|
|
forgejo_admin_email: str
|
|
forgejo_admin_password: str
|
|
forgejo_db_password: str
|
|
forgejo_secret_key: str
|
|
forgejo_internal_token: str
|
|
forgejo_lfs_jwt_secret: str
|
|
forgejo_ssh_port: int
|
|
|
|
# SMTP
|
|
smtp_host: str
|
|
smtp_port: int
|
|
smtp_password: str
|
|
smtp_tls: str
|
|
|
|
# DNS
|
|
bind_version: str
|
|
dns_server_ip: str
|
|
stalwart_dkim_selector: str
|
|
dmarc_policy: str
|
|
|
|
|
|
# ── Collection ─────────────────────────────────────────────────────────────────
|
|
|
|
def collect() -> Config:
|
|
"""Walk the user through each configuration section."""
|
|
|
|
_ui(f"\n{BOLD}{'═' * 64}{RESET}")
|
|
_ui(f"{BOLD} Sovereign Deployment Configurator{RESET}")
|
|
_ui(f"{BOLD}{'═' * 64}{RESET}")
|
|
_ui(f"""
|
|
This wizard generates a customised {DIM}group_vars/all.yml{RESET} for a new
|
|
Sovereign deployment.
|
|
|
|
You will be prompted for a handful of deployment-specific values.
|
|
All passwords and cryptographic secrets are auto-generated using a
|
|
cryptographically secure random source.
|
|
|
|
{YELLOW}⚠ Keep the output file secure — it contains all service credentials.{RESET}
|
|
""")
|
|
|
|
# ── Core ──────────────────────────────────────────────────────────────────
|
|
header("Core")
|
|
base_domain = prompt("Base domain (e.g. acme.com)", required=True)
|
|
tenant_name = prompt("Organisation name", "My Organisation")
|
|
tenant_logo_local_path = prompt(
|
|
"Logo path on Ansible controller (blank to skip)", ""
|
|
)
|
|
tenant_primary_color = prompt("Primary brand colour (hex)", "#2563eb")
|
|
tenant_accent_color = prompt("Accent colour (hex)", "#1e40af")
|
|
sovereign_base_dir = prompt("Data directory on server", "/opt/sovereign")
|
|
|
|
# Derive a shared admin email used as default for several services
|
|
admin_email = prompt("Admin email address", f"admin@{base_domain}")
|
|
|
|
# ── Traefik ───────────────────────────────────────────────────────────────
|
|
header("Traefik (reverse proxy + TLS)")
|
|
traefik_acme_email = prompt("ACME / Let's Encrypt email", admin_email)
|
|
traefik_dashboard_user = prompt("Dashboard username", "admin")
|
|
traefik_dashboard_plain = gen_password(20)
|
|
traefik_dashboard_htpasswd = htpasswd_sha1(traefik_dashboard_user, traefik_dashboard_plain)
|
|
generated("Traefik dashboard password", traefik_dashboard_plain)
|
|
|
|
# ── Authentik ─────────────────────────────────────────────────────────────
|
|
header("Authentik (identity provider)")
|
|
authentik_version = prompt("Version", "2024.10.5")
|
|
authentik_secret_key = gen_secret(50)
|
|
authentik_db_password = gen_password(24)
|
|
authentik_admin_email = admin_email
|
|
authentik_admin_password = gen_password(20)
|
|
generated("Authentik admin password", authentik_admin_password)
|
|
|
|
# ── Graylog ───────────────────────────────────────────────────────────────
|
|
header("Graylog (centralised logging)")
|
|
graylog_version = prompt("Version", "6.0")
|
|
graylog_password_secret = gen_secret(32)
|
|
graylog_admin_password = gen_password(20)
|
|
graylog_root_password_sha2 = sha256(graylog_admin_password)
|
|
graylog_host = prompt(
|
|
"Host IP reachable from Docker containers (for GELF UDP)",
|
|
"127.0.0.1",
|
|
)
|
|
generated("Graylog admin password", graylog_admin_password)
|
|
|
|
# ── Stalwart ──────────────────────────────────────────────────────────────
|
|
header("Stalwart (mail server)")
|
|
stalwart_admin_password = gen_password(20)
|
|
generated("Stalwart admin password", stalwart_admin_password)
|
|
|
|
# ── Roundcube ─────────────────────────────────────────────────────────────
|
|
# (no user-facing prompts; all auto-generated)
|
|
roundcube_db_password = gen_password(24)
|
|
roundcube_des_key = gen_password(24)
|
|
|
|
# ── Wazuh ─────────────────────────────────────────────────────────────────
|
|
header("Wazuh (security monitoring)")
|
|
wazuh_admin_password = gen_password(20)
|
|
wazuh_api_password = gen_password(20)
|
|
generated("Wazuh admin password", wazuh_admin_password)
|
|
|
|
# ── Headscale / WireGuard ─────────────────────────────────────────────────
|
|
header("Headscale (WireGuard VPN)")
|
|
wireguard_port = int(prompt("WireGuard UDP port", "51820"))
|
|
|
|
# ── Matrix / Element ──────────────────────────────────────────────────────
|
|
header("Matrix / Element (team chat)")
|
|
matrix_version = prompt("Synapse version", "v1.118.0")
|
|
matrix_registration_secret = gen_secret(32)
|
|
matrix_db_password = gen_password(24)
|
|
|
|
# ── Jitsi ─────────────────────────────────────────────────────────────────
|
|
header("Jitsi Meet (video conferencing)")
|
|
jitsi_version = prompt("Version", "stable-9753")
|
|
jitsi_jicofo_auth_password = gen_password(20)
|
|
jitsi_jvb_auth_password = gen_password(20)
|
|
jitsi_jibri_recorder_password = gen_password(20)
|
|
jitsi_jibri_xmpp_password = gen_password(20)
|
|
jitsi_turn_secret = gen_secret(32)
|
|
|
|
# ── MinIO ─────────────────────────────────────────────────────────────────
|
|
header("MinIO (object storage)")
|
|
minio_root_user = prompt("Root username", "minioadmin")
|
|
minio_root_password = gen_password(24)
|
|
minio_nextcloud_access_key = prompt("Nextcloud bucket access key", "nextcloud")
|
|
minio_nextcloud_secret_key = gen_password(32)
|
|
generated("MinIO root password", minio_root_password)
|
|
|
|
# ── Nextcloud ─────────────────────────────────────────────────────────────
|
|
header("Nextcloud (file sync)")
|
|
nextcloud_version = prompt("Version", "29")
|
|
nextcloud_admin_user = prompt("Admin username", "admin")
|
|
nextcloud_admin_password = gen_password(20)
|
|
nextcloud_db_password = gen_password(24)
|
|
nextcloud_db_root_password = gen_password(24)
|
|
generated("Nextcloud admin password", nextcloud_admin_password)
|
|
|
|
# ── Vaultwarden ───────────────────────────────────────────────────────────
|
|
header("Vaultwarden (password manager)")
|
|
vaultwarden_admin_token = gen_secret(40)
|
|
vaultwarden_db_password = gen_password(24)
|
|
generated("Vaultwarden admin token", vaultwarden_admin_token, sensitive=True)
|
|
|
|
# ── Forgejo ───────────────────────────────────────────────────────────────
|
|
header("Forgejo (Git hosting)")
|
|
forgejo_admin_user = prompt("Admin username", "admin")
|
|
forgejo_admin_email = admin_email
|
|
forgejo_admin_password = gen_password(20)
|
|
forgejo_db_password = gen_password(24)
|
|
forgejo_secret_key = gen_secret(50)
|
|
forgejo_internal_token = gen_secret(50)
|
|
forgejo_lfs_jwt_secret = gen_secret(32)
|
|
forgejo_ssh_port = int(prompt("SSH port", "2222"))
|
|
generated("Forgejo admin password", forgejo_admin_password)
|
|
|
|
# ── SMTP ──────────────────────────────────────────────────────────────────
|
|
header("SMTP (outbound email)")
|
|
info("Defaults to the bundled Stalwart server.")
|
|
info("Change smtp_host in the output file to use an external relay.")
|
|
smtp_host = prompt("SMTP host", "stalwart")
|
|
smtp_port = int(prompt("SMTP port", "587"))
|
|
smtp_tls = prompt("TLS mode", "starttls", choices=["starttls", "tls", "none"])
|
|
smtp_password = gen_password(20)
|
|
|
|
# ── DNS / BIND9 ───────────────────────────────────────────────────────────
|
|
header("DNS / BIND9 (authoritative nameserver)")
|
|
info(f"After deployment, register ns1.{base_domain} as a glue record at")
|
|
info(f"your domain registrar pointing to this server's IP, then set the")
|
|
info(f"domain nameservers to ns1.{base_domain}.")
|
|
dns_server_ip = prompt("Server public IPv4 address", required=True)
|
|
bind_version = prompt("BIND9 image version", "9.18-22.04_beta")
|
|
|
|
info("")
|
|
info("DKIM: retrieve the public key from the Stalwart admin UI after")
|
|
info("first deployment. Add it to this file to publish the DKIM record.")
|
|
stalwart_dkim_selector = prompt("DKIM selector", "default")
|
|
dmarc_policy = prompt(
|
|
"DMARC policy",
|
|
"quarantine",
|
|
choices=["none", "quarantine", "reject"],
|
|
)
|
|
|
|
return Config(
|
|
base_domain=base_domain,
|
|
tenant_name=tenant_name,
|
|
tenant_logo_local_path=tenant_logo_local_path,
|
|
tenant_primary_color=tenant_primary_color,
|
|
tenant_accent_color=tenant_accent_color,
|
|
sovereign_base_dir=sovereign_base_dir,
|
|
traefik_acme_email=traefik_acme_email,
|
|
traefik_dashboard_user=traefik_dashboard_user,
|
|
traefik_dashboard_password_plain=traefik_dashboard_plain,
|
|
traefik_dashboard_htpasswd=traefik_dashboard_htpasswd,
|
|
authentik_version=authentik_version,
|
|
authentik_secret_key=authentik_secret_key,
|
|
authentik_db_password=authentik_db_password,
|
|
authentik_admin_email=authentik_admin_email,
|
|
authentik_admin_password=authentik_admin_password,
|
|
graylog_version=graylog_version,
|
|
graylog_password_secret=graylog_password_secret,
|
|
graylog_admin_password=graylog_admin_password,
|
|
graylog_root_password_sha2=graylog_root_password_sha2,
|
|
graylog_host=graylog_host,
|
|
stalwart_admin_password=stalwart_admin_password,
|
|
roundcube_db_password=roundcube_db_password,
|
|
roundcube_des_key=roundcube_des_key,
|
|
wazuh_admin_password=wazuh_admin_password,
|
|
wazuh_api_password=wazuh_api_password,
|
|
wireguard_port=wireguard_port,
|
|
matrix_version=matrix_version,
|
|
matrix_registration_secret=matrix_registration_secret,
|
|
matrix_db_password=matrix_db_password,
|
|
jitsi_version=jitsi_version,
|
|
jitsi_jicofo_auth_password=jitsi_jicofo_auth_password,
|
|
jitsi_jvb_auth_password=jitsi_jvb_auth_password,
|
|
jitsi_jibri_recorder_password=jitsi_jibri_recorder_password,
|
|
jitsi_jibri_xmpp_password=jitsi_jibri_xmpp_password,
|
|
jitsi_turn_secret=jitsi_turn_secret,
|
|
minio_root_user=minio_root_user,
|
|
minio_root_password=minio_root_password,
|
|
minio_nextcloud_access_key=minio_nextcloud_access_key,
|
|
minio_nextcloud_secret_key=minio_nextcloud_secret_key,
|
|
nextcloud_version=nextcloud_version,
|
|
nextcloud_admin_user=nextcloud_admin_user,
|
|
nextcloud_admin_password=nextcloud_admin_password,
|
|
nextcloud_db_password=nextcloud_db_password,
|
|
nextcloud_db_root_password=nextcloud_db_root_password,
|
|
vaultwarden_admin_token=vaultwarden_admin_token,
|
|
vaultwarden_db_password=vaultwarden_db_password,
|
|
forgejo_admin_user=forgejo_admin_user,
|
|
forgejo_admin_email=forgejo_admin_email,
|
|
forgejo_admin_password=forgejo_admin_password,
|
|
forgejo_db_password=forgejo_db_password,
|
|
forgejo_secret_key=forgejo_secret_key,
|
|
forgejo_internal_token=forgejo_internal_token,
|
|
forgejo_lfs_jwt_secret=forgejo_lfs_jwt_secret,
|
|
forgejo_ssh_port=forgejo_ssh_port,
|
|
smtp_host=smtp_host,
|
|
smtp_port=smtp_port,
|
|
smtp_password=smtp_password,
|
|
smtp_tls=smtp_tls,
|
|
bind_version=bind_version,
|
|
dns_server_ip=dns_server_ip,
|
|
stalwart_dkim_selector=stalwart_dkim_selector,
|
|
dmarc_policy=dmarc_policy,
|
|
)
|
|
|
|
|
|
# ── YAML renderer ──────────────────────────────────────────────────────────────
|
|
|
|
def render(c: Config, generated_at: str) -> str:
|
|
"""Return the full content of group_vars/all.yml for this Config."""
|
|
|
|
# Shorthand so f-string lines stay readable
|
|
d = yaml_str(c.base_domain)
|
|
|
|
def s(value: str) -> str:
|
|
"""Double-quoted YAML scalar."""
|
|
return f'"{yaml_str(value)}"'
|
|
|
|
return f"""\
|
|
---
|
|
# =============================================================================
|
|
# SOVEREIGN DEPLOYMENT CONFIGURATION
|
|
# Generated by configure.py on {generated_at}
|
|
# Domain: {c.base_domain}
|
|
# =============================================================================
|
|
|
|
# Base domain — all service subdomains are derived from this.
|
|
base_domain: "{d}"
|
|
|
|
# =============================================================================
|
|
# BRANDING
|
|
# Applied across all services that support custom branding.
|
|
# =============================================================================
|
|
|
|
# Display name shown in service UIs and email subjects.
|
|
tenant_name: {s(c.tenant_name)}
|
|
|
|
# Path to a logo image on the Ansible control machine (PNG or SVG recommended).
|
|
# Leave empty to use each service's default logo.
|
|
tenant_logo_local_path: {s(c.tenant_logo_local_path)}
|
|
|
|
# Primary brand colour (hex). Used for backgrounds, buttons, and highlights.
|
|
tenant_primary_color: {s(c.tenant_primary_color)}
|
|
|
|
# Accent / secondary colour (hex).
|
|
tenant_accent_color: {s(c.tenant_accent_color)}
|
|
|
|
# Base directory for all service data on the target host.
|
|
sovereign_base_dir: {s(c.sovereign_base_dir)}
|
|
|
|
# =============================================================================
|
|
# TRAEFIK (reverse proxy + TLS termination)
|
|
# =============================================================================
|
|
|
|
traefik_acme_email: {s(c.traefik_acme_email)}
|
|
traefik_domain: "traefik.{yaml_str(c.base_domain)}"
|
|
# htpasswd hash — user: {c.traefik_dashboard_user} plain: {c.traefik_dashboard_password_plain}
|
|
traefik_dashboard_password: {s(c.traefik_dashboard_htpasswd)}
|
|
|
|
# =============================================================================
|
|
# AUTHENTIK (identity provider + SSO)
|
|
# =============================================================================
|
|
|
|
authentik_domain: "auth.{yaml_str(c.base_domain)}"
|
|
authentik_version: {s(c.authentik_version)}
|
|
authentik_secret_key: {s(c.authentik_secret_key)}
|
|
authentik_db_password: {s(c.authentik_db_password)}
|
|
authentik_admin_email: {s(c.authentik_admin_email)}
|
|
authentik_admin_password: {s(c.authentik_admin_password)}
|
|
|
|
# =============================================================================
|
|
# GRAYLOG (centralised logging)
|
|
# =============================================================================
|
|
|
|
graylog_domain: "logs.{yaml_str(c.base_domain)}"
|
|
graylog_version: {s(c.graylog_version)}
|
|
graylog_password_secret: {s(c.graylog_password_secret)}
|
|
# SHA-256 of the Graylog admin password (admin password is in the credentials summary)
|
|
graylog_root_password_sha2: "{c.graylog_root_password_sha2}"
|
|
graylog_host: {s(c.graylog_host)} # host IP reachable from containers
|
|
graylog_gelf_port: 12201
|
|
|
|
# =============================================================================
|
|
# STALWART MAIL (SMTP / IMAP)
|
|
# =============================================================================
|
|
|
|
stalwart_domain: "mail.{yaml_str(c.base_domain)}"
|
|
stalwart_admin_password: {s(c.stalwart_admin_password)}
|
|
stalwart_version: "latest"
|
|
|
|
# =============================================================================
|
|
# ROUNDCUBE (webmail)
|
|
# =============================================================================
|
|
|
|
roundcube_domain: "webmail.{yaml_str(c.base_domain)}"
|
|
roundcube_version: "latest"
|
|
roundcube_db_password: {s(c.roundcube_db_password)}
|
|
roundcube_des_key: {s(c.roundcube_des_key)}
|
|
|
|
# =============================================================================
|
|
# WAZUH (endpoint security + SIEM)
|
|
# =============================================================================
|
|
|
|
wazuh_domain: "wazuh.{yaml_str(c.base_domain)}"
|
|
wazuh_version: "4.9.0"
|
|
wazuh_admin_password: {s(c.wazuh_admin_password)}
|
|
wazuh_api_password: {s(c.wazuh_api_password)}
|
|
|
|
# =============================================================================
|
|
# HEADSCALE (WireGuard mesh VPN)
|
|
# =============================================================================
|
|
|
|
wireguard_domain: "vpn.{yaml_str(c.base_domain)}"
|
|
headscale_domain: "headscale.{yaml_str(c.base_domain)}"
|
|
headscale_version: "0.23.0"
|
|
wireguard_port: {c.wireguard_port}
|
|
headscale_noise_private_key: "" # generated automatically on first run
|
|
|
|
# =============================================================================
|
|
# MATRIX / ELEMENT (team chat)
|
|
# =============================================================================
|
|
|
|
matrix_domain: "matrix.{yaml_str(c.base_domain)}"
|
|
element_domain: "chat.{yaml_str(c.base_domain)}"
|
|
matrix_version: {s(c.matrix_version)}
|
|
matrix_registration_secret: {s(c.matrix_registration_secret)}
|
|
matrix_db_password: {s(c.matrix_db_password)}
|
|
|
|
# =============================================================================
|
|
# JITSI MEET (video conferencing)
|
|
# =============================================================================
|
|
|
|
jitsi_domain: "meet.{yaml_str(c.base_domain)}"
|
|
jitsi_version: {s(c.jitsi_version)}
|
|
jitsi_jicofo_auth_password: {s(c.jitsi_jicofo_auth_password)}
|
|
jitsi_jvb_auth_password: {s(c.jitsi_jvb_auth_password)}
|
|
jitsi_jibri_recorder_password: {s(c.jitsi_jibri_recorder_password)}
|
|
jitsi_jibri_xmpp_password: {s(c.jitsi_jibri_xmpp_password)}
|
|
jitsi_turn_secret: {s(c.jitsi_turn_secret)}
|
|
|
|
# =============================================================================
|
|
# MINIO (S3-compatible object storage)
|
|
# =============================================================================
|
|
|
|
minio_domain: "s3.{yaml_str(c.base_domain)}"
|
|
minio_console_domain: "minio.{yaml_str(c.base_domain)}"
|
|
minio_version: "latest"
|
|
minio_root_user: {s(c.minio_root_user)}
|
|
minio_root_password: {s(c.minio_root_password)}
|
|
minio_nextcloud_bucket: "nextcloud"
|
|
minio_nextcloud_access_key: {s(c.minio_nextcloud_access_key)}
|
|
minio_nextcloud_secret_key: {s(c.minio_nextcloud_secret_key)}
|
|
|
|
# =============================================================================
|
|
# NEXTCLOUD (file sync + collaboration)
|
|
# =============================================================================
|
|
|
|
nextcloud_domain: "cloud.{yaml_str(c.base_domain)}"
|
|
nextcloud_version: {s(c.nextcloud_version)}
|
|
nextcloud_admin_user: {s(c.nextcloud_admin_user)}
|
|
nextcloud_admin_password: {s(c.nextcloud_admin_password)}
|
|
nextcloud_db_password: {s(c.nextcloud_db_password)}
|
|
nextcloud_db_root_password: {s(c.nextcloud_db_root_password)}
|
|
|
|
# =============================================================================
|
|
# VAULTWARDEN (password manager)
|
|
# =============================================================================
|
|
|
|
vaultwarden_domain: "vault.{yaml_str(c.base_domain)}"
|
|
vaultwarden_version: "latest"
|
|
vaultwarden_admin_token: {s(c.vaultwarden_admin_token)}
|
|
vaultwarden_db_password: {s(c.vaultwarden_db_password)}
|
|
|
|
# =============================================================================
|
|
# FORGEJO (Git hosting)
|
|
# =============================================================================
|
|
|
|
forgejo_domain: "git.{yaml_str(c.base_domain)}"
|
|
forgejo_version: "latest"
|
|
forgejo_db_password: {s(c.forgejo_db_password)}
|
|
forgejo_secret_key: {s(c.forgejo_secret_key)}
|
|
forgejo_internal_token: {s(c.forgejo_internal_token)}
|
|
forgejo_lfs_jwt_secret: {s(c.forgejo_lfs_jwt_secret)}
|
|
forgejo_admin_user: {s(c.forgejo_admin_user)}
|
|
forgejo_admin_password: {s(c.forgejo_admin_password)}
|
|
forgejo_admin_email: {s(c.forgejo_admin_email)}
|
|
forgejo_ssh_port: {c.forgejo_ssh_port}
|
|
|
|
# =============================================================================
|
|
# WEBSITE (static landing page)
|
|
# =============================================================================
|
|
|
|
website_nginx_version: "alpine"
|
|
|
|
# =============================================================================
|
|
# SMTP (outbound email)
|
|
# =============================================================================
|
|
|
|
smtp_host: {s(c.smtp_host)}
|
|
smtp_port: {c.smtp_port}
|
|
smtp_from: "noreply@{yaml_str(c.base_domain)}"
|
|
smtp_user: "noreply@{yaml_str(c.base_domain)}"
|
|
smtp_password: {s(c.smtp_password)}
|
|
smtp_tls: {s(c.smtp_tls)}
|
|
|
|
# =============================================================================
|
|
# DNS / BIND9 (authoritative nameserver)
|
|
# =============================================================================
|
|
|
|
# BIND9 Docker image version tag (ubuntu/bind9).
|
|
bind_version: {s(c.bind_version)}
|
|
|
|
# Public IPv4 of this server.
|
|
# Register ns1.{c.base_domain} as a glue record at your domain registrar,
|
|
# then set the domain nameservers to ns1.{c.base_domain}.
|
|
dns_server_ip: {s(c.dns_server_ip)}
|
|
dns_ns_hostname: "ns1.{yaml_str(c.base_domain)}"
|
|
dns_ttl: 3600
|
|
|
|
# DKIM public key — retrieve from the Stalwart admin UI at
|
|
# mail.{c.base_domain} → Settings → DKIM keys after the first deployment,
|
|
# then re-run: ansible-playbook playbooks/site.yml --tags dns
|
|
stalwart_dkim_selector: {s(c.stalwart_dkim_selector)}
|
|
stalwart_dkim_public_key: "" # fill in after first Stalwart deployment
|
|
|
|
# DMARC email authentication policy.
|
|
dmarc_policy: {s(c.dmarc_policy)} # none | quarantine | reject
|
|
dmarc_rua: "mailto:dmarc-reports@{yaml_str(c.base_domain)}"
|
|
dmarc_ruf: "mailto:dmarc-forensics@{yaml_str(c.base_domain)}"
|
|
"""
|
|
|
|
|
|
# ── Credentials summary ────────────────────────────────────────────────────────
|
|
|
|
def print_credentials(c: Config) -> None:
|
|
"""Print a human-readable table of all admin credentials."""
|
|
|
|
col_w = 28
|
|
val_w = 40
|
|
|
|
def row(service: str, detail: str, credential: str) -> None:
|
|
svc = f"{BOLD}{service:<{col_w}}{RESET}"
|
|
det = f"{DIM}{detail:<{col_w}}{RESET}"
|
|
val = f"{GREEN}{credential}{RESET}"
|
|
_ui(f" {svc} {det} {val}")
|
|
|
|
def divider() -> None:
|
|
_ui(f" {DIM}{'─' * (col_w * 2 + val_w + 6)}{RESET}")
|
|
|
|
header("Credential Summary — save this somewhere secure")
|
|
_ui(
|
|
f"\n {DIM}{'Service':<{col_w}} {'Detail':<{col_w}} Credential{RESET}"
|
|
)
|
|
divider()
|
|
|
|
row("Traefik dashboard", f"user: {c.traefik_dashboard_user}", c.traefik_dashboard_password_plain)
|
|
row("Authentik", f"user: {c.authentik_admin_email}", c.authentik_admin_password)
|
|
row("Graylog", "user: admin", c.graylog_admin_password)
|
|
row("Stalwart mail", "admin UI", c.stalwart_admin_password)
|
|
row("Wazuh", "user: admin", c.wazuh_admin_password)
|
|
row("MinIO", f"user: {c.minio_root_user}", c.minio_root_password)
|
|
row("Nextcloud", f"user: {c.nextcloud_admin_user}", c.nextcloud_admin_password)
|
|
row("Vaultwarden", "admin token", c.vaultwarden_admin_token)
|
|
row("Forgejo", f"user: {c.forgejo_admin_user}", c.forgejo_admin_password)
|
|
|
|
divider()
|
|
|
|
_ui(f"""
|
|
{YELLOW}Post-deployment checklist:{RESET}
|
|
1. Point your registrar's nameservers to ns1.{c.base_domain}
|
|
and add a glue record ns1.{c.base_domain} → {c.dns_server_ip}
|
|
2. After Stalwart is running, retrieve its DKIM public key from
|
|
https://mail.{c.base_domain} and add it to stalwart_dkim_public_key
|
|
in all.yml, then re-run: just update-service dns
|
|
3. Wazuh requires TLS certs before first run — see the Wazuh
|
|
Docker documentation for the cert-generation step.
|
|
""")
|
|
|
|
|
|
# ── Entry point ────────────────────────────────────────────────────────────────
|
|
|
|
def main() -> None:
|
|
here = Path(__file__).parent
|
|
default_output = here / "inventories" / "production" / "group_vars" / "all.yml"
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Interactive configurator for Sovereign group_vars/all.yml",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
parser.add_argument(
|
|
"-o", "--output",
|
|
metavar="PATH",
|
|
default=str(default_output),
|
|
help=f"Output file path (default: {default_output})",
|
|
)
|
|
parser.add_argument(
|
|
"--stdout",
|
|
action="store_true",
|
|
help="Print the generated YAML to stdout instead of writing a file",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if not sys.stdout.isatty():
|
|
_strip_colour()
|
|
|
|
config = collect()
|
|
|
|
generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
yaml_content = render(config, generated_at)
|
|
|
|
if args.stdout:
|
|
# YAML to stdout; all UI (prompts, summary) already goes to stderr
|
|
sys.stdout.write(yaml_content)
|
|
else:
|
|
output_path = Path(args.output)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Refuse to silently overwrite an existing configured file
|
|
if output_path.exists():
|
|
header("Output file already exists")
|
|
_ui(f"\n {YELLOW}⚠ {output_path}{RESET}")
|
|
answer = prompt("Overwrite?", choices=["yes", "no"], default="no")
|
|
if answer != "yes":
|
|
_ui(f"\n {DIM}Aborted. Nothing was written.{RESET}\n")
|
|
sys.exit(0)
|
|
|
|
output_path.write_text(yaml_content, encoding="utf-8")
|
|
_ui(f"\n {GREEN}✔ Written to {output_path}{RESET}")
|
|
|
|
print_credentials(config)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|