From c32d52851874638b67ddbb817c72ddb1309463bf Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:37:16 -0400 Subject: [PATCH] Updating README --- README.md | 176 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index be0ddbc..18efff1 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Sovereign is an Ansible project that deploys a complete self-hosted infrastructu | Role | Service | URL | |------|---------|-----| | `common` | Traefik (reverse proxy + TLS) | `traefik.` | +| `dns` | BIND9 (authoritative nameserver) | `ns1.` | | `graylog` | Graylog + OpenSearch + MongoDB | `logs.` | | `authentik` | Authentik (identity provider) | `auth.` | | `minio` | MinIO (object storage) | `s3.`, `minio.` | @@ -49,8 +50,8 @@ Sovereign is an Ansible project that deploys a complete self-hosted infrastructu - Ubuntu 22.04 or 24.04 (amd64) - Root or sudo access -- Ports 80, 443, and 51820/UDP open -- DNS A records pointing `` and `*.` to the host IP +- Ports 80, 443, 51820/UDP, and 53/TCP+UDP open +- Domain registered with a registrar that supports custom nameservers and glue records ### Installing Dependencies @@ -77,14 +78,13 @@ git clone sovereign && cd sovereign pip install -r requirements.txt ansible-galaxy collection install -r requirements.yml -# 3. Configure the target host +# 3. Set the target host connection details export SOVEREIGN_HOST=203.0.113.10 export SOVEREIGN_USER=ubuntu export SOVEREIGN_SSH_KEY=~/.ssh/id_rsa -# 4. Edit the tenant config -cp inventories/production/group_vars/all.yml inventories/production/group_vars/all.yml.bak -$EDITOR inventories/production/group_vars/all.yml +# 4. Generate a complete, deployment-ready config +python3 configure.py # 5. Deploy ansible-playbook playbooks/site.yml @@ -94,27 +94,48 @@ ansible-playbook playbooks/site.yml ## New Tenant Setup -Each deployment is controlled entirely by `inventories/production/group_vars/all.yml`. Follow these steps for every new tenant. +Each deployment is controlled entirely by `inventories/production/group_vars/all.yml`. The recommended way to create this file for a new tenant is with the interactive configurator script. -### 1. Set the base domain +### Using the configurator (recommended) -```yaml -base_domain: "example.com" +```bash +python3 configure.py ``` -All service subdomains are derived from this value automatically. DNS must have an A record for `example.com` and a wildcard `*.example.com` pointing to the server IP before deployment. +The script walks you through each configuration section, prompts for the handful of deployment-specific values (domain name, organisation name, server IP, admin email), and auto-generates every password and cryptographic secret using a cryptographically secure random source. It then writes a complete `group_vars/all.yml` with no `changeme_*` placeholders left, and prints a credential summary to the terminal. -### 2. Replace all secrets +You can also write to a custom path or pipe the YAML to stdout: -Search the config file for every `changeme_*` placeholder and replace with secure values. The sections below describe how to generate each one. +```bash +python3 configure.py -o /path/to/all.yml # custom output path +python3 configure.py --stdout > all.yml # print YAML; prompts go to stderr +just configure # shorthand via Justfile +just configure-to /path/to/all.yml +``` -**General secrets** (use a password manager or `openssl rand -base64 32`): +The configurator prompts for the following values (all others take secure defaults): + +| Prompt | Notes | +|--------|-------| +| Base domain | e.g. `acme.com` — required | +| Organisation name | Shown in service UIs | +| Admin email | Used for ACME/Let's Encrypt and initial admin accounts | +| Graylog host IP | IP reachable from Docker containers for GELF UDP | +| Server public IPv4 | Used to populate DNS A records | +| DMARC policy | `none`, `quarantine`, or `reject` | +| DKIM selector | Defaults to `default` | + +All passwords, secret keys, database credentials, and signing tokens are generated automatically. + +### Manual setup + +If you prefer to configure the file by hand, copy `all.yml`, set `base_domain`, and replace every `changeme_*` placeholder with a secure value. Helper commands for generating specific values: ```bash # Generic random secret openssl rand -base64 32 -# Graylog password_secret (min 16 chars, recommend 64) +# Graylog password_secret (min 16 chars) openssl rand -base64 48 # Graylog root password hash @@ -122,53 +143,48 @@ echo -n 'yourpassword' | sha256sum | awk '{print $1}' # Traefik dashboard password (htpasswd format) htpasswd -nb admin yourpassword -``` -**Authentik secret key** — must be exactly 50 characters: - -```bash +# Authentik secret key (exactly 50 characters) openssl rand -base64 37 | head -c 50 -``` -**Roundcube DES key** — must be exactly 24 characters: - -```bash +# Roundcube DES key (exactly 24 characters) openssl rand -base64 18 | head -c 24 + +# Forgejo tokens (run 3× for secret_key, internal_token, lfs_jwt_secret) +openssl rand -hex 32 ``` -**Forgejo tokens** — generate three separate secrets: +### Post-deployment steps -```bash -# forgejo_secret_key, forgejo_internal_token, forgejo_lfs_jwt_secret -for i in 1 2 3; do openssl rand -hex 32; done -``` +These steps must be completed after the first `ansible-playbook` run regardless of whether you used the configurator or manual setup. -**Vaultwarden admin token** — hash a password with argon2: +#### DNS — nameserver delegation -```bash -# Requires the vaultwarden container to be running, or use the web tool at -# https://argon2.online — use the token output directly -echo -n 'yourpassword' | argon2 "$(openssl rand -base64 32)" -id -t 3 -m 16 -p 4 -l 32 -e -``` +The `dns` role runs BIND9 as an authoritative nameserver for your domain. After deployment: -### 3. Configure SMTP +1. Register a **glue record** at your domain registrar: `ns1.` → your server's public IP. +2. Set your domain's **nameservers** to `ns1.`. -The `smtp_*` variables control outbound email for all services. The default routes through the bundled Stalwart mail server: +Once delegation propagates (typically minutes to hours), all service subdomains will resolve via BIND9 without needing individual A records at your registrar. -```yaml -smtp_host: "stalwart" -smtp_port: 587 -smtp_from: "noreply@{{ base_domain }}" -smtp_user: "noreply@{{ base_domain }}" -smtp_password: "changeme_smtp" -smtp_tls: "starttls" -``` +#### DKIM — email signing key -To use an external relay (SendGrid, Postmark, etc.), replace `smtp_host` with the relay hostname and update credentials accordingly. +Stalwart generates its DKIM signing key on first start. After Stalwart is running: -### 4. Create Authentik OIDC applications +1. Log in to the Stalwart admin UI at `https://mail.`. +2. Navigate to **Settings → DKIM keys** and copy the public key. +3. Add it to `all.yml`: + ```yaml + stalwart_dkim_public_key: "MIGfMA0GCSqGSIb3DQEB..." + ``` +4. Re-run the DNS role to publish the TXT record: + ```bash + ansible-playbook playbooks/site.yml --tags dns + ``` -After the first deployment, log into Authentik at `https://auth.` and create an OAuth2/OIDC provider and application for each service that integrates with SSO. Then fill in the `changeme_*_oidc_secret` placeholders in the relevant compose templates under `roles//templates/`. +#### Authentik OIDC applications + +Log into Authentik at `https://auth.` and create an OAuth2/OIDC provider and application for each service that integrates with SSO. Then fill in the `changeme_*_oidc_secret` placeholders in the relevant compose templates under `roles//templates/`. Services that require Authentik OIDC configuration: @@ -179,7 +195,7 @@ Services that require Authentik OIDC configuration: | Vaultwarden | `changeme_vaultwarden_oidc_secret` | | Forgejo | `changeme_forgejo_oidc_secret` | -### 5. Wazuh TLS certificates +#### Wazuh TLS certificates Wazuh requires TLS certificates between its manager, indexer, and dashboard components before the first run. Generate them using the Wazuh certificate tool: @@ -197,7 +213,7 @@ bash wazuh-certs-tool.sh -A Refer to the [Wazuh Docker documentation](https://documentation.wazuh.com/current/deployment-options/docker/wazuh-container.html) for full details. -### 6. Static website content +#### Static website content Place your static HTML/CSS/JS files in `/opt/sovereign/website/html/` on the target host. Nginx serves this directory at `https://`. The directory is created by the `website` role on first deployment — you can populate it before or after running the playbook. @@ -236,6 +252,31 @@ Services with branding support: Authentik (title, colour, logo via blueprint), E | `traefik_dashboard_password` | — | htpasswd-formatted credential for dashboard basic auth. | | `traefik_version` | `v3.1` | Traefik image tag. | +### DNS / BIND9 + +| Variable | Default | Description | +|----------|---------|-------------| +| `dns_server_ip` | — | Public IPv4 address of this server. Used for all service A records and the `ns1` glue record. | +| `dns_ns_hostname` | `ns1.` | Fully-qualified hostname of the nameserver. | +| `dns_ttl` | `3600` | Default TTL for zone records (seconds). | +| `bind_version` | `9.18-22.04_beta` | `ubuntu/bind9` image tag. | +| `stalwart_dkim_selector` | `default` | DKIM selector name. Must match the selector configured in Stalwart. | +| `stalwart_dkim_public_key` | `""` | RSA public key for DKIM signing. Retrieve from the Stalwart admin UI after first deployment. Leave empty to skip the DKIM TXT record. Long keys are automatically split into 255-byte chunks as required by RFC 4871. | +| `dmarc_policy` | `quarantine` | DMARC enforcement policy: `none`, `quarantine`, or `reject`. | +| `dmarc_rua` | `mailto:dmarc-reports@` | Address to receive aggregate DMARC reports. | +| `dmarc_ruf` | `mailto:dmarc-forensics@` | Address to receive forensic DMARC reports. | + +The DNS role publishes the following records for ``: + +| Type | Name | Value | +|------|------|-------| +| A | `ns1` | `dns_server_ip` | +| A | `@`, all service subdomains | `dns_server_ip` | +| MX | `@` | `mail.` (priority 10) | +| TXT | `@` | SPF: `v=spf1 mx ~all` | +| TXT | `_dmarc` | DMARC policy record | +| TXT | `._domainkey` | DKIM public key (when `stalwart_dkim_public_key` is set) | + ### Graylog | Variable | Default | Description | @@ -345,7 +386,7 @@ Services with branding support: Authentik (title, colour, logo via blueprint), E |----------|---------|-------------| | `vaultwarden_domain` | `vault.` | Vaultwarden URL. | | `vaultwarden_version` | `latest` | Vaultwarden image tag. | -| `vaultwarden_admin_token` | — | Argon2-hashed token for the `/admin` panel. | +| `vaultwarden_admin_token` | — | Token for the `/admin` panel. | | `vaultwarden_db_password` | — | PostgreSQL password for Vaultwarden's database. | ### Forgejo @@ -355,9 +396,9 @@ Services with branding support: Authentik (title, colour, logo via blueprint), E | `forgejo_domain` | `git.` | Forgejo URL. | | `forgejo_version` | `latest` | Forgejo image tag. | | `forgejo_db_password` | — | PostgreSQL password for Forgejo's database. | -| `forgejo_secret_key` | — | Random hex secret for internal signing. | -| `forgejo_internal_token` | — | Random hex token for internal API calls. | -| `forgejo_lfs_jwt_secret` | — | Random hex secret for Git LFS JWT tokens. | +| `forgejo_secret_key` | — | Random secret for internal signing. | +| `forgejo_internal_token` | — | Random token for internal API calls. | +| `forgejo_lfs_jwt_secret` | — | Random secret for Git LFS JWT tokens. | | `forgejo_admin_user` | `admin` | Initial admin username. | | `forgejo_admin_password` | — | Initial admin password. | | `forgejo_admin_email` | `admin@` | Initial admin email. | @@ -380,7 +421,7 @@ These variables are consumed by every service that sends email. | `smtp_from` | `noreply@` | Default sender address. | | `smtp_user` | `noreply@` | SMTP authentication username. | | `smtp_password` | — | SMTP authentication password. | -| `smtp_tls` | `starttls` | TLS mode: `starttls` or `tls`. | +| `smtp_tls` | `starttls` | TLS mode: `starttls`, `tls`, or `none`. | --- @@ -402,19 +443,20 @@ export SOVEREIGN_SSH_KEY=~/.ssh/id_rsa ansible-playbook playbooks/site.yml ``` -Services are deployed in dependency order: Graylog (logging) → Authentik (auth) → all other services. +Services are deployed in dependency order: common (Docker + Traefik) → DNS → Graylog (logging) → Authentik (auth) → all other services. ### Deploy a single service Use the role's tag to deploy only that service: ```bash +ansible-playbook playbooks/site.yml --tags dns ansible-playbook playbooks/site.yml --tags authentik ansible-playbook playbooks/site.yml --tags nextcloud ansible-playbook playbooks/site.yml --tags website ``` -Available tags: `common`, `graylog`, `authentik`, `minio`, `nextcloud`, `stalwart`, `roundcube`, `matrix`, `jitsi`, `headscale`, `wazuh`, `vaultwarden`, `forgejo`, `website`. +Available tags: `common`, `dns`, `graylog`, `authentik`, `minio`, `nextcloud`, `stalwart`, `roundcube`, `matrix`, `jitsi`, `headscale`, `wazuh`, `vaultwarden`, `forgejo`, `website`. ### Dry run @@ -441,7 +483,7 @@ Each role has a [Molecule](https://ansible.readthedocs.io/projects/molecule/) te - **Directory creation** — all expected data directories are created with correct permissions. - **Template rendering** — every Jinja2 template renders without errors and with all variables substituted (no unresolved `{{ }}` in output files). -- **Config file content** — role-specific config files (Element `config.json`, Headscale `config.yaml`, Authentik branding blueprint, Roundcube `custom.inc.php`, Jitsi interface config, Wazuh dashboard YAML) contain the expected values. +- **Config file content** — role-specific config files (Element `config.json`, Headscale `config.yaml`, Authentik branding blueprint, Roundcube `custom.inc.php`, Jitsi interface config, Wazuh dashboard YAML, BIND9 `named.conf` and zone file) contain the expected values. - **Docker Compose structure** — `docker-compose.yml` references the correct image, Traefik routing labels, GELF logging address, and external network declaration. - **Idempotency** — Molecule re-runs each role after converge and asserts zero changed tasks. @@ -461,6 +503,13 @@ cd roles/authentik molecule test ``` +Or using the Justfile shorthand: + +```bash +just test-role authentik +just test-role dns +``` + `molecule test` runs the full lifecycle: dependency → converge → idempotency check → verify → cleanup. For a faster iteration loop during development: @@ -476,12 +525,11 @@ molecule destroy ### Run tests for all roles ```bash -for role in roles/*/; do - echo "=== Testing $role ===" - (cd "$role" && molecule test) -done +just test ``` +This iterates over all roles and reports any failures at the end. + ### Lint ```bash @@ -493,8 +541,8 @@ Both tools are configured via `.ansible-lint` and `.yamllint` at the repo root. ### Adding tests for a new role -1. Create `roles//molecule/default/` with `molecule.yml`, `converge.yml`, and `verify.yml` following the pattern of an existing simple role (e.g. `roles/website/molecule/default/`). -2. Add the new role's variables to `molecule/shared/vars.yml`. +1. Create `roles//molecule/default/` with `molecule.yml`, `converge.yml`, and `verify.yml` following the pattern of an existing simple role (e.g. `roles/dns/molecule/default/`). +2. Add the new role's variables to `roles/molecule/shared/vars.yml`. 3. Add `when: not (molecule_test_mode | default(false))` to any tasks that call `community.docker.docker_compose_v2`, `ansible.builtin.uri` (health checks), or `ansible.builtin.command` (docker exec). 4. Add the same guard to the role's restart handler in `handlers/main.yml`. @@ -583,6 +631,8 @@ Follow the pattern used by existing roles: - **Reverse proxy**: Traefik handles all inbound HTTPS traffic, terminates TLS using Let's Encrypt (TLS challenge), and routes to containers via Docker labels. - **Authentication**: The `authentik` Traefik forward-auth middleware is defined in the `common` role and can be applied to any router label: `traefik.http.routers..middlewares=authentik`. +- **DNS**: BIND9 runs as an authoritative-only nameserver (recursion disabled) on port 53/TCP+UDP. It publishes A records for every service subdomain, MX records pointing to Stalwart, and email authentication records (SPF, DMARC, DKIM). Users must register a glue record at their domain registrar and delegate the domain's nameservers to `ns1.` after deployment. +- **Email authentication**: SPF restricts sending to the MX host. DMARC policy is configurable (`none`/`quarantine`/`reject`). DKIM requires retrieving the public key from Stalwart after first deployment and re-running the `dns` role to publish it. - **Networking**: All containers that need Traefik routing join the external `sovereign` Docker network. Services with databases also have a private `internal` network for backend isolation. - **Logging**: Every container uses the `gelf` log driver pointed at `graylog_host:12201`. `graylog_host` should be an IP reachable from inside Docker containers (typically the host's IP on the Docker bridge, not `localhost`). - **Data persistence**: Each service stores data under `{{ sovereign_base_dir }}//` (default `/opt/sovereign//`). This path is defined in each role's `defaults/main.yml` as `_data_dir`.