Adding configuration script
This commit is contained in:
@@ -30,7 +30,12 @@
|
||||
"Bash(ansible-lint roles/dns/)",
|
||||
"Bash(ansible-lint roles/graylog/)",
|
||||
"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\\)\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ setup-host:
|
||||
test:
|
||||
#!/usr/bin/env bash
|
||||
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=()
|
||||
for role in "${roles[@]}"; do
|
||||
echo "==> Testing role: $role"
|
||||
@@ -49,3 +49,11 @@ check:
|
||||
# Lint the project
|
||||
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
@@ -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()
|
||||
Reference in New Issue
Block a user