Adding configuration script

This commit is contained in:
Ian Roddis
2026-04-21 10:28:26 -04:00
parent 043d315b80
commit 18c7e6ad57
3 changed files with 804 additions and 2 deletions
+6 -1
View File
@@ -30,7 +30,12 @@
"Bash(ansible-lint roles/dns/)", "Bash(ansible-lint roles/dns/)",
"Bash(ansible-lint roles/graylog/)", "Bash(ansible-lint roles/graylog/)",
"Bash(ansible-lint .)", "Bash(ansible-lint .)",
"Bash(grep -v \"^WARNING\\\\|^$\\\\|^A new\\\\|^Upgrade\\\\|^Read\")" "Bash(grep -v \"^WARNING\\\\|^$\\\\|^A new\\\\|^Upgrade\\\\|^Read\")",
"Bash(python3 configure.py --stdout)",
"Bash(grep -E \"^\\(---$|base_domain:|traefik_domain:|graylog_root_password_sha2:|dns_server_ip:|dmarc_policy:|forgejo_ssh_port:|stalwart_dkim_selector:\\)\")",
"Bash(python3 -c \"import sys, yaml; data = yaml.safe_load\\(sys.stdin\\); print\\(f'Valid YAML: {len\\(data\\)} keys'\\); print\\('changeme keys:', [k for k,v in data.items\\(\\) if isinstance\\(v,str\\) and 'changeme' in v]\\)\")",
"Bash(python3 -c \"import sys, yaml; d = yaml.safe_load\\(sys.stdin\\); print\\('base_domain:', d['base_domain']\\); print\\('graylog_root_password_sha2 len:', len\\(d['graylog_root_password_sha2']\\)\\); print\\('authentik_secret_key len:', len\\(d['authentik_secret_key']\\)\\)\")",
"Bash(python3 -c \"import sys, yaml; d = yaml.safe_load\\(sys.stdin\\); print\\('base_domain type:', type\\(d['base_domain']\\).__name__, '=', repr\\(d['base_domain']\\)\\); print\\('All 93 keys present:', len\\(d\\) == 93\\)\")"
] ]
} }
} }
+9 -1
View File
@@ -16,7 +16,7 @@ setup-host:
test: test:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
roles=(authentik common forgejo graylog headscale jitsi matrix minio nextcloud roundcube stalwart vaultwarden wazuh website) roles=(authentik common dns forgejo graylog headscale jitsi matrix minio nextcloud roundcube stalwart vaultwarden wazuh website)
failed=() failed=()
for role in "${roles[@]}"; do for role in "${roles[@]}"; do
echo "==> Testing role: $role" echo "==> Testing role: $role"
@@ -49,3 +49,11 @@ check:
# Lint the project # Lint the project
lint: lint:
ansible-lint ansible-lint
# Interactively generate a new group_vars/all.yml for a deployment
configure:
python3 configure.py
# Generate group_vars to a custom path: just configure-to /tmp/all.yml
configure-to path:
python3 configure.py -o {{ path }}
+789
View File
@@ -0,0 +1,789 @@
#!/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()