From 5920d3fd7a61b47b8b7cf2c829bc91abec7e88f0 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:29:32 -0300 Subject: [PATCH] Initial commit after Claude implementation --- .claude/settings.local.json | 7 + CLAUDE.md | 103 ++++++++++++++ README.md | 21 +++ ansible.cfg | 10 ++ inventories/production/group_vars/all.yml | 116 ++++++++++++++++ inventories/production/hosts.yml | 6 + playbooks/site.yml | 31 +++++ requirements.yml | 8 ++ roles/authentik/defaults/main.yml | 2 + roles/authentik/handlers/main.yml | 6 + roles/authentik/tasks/main.yml | 34 +++++ .../authentik/templates/docker-compose.yml.j2 | 116 ++++++++++++++++ roles/common/defaults/main.yml | 4 + roles/common/handlers/main.yml | 6 + roles/common/tasks/main.yml | 71 ++++++++++ roles/common/templates/docker-compose.yml.j2 | 46 ++++++ roles/forgejo/defaults/main.yml | 2 + roles/forgejo/handlers/main.yml | 6 + roles/forgejo/tasks/main.yml | 22 +++ roles/forgejo/templates/docker-compose.yml.j2 | 77 ++++++++++ roles/graylog/defaults/main.yml | 4 + roles/graylog/handlers/main.yml | 6 + roles/graylog/tasks/main.yml | 50 +++++++ roles/graylog/templates/docker-compose.yml.j2 | 70 ++++++++++ roles/headscale/defaults/main.yml | 2 + roles/headscale/handlers/main.yml | 6 + roles/headscale/tasks/main.yml | 29 ++++ .../headscale/templates/docker-compose.yml.j2 | 28 ++++ .../templates/headscale-config.yaml.j2 | 50 +++++++ roles/jitsi/defaults/main.yml | 2 + roles/jitsi/handlers/main.yml | 6 + roles/jitsi/tasks/main.yml | 24 ++++ roles/jitsi/templates/docker-compose.yml.j2 | 131 ++++++++++++++++++ roles/matrix/defaults/main.yml | 2 + roles/matrix/handlers/main.yml | 6 + roles/matrix/tasks/main.yml | 39 ++++++ roles/matrix/templates/docker-compose.yml.j2 | 69 +++++++++ roles/matrix/templates/element-config.json.j2 | 32 +++++ roles/minio/defaults/main.yml | 2 + roles/minio/handlers/main.yml | 6 + roles/minio/tasks/main.yml | 40 ++++++ roles/minio/templates/docker-compose.yml.j2 | 42 ++++++ roles/nextcloud/defaults/main.yml | 2 + roles/nextcloud/handlers/main.yml | 6 + roles/nextcloud/tasks/main.yml | 21 +++ .../nextcloud/templates/docker-compose.yml.j2 | 97 +++++++++++++ roles/roundcube/defaults/main.yml | 2 + roles/roundcube/handlers/main.yml | 6 + roles/roundcube/tasks/main.yml | 20 +++ .../roundcube/templates/docker-compose.yml.j2 | 57 ++++++++ roles/stalwart/defaults/main.yml | 2 + roles/stalwart/handlers/main.yml | 6 + roles/stalwart/tasks/main.yml | 21 +++ .../stalwart/templates/docker-compose.yml.j2 | 32 +++++ roles/vaultwarden/defaults/main.yml | 2 + roles/vaultwarden/handlers/main.yml | 6 + roles/vaultwarden/tasks/main.yml | 21 +++ .../templates/docker-compose.yml.j2 | 63 +++++++++ roles/wazuh/defaults/main.yml | 3 + roles/wazuh/handlers/main.yml | 6 + roles/wazuh/tasks/main.yml | 28 ++++ roles/wazuh/templates/docker-compose.yml.j2 | 106 ++++++++++++++ 62 files changed, 1847 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 ansible.cfg create mode 100644 inventories/production/group_vars/all.yml create mode 100644 inventories/production/hosts.yml create mode 100644 playbooks/site.yml create mode 100644 requirements.yml create mode 100644 roles/authentik/defaults/main.yml create mode 100644 roles/authentik/handlers/main.yml create mode 100644 roles/authentik/tasks/main.yml create mode 100644 roles/authentik/templates/docker-compose.yml.j2 create mode 100644 roles/common/defaults/main.yml create mode 100644 roles/common/handlers/main.yml create mode 100644 roles/common/tasks/main.yml create mode 100644 roles/common/templates/docker-compose.yml.j2 create mode 100644 roles/forgejo/defaults/main.yml create mode 100644 roles/forgejo/handlers/main.yml create mode 100644 roles/forgejo/tasks/main.yml create mode 100644 roles/forgejo/templates/docker-compose.yml.j2 create mode 100644 roles/graylog/defaults/main.yml create mode 100644 roles/graylog/handlers/main.yml create mode 100644 roles/graylog/tasks/main.yml create mode 100644 roles/graylog/templates/docker-compose.yml.j2 create mode 100644 roles/headscale/defaults/main.yml create mode 100644 roles/headscale/handlers/main.yml create mode 100644 roles/headscale/tasks/main.yml create mode 100644 roles/headscale/templates/docker-compose.yml.j2 create mode 100644 roles/headscale/templates/headscale-config.yaml.j2 create mode 100644 roles/jitsi/defaults/main.yml create mode 100644 roles/jitsi/handlers/main.yml create mode 100644 roles/jitsi/tasks/main.yml create mode 100644 roles/jitsi/templates/docker-compose.yml.j2 create mode 100644 roles/matrix/defaults/main.yml create mode 100644 roles/matrix/handlers/main.yml create mode 100644 roles/matrix/tasks/main.yml create mode 100644 roles/matrix/templates/docker-compose.yml.j2 create mode 100644 roles/matrix/templates/element-config.json.j2 create mode 100644 roles/minio/defaults/main.yml create mode 100644 roles/minio/handlers/main.yml create mode 100644 roles/minio/tasks/main.yml create mode 100644 roles/minio/templates/docker-compose.yml.j2 create mode 100644 roles/nextcloud/defaults/main.yml create mode 100644 roles/nextcloud/handlers/main.yml create mode 100644 roles/nextcloud/tasks/main.yml create mode 100644 roles/nextcloud/templates/docker-compose.yml.j2 create mode 100644 roles/roundcube/defaults/main.yml create mode 100644 roles/roundcube/handlers/main.yml create mode 100644 roles/roundcube/tasks/main.yml create mode 100644 roles/roundcube/templates/docker-compose.yml.j2 create mode 100644 roles/stalwart/defaults/main.yml create mode 100644 roles/stalwart/handlers/main.yml create mode 100644 roles/stalwart/tasks/main.yml create mode 100644 roles/stalwart/templates/docker-compose.yml.j2 create mode 100644 roles/vaultwarden/defaults/main.yml create mode 100644 roles/vaultwarden/handlers/main.yml create mode 100644 roles/vaultwarden/tasks/main.yml create mode 100644 roles/vaultwarden/templates/docker-compose.yml.j2 create mode 100644 roles/wazuh/defaults/main.yml create mode 100644 roles/wazuh/handlers/main.yml create mode 100644 roles/wazuh/tasks/main.yml create mode 100644 roles/wazuh/templates/docker-compose.yml.j2 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..47658dc --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(ls -la /home/iroddis/dev/sovereign/.*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4c41e0d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**Sovereign** is an Ansible project that deploys a complete self-hosted infrastructure stack for small businesses using Docker and Docker Compose on a single Linux host. + +## Architecture + +### Services + +| Role | Tool | Subdomain | +|------|------|-----------| +| common | Traefik (reverse proxy + TLS) | `traefik.` | +| graylog | Graylog + OpenSearch + MongoDB | `logs.` | +| authentik | Authentik (identity provider) | `auth.` | +| minio | MinIO (object storage) | `s3.`, `minio.` | +| nextcloud | Nextcloud + MariaDB + Redis | `cloud.` | +| stalwart | Stalwart Mail (SMTP/IMAP) | `mail.` | +| roundcube | Roundcube (webmail) | `webmail.` | +| matrix | Synapse + Element | `matrix.`, `chat.` | +| jitsi | Jitsi Meet | `meet.` | +| headscale | Headscale (WireGuard mesh VPN) | `headscale.` | +| wazuh | Wazuh Manager + Indexer + Dashboard | `wazuh.` | +| vaultwarden | Vaultwarden + PostgreSQL | `vault.` | +| forgejo | Forgejo + PostgreSQL | `git.` | + +### Design Principles + +- **Single authentication**: All services authenticate via Authentik OIDC/OAuth2 +- **Single config per deployment**: All variables live in `inventories/production/group_vars/all.yml` +- **Centralized logging**: Every container ships logs via GELF UDP to Graylog (`graylog_host:12201`) +- **Networking**: All services share an external Docker network named `sovereign` for Traefik routing; each service has its own `internal` network for db/cache isolation +- **Deployment order**: `site.yml` deploys Graylog first (logging), then Authentik (auth), then all other services + +### Tenant Configuration + +All deployment variables are in one place: `inventories/production/group_vars/all.yml`. For a new tenant/deployment, copy this file and update `base_domain`, passwords, and secrets. All service subdomains derive from `base_domain`. + +## Project Structure + +``` +sovereign/ +├── ansible.cfg +├── requirements.yml # Ansible Galaxy collections +├── inventories/ +│ └── production/ +│ ├── hosts.yml +│ └── group_vars/ +│ └── all.yml # ← single tenant config file +├── playbooks/ +│ └── site.yml +└── roles/ + └── / + ├── defaults/main.yml + ├── handlers/main.yml + ├── tasks/main.yml + └── templates/ + ├── docker-compose.yml.j2 + └── ... # service-specific configs +``` + +Each role deploys its Docker Compose stack to `/opt/sovereign//` on the target host. + +## Common Commands + +```bash +# Install required Ansible collections +ansible-galaxy collection install -r requirements.yml + +# Run a full deployment +ansible-playbook playbooks/site.yml + +# Deploy a single service +ansible-playbook playbooks/site.yml --tags authentik + +# Dry run +ansible-playbook playbooks/site.yml --check --diff + +# Syntax check +ansible-playbook playbooks/site.yml --syntax-check + +# Lint +ansible-lint +``` + +### Environment variables for inventory + +```bash +export SOVEREIGN_HOST=your-server-ip +export SOVEREIGN_USER=ubuntu +export SOVEREIGN_SSH_KEY=~/.ssh/id_rsa +``` + +## Initial Setup Notes + +Before first deployment, update `inventories/production/group_vars/all.yml`: +1. Set `base_domain` +2. Replace all `changeme_*` passwords and secrets with secure values +3. Generate `graylog_root_password_sha2`: `echo -n yourpassword | sha256sum` +4. Create Authentik OIDC applications for each service that uses SSO (MinIO, Headscale, Vaultwarden, Forgejo) and fill in the `changeme_*_oidc_secret` placeholders in the respective compose templates +5. Wazuh requires TLS certificates — see Wazuh Docker documentation for generating certs before first run diff --git a/README.md b/README.md new file mode 100644 index 0000000..76897a4 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Sovereign + +Sovereign is an ansible project that deploys a complete sovereign data solution for a small business using +docker and docker compose. The tools used are: + +- Identity: Authentik +- E-mail: Stalwart Mail + Roundcube/SOGo +- Endpoint Management: Wazuh +- Remote Access: WireGuard / Headscale +- Collaboration (chat): Matrix/Element +- Collaboration (video): Jitsi Meet +- Online documents, fileshare, office suite, and calendar: Nextcloud + MinIO +- Password Management: Vaultwarden +- Software version control: Forgejo +- Centralized Logging: Graylog + +Some common requirements: + +- All solutions should use Authentik for login and authorization. +- Variables for the installation should be defined in a single file per tenant / deployment. +- Logs should be centrally captured in Graylog diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..230e22d --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,10 @@ +[defaults] +inventory = inventories/production +roles_path = roles +retry_files_enabled = False +host_key_checking = False +stdout_callback = yaml +callbacks_enabled = timer, profile_tasks + +[ssh_connection] +pipelining = True diff --git a/inventories/production/group_vars/all.yml b/inventories/production/group_vars/all.yml new file mode 100644 index 0000000..171c5c5 --- /dev/null +++ b/inventories/production/group_vars/all.yml @@ -0,0 +1,116 @@ +--- +# ============================================================================= +# SOVEREIGN DEPLOYMENT CONFIGURATION +# All variables for this deployment are defined here. +# ============================================================================= + +# Base domain - all services are subdomains of this +base_domain: "example.com" + +# Base directory for all service data +sovereign_base_dir: /opt/sovereign + +# Traefik +traefik_acme_email: "admin@{{ base_domain }}" +traefik_domain: "traefik.{{ base_domain }}" +traefik_dashboard_password: "changeme" # htpasswd hash + +# Authentik +authentik_domain: "auth.{{ base_domain }}" +authentik_version: "2024.10.5" +authentik_secret_key: "change-me-to-a-50-char-random-string" +authentik_db_password: "changeme_authentik_db" +authentik_admin_email: "admin@{{ base_domain }}" +authentik_admin_password: "changeme_admin" + +# Graylog +graylog_domain: "logs.{{ base_domain }}" +graylog_version: "6.0" +graylog_password_secret: "changeme_graylog_secret_min_16_chars" # min 16 chars +graylog_root_password_sha2: "changeme_sha256_of_password" # echo -n yourpassword | sha256sum +graylog_host: "127.0.0.1" # host IP reachable from containers +graylog_gelf_port: 12201 + +# Stalwart Mail +stalwart_domain: "mail.{{ base_domain }}" +stalwart_admin_password: "changeme_mail_admin" +stalwart_version: "latest" + +# Roundcube +roundcube_domain: "webmail.{{ base_domain }}" +roundcube_version: "latest" +roundcube_db_password: "changeme_roundcube_db" +roundcube_des_key: "changeme_24_char_des_key____" + +# Wazuh +wazuh_domain: "wazuh.{{ base_domain }}" +wazuh_version: "4.9.0" +wazuh_admin_password: "changeme_wazuh_admin" +wazuh_api_password: "changeme_wazuh_api" + +# WireGuard / Headscale +wireguard_domain: "vpn.{{ base_domain }}" +headscale_domain: "headscale.{{ base_domain }}" +headscale_version: "0.23.0" +wireguard_port: 51820 +headscale_noise_private_key: "" # generated on first run + +# Matrix / Element +matrix_domain: "matrix.{{ base_domain }}" +element_domain: "chat.{{ base_domain }}" +matrix_version: "v1.118.0" +matrix_registration_secret: "changeme_registration_secret" +matrix_db_password: "changeme_matrix_db" + +# Jitsi +jitsi_domain: "meet.{{ base_domain }}" +jitsi_version: "stable-9753" +jitsi_jicofo_auth_password: "changeme_jicofo" +jitsi_jvb_auth_password: "changeme_jvb" +jitsi_jibri_recorder_password: "changeme_jibri_recorder" +jitsi_jibri_xmpp_password: "changeme_jibri_xmpp" +jitsi_turn_secret: "changeme_turn" + +# MinIO +minio_domain: "s3.{{ base_domain }}" +minio_console_domain: "minio.{{ base_domain }}" +minio_version: "latest" +minio_root_user: "minioadmin" +minio_root_password: "changeme_minio" +minio_nextcloud_bucket: "nextcloud" +minio_nextcloud_access_key: "nextcloud" +minio_nextcloud_secret_key: "changeme_nextcloud_s3" + +# Nextcloud +nextcloud_domain: "cloud.{{ base_domain }}" +nextcloud_version: "29" +nextcloud_admin_user: "admin" +nextcloud_admin_password: "changeme_nextcloud" +nextcloud_db_password: "changeme_nextcloud_db" +nextcloud_db_root_password: "changeme_nextcloud_db_root" + +# Vaultwarden +vaultwarden_domain: "vault.{{ base_domain }}" +vaultwarden_version: "latest" +vaultwarden_admin_token: "changeme_vaultwarden_admin_token" +vaultwarden_db_password: "changeme_vaultwarden_db" + +# Forgejo +forgejo_domain: "git.{{ base_domain }}" +forgejo_version: "latest" +forgejo_db_password: "changeme_forgejo_db" +forgejo_secret_key: "changeme_forgejo_secret" +forgejo_internal_token: "changeme_forgejo_internal_token" +forgejo_lfs_jwt_secret: "changeme_forgejo_lfs_jwt" +forgejo_admin_user: "admin" +forgejo_admin_password: "changeme_forgejo_admin" +forgejo_admin_email: "admin@{{ base_domain }}" +forgejo_ssh_port: 2222 + +# SMTP (for services that send email) +smtp_host: "stalwart" +smtp_port: 587 +smtp_from: "noreply@{{ base_domain }}" +smtp_user: "noreply@{{ base_domain }}" +smtp_password: "changeme_smtp" +smtp_tls: "starttls" diff --git a/inventories/production/hosts.yml b/inventories/production/hosts.yml new file mode 100644 index 0000000..391e71f --- /dev/null +++ b/inventories/production/hosts.yml @@ -0,0 +1,6 @@ +all: + hosts: + sovereign: + ansible_host: "{{ lookup('env', 'SOVEREIGN_HOST') | default('your-server-ip') }}" + ansible_user: "{{ lookup('env', 'SOVEREIGN_USER') | default('ubuntu') }}" + ansible_ssh_private_key_file: "{{ lookup('env', 'SOVEREIGN_SSH_KEY') | default('~/.ssh/id_rsa') }}" diff --git a/playbooks/site.yml b/playbooks/site.yml new file mode 100644 index 0000000..7114faf --- /dev/null +++ b/playbooks/site.yml @@ -0,0 +1,31 @@ +--- +- name: Deploy Sovereign infrastructure + hosts: sovereign + become: true + roles: + - role: common + tags: [common, traefik] + - role: graylog + tags: [graylog, logging] + - role: authentik + tags: [authentik, auth, identity] + - role: minio + tags: [minio, storage] + - role: nextcloud + tags: [nextcloud, files, cloud] + - role: stalwart + tags: [stalwart, mail, email] + - role: roundcube + tags: [roundcube, webmail, email] + - role: matrix + tags: [matrix, chat, element] + - role: jitsi + tags: [jitsi, video, meet] + - role: headscale + tags: [headscale, vpn, wireguard] + - role: wazuh + tags: [wazuh, endpoint, security] + - role: vaultwarden + tags: [vaultwarden, passwords, vault] + - role: forgejo + tags: [forgejo, git, vcs] diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..0c80e9c --- /dev/null +++ b/requirements.yml @@ -0,0 +1,8 @@ +--- +collections: + - name: community.docker + version: ">=3.0.0" + - name: community.general + version: ">=8.0.0" + - name: ansible.posix + version: ">=1.5.0" diff --git a/roles/authentik/defaults/main.yml b/roles/authentik/defaults/main.yml new file mode 100644 index 0000000..93963b3 --- /dev/null +++ b/roles/authentik/defaults/main.yml @@ -0,0 +1,2 @@ +--- +authentik_data_dir: "{{ sovereign_base_dir }}/authentik" diff --git a/roles/authentik/handlers/main.yml b/roles/authentik/handlers/main.yml new file mode 100644 index 0000000..223deef --- /dev/null +++ b/roles/authentik/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart authentik + community.docker.docker_compose_v2: + project_src: "{{ authentik_data_dir }}" + state: present + recreate: always diff --git a/roles/authentik/tasks/main.yml b/roles/authentik/tasks/main.yml new file mode 100644 index 0000000..463e44f --- /dev/null +++ b/roles/authentik/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: Create Authentik directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ authentik_data_dir }}" + - "{{ authentik_data_dir }}/media" + - "{{ authentik_data_dir }}/custom-templates" + - "{{ authentik_data_dir }}/certs" + - "{{ authentik_data_dir }}/postgres" + +- name: Deploy Authentik docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ authentik_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart authentik + +- name: Start Authentik + community.docker.docker_compose_v2: + project_src: "{{ authentik_data_dir }}" + state: present + +- name: Wait for Authentik to be ready + ansible.builtin.uri: + url: "http://localhost:9001/-/health/ready/" + method: GET + status_code: 200 + register: result + until: result.status == 200 + retries: 30 + delay: 10 diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..f64b3ce --- /dev/null +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -0,0 +1,116 @@ +services: + authentik-postgresql: + image: docker.io/library/postgres:16-alpine + container_name: authentik-postgresql + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + interval: 30s + timeout: 5s + retries: 5 + environment: + POSTGRES_PASSWORD: "{{ authentik_db_password }}" + POSTGRES_USER: authentik + POSTGRES_DB: authentik + volumes: + - {{ authentik_data_dir }}/postgres:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "authentik-postgresql" + + authentik-redis: + image: docker.io/library/redis:alpine + container_name: authentik-redis + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 30s + timeout: 3s + retries: 5 + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "authentik-redis" + + authentik-server: + image: ghcr.io/goauthentik/server:{{ authentik_version }} + container_name: authentik-server + restart: unless-stopped + command: server + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql + AUTHENTIK_POSTGRESQL__USER: authentik + AUTHENTIK_POSTGRESQL__PASSWORD: "{{ authentik_db_password }}" + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_SECRET_KEY: "{{ authentik_secret_key }}" + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + AUTHENTIK_EMAIL__HOST: "{{ smtp_host }}" + AUTHENTIK_EMAIL__PORT: "{{ smtp_port }}" + AUTHENTIK_EMAIL__USERNAME: "{{ smtp_user }}" + AUTHENTIK_EMAIL__PASSWORD: "{{ smtp_password }}" + AUTHENTIK_EMAIL__FROM: "{{ smtp_from }}" + AUTHENTIK_EMAIL__USE_TLS: "true" + volumes: + - {{ authentik_data_dir }}/media:/media + - {{ authentik_data_dir }}/custom-templates:/templates + - {{ authentik_data_dir }}/certs:/certs + ports: + - "127.0.0.1:9001:9000" + depends_on: + - authentik-postgresql + - authentik-redis + labels: + - "traefik.enable=true" + - "traefik.http.routers.authentik.rule=Host(`{{ authentik_domain }}`)" + - "traefik.http.routers.authentik.tls=true" + - "traefik.http.routers.authentik.tls.certresolver=letsencrypt" + - "traefik.http.services.authentik.loadbalancer.server.port=9000" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "authentik-server" + + authentik-worker: + image: ghcr.io/goauthentik/server:{{ authentik_version }} + container_name: authentik-worker + restart: unless-stopped + command: worker + environment: + AUTHENTIK_REDIS__HOST: authentik-redis + AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql + AUTHENTIK_POSTGRESQL__USER: authentik + AUTHENTIK_POSTGRESQL__PASSWORD: "{{ authentik_db_password }}" + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_SECRET_KEY: "{{ authentik_secret_key }}" + AUTHENTIK_ERROR_REPORTING__ENABLED: "false" + volumes: + - {{ authentik_data_dir }}/media:/media + - {{ authentik_data_dir }}/certs:/certs + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - authentik-postgresql + - authentik-redis + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "authentik-worker" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 0000000..3dd074a --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,4 @@ +--- +sovereign_network_name: sovereign +traefik_version: "v3.1" +traefik_data_dir: "{{ sovereign_base_dir }}/traefik" diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 0000000..0d5ec5f --- /dev/null +++ b/roles/common/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart traefik + community.docker.docker_compose_v2: + project_src: "{{ traefik_data_dir }}" + state: present + recreate: always diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..c4b0ccb --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,71 @@ +--- +- name: Install required packages + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - python3-pip + - python3-docker + state: present + update_cache: true + +- name: Add Docker GPG key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Add Docker repository + ansible.builtin.apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + +- name: Install Docker + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + state: present + update_cache: true + +- name: Enable and start Docker + ansible.builtin.systemd: + name: docker + enabled: true + state: started + +- name: Create sovereign Docker network + community.docker.docker_network: + name: "{{ sovereign_network_name }}" + state: present + +- name: Create Traefik data directory + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ traefik_data_dir }}" + - "{{ traefik_data_dir }}/config" + +- name: Create acme.json for Let's Encrypt + ansible.builtin.file: + path: "{{ traefik_data_dir }}/acme.json" + state: touch + mode: '0600' + +- name: Deploy Traefik docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ traefik_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart traefik + +- name: Start Traefik + community.docker.docker_compose_v2: + project_src: "{{ traefik_data_dir }}" + state: present diff --git a/roles/common/templates/docker-compose.yml.j2 b/roles/common/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..25170f4 --- /dev/null +++ b/roles/common/templates/docker-compose.yml.j2 @@ -0,0 +1,46 @@ +services: + traefik: + image: traefik:{{ traefik_version }} + container_name: traefik + restart: unless-stopped + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network={{ sovereign_network_name }}" + - "--entrypoints.web.address=:80" + - "--entrypoints.web.http.redirections.entrypoint.to=websecure" + - "--entrypoints.web.http.redirections.entrypoint.scheme=https" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.email={{ traefik_acme_email }}" + - "--certificatesresolvers.letsencrypt.acme.storage=/acme.json" + - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" + - "--log.level=INFO" + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - {{ traefik_data_dir }}/acme.json:/acme.json + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik-dashboard.rule=Host(`{{ traefik_domain }}`)" + - "traefik.http.routers.traefik-dashboard.tls=true" + - "traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik-dashboard.service=api@internal" + - "traefik.http.routers.traefik-dashboard.middlewares=traefik-auth" + - "traefik.http.middlewares.traefik-auth.basicauth.users={{ traefik_dashboard_password }}" + - "traefik.http.middlewares.authentik.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/traefik" + - "traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true" + - "traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version" + networks: + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "traefik" + +networks: + {{ sovereign_network_name }}: + external: true diff --git a/roles/forgejo/defaults/main.yml b/roles/forgejo/defaults/main.yml new file mode 100644 index 0000000..e7693cf --- /dev/null +++ b/roles/forgejo/defaults/main.yml @@ -0,0 +1,2 @@ +--- +forgejo_data_dir: "{{ sovereign_base_dir }}/forgejo" diff --git a/roles/forgejo/handlers/main.yml b/roles/forgejo/handlers/main.yml new file mode 100644 index 0000000..a46ab7b --- /dev/null +++ b/roles/forgejo/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart forgejo + community.docker.docker_compose_v2: + project_src: "{{ forgejo_data_dir }}" + state: present + recreate: always diff --git a/roles/forgejo/tasks/main.yml b/roles/forgejo/tasks/main.yml new file mode 100644 index 0000000..5008baf --- /dev/null +++ b/roles/forgejo/tasks/main.yml @@ -0,0 +1,22 @@ +--- +- name: Create Forgejo directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ forgejo_data_dir }}" + - "{{ forgejo_data_dir }}/data" + - "{{ forgejo_data_dir }}/config" + +- name: Deploy Forgejo docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ forgejo_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart forgejo + +- name: Start Forgejo + community.docker.docker_compose_v2: + project_src: "{{ forgejo_data_dir }}" + state: present diff --git a/roles/forgejo/templates/docker-compose.yml.j2 b/roles/forgejo/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..ec0cab7 --- /dev/null +++ b/roles/forgejo/templates/docker-compose.yml.j2 @@ -0,0 +1,77 @@ +services: + forgejo-db: + image: postgres:16-alpine + container_name: forgejo-db + restart: unless-stopped + environment: + POSTGRES_DB: forgejo + POSTGRES_USER: forgejo + POSTGRES_PASSWORD: "{{ forgejo_db_password }}" + volumes: + - {{ forgejo_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "forgejo-db" + + forgejo: + image: codeberg.org/forgejo/forgejo:{{ forgejo_version }} + container_name: forgejo + restart: unless-stopped + depends_on: + - forgejo-db + environment: + USER_UID: 1000 + USER_GID: 1000 + FORGEJO__database__DB_TYPE: postgres + FORGEJO__database__HOST: forgejo-db:5432 + FORGEJO__database__NAME: forgejo + FORGEJO__database__USER: forgejo + FORGEJO__database__PASSWD: "{{ forgejo_db_password }}" + FORGEJO__server__DOMAIN: "{{ forgejo_domain }}" + FORGEJO__server__ROOT_URL: "https://{{ forgejo_domain }}" + FORGEJO__server__SSH_DOMAIN: "{{ forgejo_domain }}" + FORGEJO__server__SSH_PORT: "{{ forgejo_ssh_port }}" + FORGEJO__server__SSH_LISTEN_PORT: 22 + FORGEJO__security__SECRET_KEY: "{{ forgejo_secret_key }}" + FORGEJO__security__INTERNAL_TOKEN: "{{ forgejo_internal_token }}" + FORGEJO__lfs__JWT_SECRET: "{{ forgejo_lfs_jwt_secret }}" + FORGEJO__mailer__ENABLED: "true" + FORGEJO__mailer__SMTP_ADDR: "{{ smtp_host }}" + FORGEJO__mailer__SMTP_PORT: "{{ smtp_port }}" + FORGEJO__mailer__FROM: "{{ smtp_from }}" + FORGEJO__mailer__USER: "{{ smtp_user }}" + FORGEJO__mailer__PASSWD: "{{ smtp_password }}" + FORGEJO__openid__ENABLE_OPENID_SIGNIN: "true" + FORGEJO__openid__ENABLE_OPENID_SIGNUP: "false" + FORGEJO__oauth2_client__REGISTER_EMAIL_CONFIRM: "false" + FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION: "true" + FORGEJO__log__LEVEL: warn + ports: + - "{{ forgejo_ssh_port }}:22" + volumes: + - {{ forgejo_data_dir }}/data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.forgejo.rule=Host(`{{ forgejo_domain }}`)" + - "traefik.http.routers.forgejo.tls=true" + - "traefik.http.routers.forgejo.tls.certresolver=letsencrypt" + - "traefik.http.services.forgejo.loadbalancer.server.port=3000" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "forgejo" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/graylog/defaults/main.yml b/roles/graylog/defaults/main.yml new file mode 100644 index 0000000..64019e1 --- /dev/null +++ b/roles/graylog/defaults/main.yml @@ -0,0 +1,4 @@ +--- +graylog_data_dir: "{{ sovereign_base_dir }}/graylog" +opensearch_version: "2.15.0" +mongodb_version: "6.0" diff --git a/roles/graylog/handlers/main.yml b/roles/graylog/handlers/main.yml new file mode 100644 index 0000000..c85ac0b --- /dev/null +++ b/roles/graylog/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart graylog + community.docker.docker_compose_v2: + project_src: "{{ graylog_data_dir }}" + state: present + recreate: always diff --git a/roles/graylog/tasks/main.yml b/roles/graylog/tasks/main.yml new file mode 100644 index 0000000..a21fc57 --- /dev/null +++ b/roles/graylog/tasks/main.yml @@ -0,0 +1,50 @@ +--- +- name: Create Graylog directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ graylog_data_dir }}" + - "{{ graylog_data_dir }}/data" + - "{{ graylog_data_dir }}/config" + - "{{ graylog_data_dir }}/opensearch" + +- name: Set OpenSearch data directory permissions + ansible.builtin.file: + path: "{{ graylog_data_dir }}/opensearch" + owner: "1000" + group: "1000" + mode: '0775' + +- name: Set vm.max_map_count for OpenSearch + ansible.posix.sysctl: + name: vm.max_map_count + value: '262144' + state: present + sysctl_set: true + +- name: Deploy Graylog docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ graylog_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart graylog + +- name: Start Graylog + community.docker.docker_compose_v2: + project_src: "{{ graylog_data_dir }}" + state: present + +- name: Wait for Graylog to be ready + ansible.builtin.uri: + url: "http://localhost:9000/api/system/loglevel" + method: GET + user: admin + password: "{{ graylog_root_password_sha2 }}" + force_basic_auth: true + status_code: 200 + register: result + until: result.status == 200 + retries: 30 + delay: 10 diff --git a/roles/graylog/templates/docker-compose.yml.j2 b/roles/graylog/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..bb36351 --- /dev/null +++ b/roles/graylog/templates/docker-compose.yml.j2 @@ -0,0 +1,70 @@ +services: + mongodb: + image: mongo:{{ mongodb_version }} + container_name: graylog-mongodb + restart: unless-stopped + volumes: + - {{ graylog_data_dir }}/data/mongodb:/data/db + networks: + - internal + + opensearch: + image: opensearchproject/opensearch:{{ opensearch_version }} + container_name: graylog-opensearch + restart: unless-stopped + environment: + - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g" + - "bootstrap.memory_lock=true" + - "discovery.type=single-node" + - "action.auto_create_index=false" + - "plugins.security.ssl.http.enabled=false" + - "plugins.security.disabled=true" + - "OPENSEARCH_INITIAL_ADMIN_PASSWORD=changeme_os_admin" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - {{ graylog_data_dir }}/opensearch:/usr/share/opensearch/data + networks: + - internal + + graylog: + image: graylog/graylog:{{ graylog_version }} + container_name: graylog + restart: unless-stopped + depends_on: + - mongodb + - opensearch + environment: + GRAYLOG_NODE_ID_FILE: "/usr/share/graylog/data/config/node-id" + GRAYLOG_HTTP_BIND_ADDRESS: "0.0.0.0:9000" + GRAYLOG_ELASTICSEARCH_HOSTS: "http://opensearch:9200" + GRAYLOG_MONGODB_URI: "mongodb://mongodb:27017/graylog" + GRAYLOG_PASSWORD_SECRET: "{{ graylog_password_secret }}" + GRAYLOG_ROOT_PASSWORD_SHA2: "{{ graylog_root_password_sha2 }}" + GRAYLOG_HTTP_EXTERNAL_URI: "https://{{ graylog_domain }}/" + GRAYLOG_TRANSPORT_EMAIL_ENABLED: "true" + GRAYLOG_TRANSPORT_EMAIL_HOSTNAME: "{{ smtp_host }}" + GRAYLOG_TRANSPORT_EMAIL_PORT: "{{ smtp_port }}" + GRAYLOG_TRANSPORT_EMAIL_FROM_EMAIL: "{{ smtp_from }}" + ports: + - "127.0.0.1:9000:9000" + - "0.0.0.0:12201:12201/udp" # GELF UDP - must be accessible from all containers + volumes: + - {{ graylog_data_dir }}/data/graylog:/usr/share/graylog/data + - {{ graylog_data_dir }}/config:/usr/share/graylog/data/config + labels: + - "traefik.enable=true" + - "traefik.http.routers.graylog.rule=Host(`{{ graylog_domain }}`)" + - "traefik.http.routers.graylog.tls=true" + - "traefik.http.routers.graylog.tls.certresolver=letsencrypt" + - "traefik.http.services.graylog.loadbalancer.server.port=9000" + networks: + - internal + - {{ sovereign_network_name }} + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/headscale/defaults/main.yml b/roles/headscale/defaults/main.yml new file mode 100644 index 0000000..f0efb77 --- /dev/null +++ b/roles/headscale/defaults/main.yml @@ -0,0 +1,2 @@ +--- +headscale_data_dir: "{{ sovereign_base_dir }}/headscale" diff --git a/roles/headscale/handlers/main.yml b/roles/headscale/handlers/main.yml new file mode 100644 index 0000000..d43b2c6 --- /dev/null +++ b/roles/headscale/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart headscale + community.docker.docker_compose_v2: + project_src: "{{ headscale_data_dir }}" + state: present + recreate: always diff --git a/roles/headscale/tasks/main.yml b/roles/headscale/tasks/main.yml new file mode 100644 index 0000000..42ba331 --- /dev/null +++ b/roles/headscale/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Create Headscale directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ headscale_data_dir }}" + - "{{ headscale_data_dir }}/config" + - "{{ headscale_data_dir }}/data" + +- name: Deploy Headscale config + ansible.builtin.template: + src: headscale-config.yaml.j2 + dest: "{{ headscale_data_dir }}/config/config.yaml" + mode: '0644' + notify: restart headscale + +- name: Deploy Headscale docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ headscale_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart headscale + +- name: Start Headscale + community.docker.docker_compose_v2: + project_src: "{{ headscale_data_dir }}" + state: present diff --git a/roles/headscale/templates/docker-compose.yml.j2 b/roles/headscale/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..76c66f0 --- /dev/null +++ b/roles/headscale/templates/docker-compose.yml.j2 @@ -0,0 +1,28 @@ +services: + headscale: + image: headscale/headscale:{{ headscale_version }} + container_name: headscale + restart: unless-stopped + command: serve + volumes: + - {{ headscale_data_dir }}/config:/etc/headscale + - {{ headscale_data_dir }}/data:/var/lib/headscale + ports: + - "{{ wireguard_port }}:{{ wireguard_port }}/udp" + labels: + - "traefik.enable=true" + - "traefik.http.routers.headscale.rule=Host(`{{ headscale_domain }}`)" + - "traefik.http.routers.headscale.tls=true" + - "traefik.http.routers.headscale.tls.certresolver=letsencrypt" + - "traefik.http.services.headscale.loadbalancer.server.port=8080" + networks: + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "headscale" + +networks: + {{ sovereign_network_name }}: + external: true diff --git a/roles/headscale/templates/headscale-config.yaml.j2 b/roles/headscale/templates/headscale-config.yaml.j2 new file mode 100644 index 0000000..f48ca80 --- /dev/null +++ b/roles/headscale/templates/headscale-config.yaml.j2 @@ -0,0 +1,50 @@ +server_url: "https://{{ headscale_domain }}" +listen_addr: 0.0.0.0:8080 +grpc_listen_addr: 0.0.0.0:50443 +grpc_allow_insecure: false + +private_key_path: /var/lib/headscale/private.key +noise: + private_key_path: /var/lib/headscale/noise_private.key + +prefixes: + v6: fd7a:115c:a1e0::/48 + v4: 100.64.0.0/10 + allocation: sequential + +derp: + server: + enabled: false + urls: + - https://controlplane.tailscale.com/derpmap/default + auto_update_enabled: true + update_frequency: 24h + +disable_check_updates: true +ephemeral_node_inactivity_timeout: 30m + +database: + type: sqlite + sqlite: + path: /var/lib/headscale/db.sqlite + +log: + format: text + level: info + +dns: + magic_dns: true + base_domain: "{{ base_domain }}" + nameservers: + global: + - 1.1.1.1 + - 8.8.8.8 + +oidc: + only_start_if_oidc_is_available: true + issuer: "https://{{ authentik_domain }}/application/o/headscale/" + client_id: "headscale" + client_secret: "changeme_headscale_oidc_secret" + scope: ["openid", "profile", "email"] + extra_params: + domain_hint: "{{ base_domain }}" diff --git a/roles/jitsi/defaults/main.yml b/roles/jitsi/defaults/main.yml new file mode 100644 index 0000000..999d7b5 --- /dev/null +++ b/roles/jitsi/defaults/main.yml @@ -0,0 +1,2 @@ +--- +jitsi_data_dir: "{{ sovereign_base_dir }}/jitsi" diff --git a/roles/jitsi/handlers/main.yml b/roles/jitsi/handlers/main.yml new file mode 100644 index 0000000..7a03733 --- /dev/null +++ b/roles/jitsi/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart jitsi + community.docker.docker_compose_v2: + project_src: "{{ jitsi_data_dir }}" + state: present + recreate: always diff --git a/roles/jitsi/tasks/main.yml b/roles/jitsi/tasks/main.yml new file mode 100644 index 0000000..3592d7b --- /dev/null +++ b/roles/jitsi/tasks/main.yml @@ -0,0 +1,24 @@ +--- +- name: Create Jitsi directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ jitsi_data_dir }}" + - "{{ jitsi_data_dir }}/web" + - "{{ jitsi_data_dir }}/prosody" + - "{{ jitsi_data_dir }}/jicofo" + - "{{ jitsi_data_dir }}/jvb" + +- name: Deploy Jitsi docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ jitsi_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart jitsi + +- name: Start Jitsi + community.docker.docker_compose_v2: + project_src: "{{ jitsi_data_dir }}" + state: present diff --git a/roles/jitsi/templates/docker-compose.yml.j2 b/roles/jitsi/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..420b48b --- /dev/null +++ b/roles/jitsi/templates/docker-compose.yml.j2 @@ -0,0 +1,131 @@ +services: + jitsi-web: + image: jitsi/web:{{ jitsi_version }} + container_name: jitsi-web + restart: unless-stopped + environment: + PUBLIC_URL: "https://{{ jitsi_domain }}" + XMPP_SERVER: jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + XMPP_MUC_DOMAIN: muc.meet.jitsi + XMPP_BOSH_URL_BASE: http://jitsi-prosody:5280 + XMPP_RECORDER_DOMAIN: recorder.meet.jitsi + ENABLE_AUTH: 1 + ENABLE_GUESTS: 1 + AUTH_TYPE: jwt + JWT_APP_ID: "jitsi" + JWT_APP_SECRET: "{{ jitsi_turn_secret }}" + TURN_CREDENTIALS: "{{ jitsi_turn_secret }}" + LETSENCRYPT_DOMAIN: "{{ jitsi_domain }}" + TZ: UTC + volumes: + - {{ jitsi_data_dir }}/web:/config + labels: + - "traefik.enable=true" + - "traefik.http.routers.jitsi.rule=Host(`{{ jitsi_domain }}`)" + - "traefik.http.routers.jitsi.tls=true" + - "traefik.http.routers.jitsi.tls.certresolver=letsencrypt" + - "traefik.http.services.jitsi.loadbalancer.server.port=80" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "jitsi-web" + + jitsi-prosody: + image: jitsi/prosody:{{ jitsi_version }} + container_name: jitsi-prosody + restart: unless-stopped + environment: + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_MUC_DOMAIN: muc.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + XMPP_RECORDER_DOMAIN: recorder.meet.jitsi + JICOFO_AUTH_USER: focus + JICOFO_AUTH_PASSWORD: "{{ jitsi_jicofo_auth_password }}" + JVB_AUTH_USER: jvb + JVB_AUTH_PASSWORD: "{{ jitsi_jvb_auth_password }}" + JIBRI_RECORDER_USER: recorder + JIBRI_RECORDER_PASSWORD: "{{ jitsi_jibri_recorder_password }}" + JIBRI_XMPP_USER: jibri + JIBRI_XMPP_PASSWORD: "{{ jitsi_jibri_xmpp_password }}" + JWT_APP_ID: "jitsi" + JWT_APP_SECRET: "{{ jitsi_turn_secret }}" + ENABLE_AUTH: 1 + ENABLE_GUESTS: 1 + AUTH_TYPE: jwt + TZ: UTC + volumes: + - {{ jitsi_data_dir }}/prosody:/config + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "jitsi-prosody" + + jitsi-jicofo: + image: jitsi/jicofo:{{ jitsi_version }} + container_name: jitsi-jicofo + restart: unless-stopped + depends_on: + - jitsi-prosody + environment: + XMPP_SERVER: jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + JICOFO_AUTH_USER: focus + JICOFO_AUTH_PASSWORD: "{{ jitsi_jicofo_auth_password }}" + JVB_BREWERY_MUC: jvbbrewery + TZ: UTC + volumes: + - {{ jitsi_data_dir }}/jicofo:/config + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "jitsi-jicofo" + + jitsi-jvb: + image: jitsi/jvb:{{ jitsi_version }} + container_name: jitsi-jvb + restart: unless-stopped + depends_on: + - jitsi-prosody + environment: + XMPP_SERVER: jitsi-prosody + XMPP_DOMAIN: meet.jitsi + XMPP_AUTH_DOMAIN: auth.meet.jitsi + XMPP_INTERNAL_MUC_DOMAIN: internal-muc.meet.jitsi + JVB_AUTH_USER: jvb + JVB_AUTH_PASSWORD: "{{ jitsi_jvb_auth_password }}" + JVB_BREWERY_MUC: jvbbrewery + JVB_PORT: 10000 + JVB_TCP_HARVESTER_DISABLED: "true" + TZ: UTC + ports: + - "10000:10000/udp" + volumes: + - {{ jitsi_data_dir }}/jvb:/config + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "jitsi-jvb" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/matrix/defaults/main.yml b/roles/matrix/defaults/main.yml new file mode 100644 index 0000000..bea827e --- /dev/null +++ b/roles/matrix/defaults/main.yml @@ -0,0 +1,2 @@ +--- +matrix_data_dir: "{{ sovereign_base_dir }}/matrix" diff --git a/roles/matrix/handlers/main.yml b/roles/matrix/handlers/main.yml new file mode 100644 index 0000000..2ab0613 --- /dev/null +++ b/roles/matrix/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart matrix + community.docker.docker_compose_v2: + project_src: "{{ matrix_data_dir }}" + state: present + recreate: always diff --git a/roles/matrix/tasks/main.yml b/roles/matrix/tasks/main.yml new file mode 100644 index 0000000..d6baaf9 --- /dev/null +++ b/roles/matrix/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Create Matrix directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ matrix_data_dir }}" + - "{{ matrix_data_dir }}/synapse" + - "{{ matrix_data_dir }}/element" + +- name: Generate Synapse config if not present + ansible.builtin.command: + cmd: > + docker run --rm + -v {{ matrix_data_dir }}/synapse:/data + -e SYNAPSE_SERVER_NAME={{ matrix_domain }} + -e SYNAPSE_REPORT_STATS=no + ghcr.io/element-hq/synapse:{{ matrix_version }} + generate + creates: "{{ matrix_data_dir }}/synapse/homeserver.yaml" + +- name: Deploy Element config + ansible.builtin.template: + src: element-config.json.j2 + dest: "{{ matrix_data_dir }}/element/config.json" + mode: '0644' + +- name: Deploy Matrix docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ matrix_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart matrix + +- name: Start Matrix + community.docker.docker_compose_v2: + project_src: "{{ matrix_data_dir }}" + state: present diff --git a/roles/matrix/templates/docker-compose.yml.j2 b/roles/matrix/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..77f11aa --- /dev/null +++ b/roles/matrix/templates/docker-compose.yml.j2 @@ -0,0 +1,69 @@ +services: + matrix-db: + image: postgres:16-alpine + container_name: matrix-db + restart: unless-stopped + environment: + POSTGRES_USER: synapse + POSTGRES_PASSWORD: "{{ matrix_db_password }}" + POSTGRES_DB: synapse + POSTGRES_INITDB_ARGS: "--encoding='UTF8' --lc-collate='C' --lc-ctype='C'" + volumes: + - {{ matrix_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "matrix-db" + + synapse: + image: ghcr.io/element-hq/synapse:{{ matrix_version }} + container_name: synapse + restart: unless-stopped + depends_on: + - matrix-db + environment: + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + volumes: + - {{ matrix_data_dir }}/synapse:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.matrix.rule=Host(`{{ matrix_domain }}`)" + - "traefik.http.routers.matrix.tls=true" + - "traefik.http.routers.matrix.tls.certresolver=letsencrypt" + - "traefik.http.services.matrix.loadbalancer.server.port=8008" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "synapse" + + element: + image: vectorim/element-web:latest + container_name: element + restart: unless-stopped + volumes: + - {{ matrix_data_dir }}/element/config.json:/app/config.json:ro + labels: + - "traefik.enable=true" + - "traefik.http.routers.element.rule=Host(`{{ element_domain }}`)" + - "traefik.http.routers.element.tls=true" + - "traefik.http.routers.element.tls.certresolver=letsencrypt" + - "traefik.http.services.element.loadbalancer.server.port=80" + networks: + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "element" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/matrix/templates/element-config.json.j2 b/roles/matrix/templates/element-config.json.j2 new file mode 100644 index 0000000..4c57088 --- /dev/null +++ b/roles/matrix/templates/element-config.json.j2 @@ -0,0 +1,32 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://{{ matrix_domain }}", + "server_name": "{{ matrix_domain }}" + } + }, + "brand": "Element", + "integrations_ui_url": "https://scalar.vector.im/", + "integrations_rest_url": "https://scalar.vector.im/api", + "bug_report_endpoint_url": "", + "default_country_code": "US", + "show_labs_settings": false, + "features": {}, + "default_federate": true, + "default_theme": "light", + "room_directory": { + "servers": ["{{ matrix_domain }}"] + }, + "enable_presence_by_hs_url": { + "https://{{ matrix_domain }}": false + }, + "setting_defaults": { + "breadcrumbs": true + }, + "jitsi": { + "preferred_domain": "{{ jitsi_domain }}" + }, + "sso_redirect_options": { + "immediate": false + } +} diff --git a/roles/minio/defaults/main.yml b/roles/minio/defaults/main.yml new file mode 100644 index 0000000..bab786b --- /dev/null +++ b/roles/minio/defaults/main.yml @@ -0,0 +1,2 @@ +--- +minio_data_dir: "{{ sovereign_base_dir }}/minio" diff --git a/roles/minio/handlers/main.yml b/roles/minio/handlers/main.yml new file mode 100644 index 0000000..561d5bc --- /dev/null +++ b/roles/minio/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart minio + community.docker.docker_compose_v2: + project_src: "{{ minio_data_dir }}" + state: present + recreate: always diff --git a/roles/minio/tasks/main.yml b/roles/minio/tasks/main.yml new file mode 100644 index 0000000..5557cbb --- /dev/null +++ b/roles/minio/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Create MinIO directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ minio_data_dir }}" + - "{{ minio_data_dir }}/data" + +- name: Deploy MinIO docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ minio_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart minio + +- name: Start MinIO + community.docker.docker_compose_v2: + project_src: "{{ minio_data_dir }}" + state: present + +- name: Wait for MinIO to be ready + ansible.builtin.uri: + url: "http://localhost:9010/minio/health/live" + method: GET + status_code: 200 + register: result + until: result.status == 200 + retries: 15 + delay: 5 + +- name: Create Nextcloud bucket in MinIO + community.general.minio: + endpoint: "http://localhost:9010" + access_key: "{{ minio_root_user }}" + secret_key: "{{ minio_root_password }}" + name: "{{ minio_nextcloud_bucket }}" + state: present + ignore_errors: true diff --git a/roles/minio/templates/docker-compose.yml.j2 b/roles/minio/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..792ae46 --- /dev/null +++ b/roles/minio/templates/docker-compose.yml.j2 @@ -0,0 +1,42 @@ +services: + minio: + image: quay.io/minio/minio:{{ minio_version }} + container_name: minio + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: "{{ minio_root_user }}" + MINIO_ROOT_PASSWORD: "{{ minio_root_password }}" + MINIO_BROWSER_REDIRECT_URL: "https://{{ minio_console_domain }}" + MINIO_IDENTITY_OPENID_CONFIG_URL: "https://{{ authentik_domain }}/application/o/minio/.well-known/openid-configuration" + MINIO_IDENTITY_OPENID_CLIENT_ID: "minio" + MINIO_IDENTITY_OPENID_CLIENT_SECRET: "changeme_minio_oidc_secret" + MINIO_IDENTITY_OPENID_CLAIM_NAME: "policy" + MINIO_IDENTITY_OPENID_REDIRECT_URI: "https://{{ minio_console_domain }}/oauth_callback" + ports: + - "127.0.0.1:9010:9000" + volumes: + - {{ minio_data_dir }}/data:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.minio-api.rule=Host(`{{ minio_domain }}`)" + - "traefik.http.routers.minio-api.tls=true" + - "traefik.http.routers.minio-api.tls.certresolver=letsencrypt" + - "traefik.http.routers.minio-api.service=minio-api" + - "traefik.http.services.minio-api.loadbalancer.server.port=9000" + - "traefik.http.routers.minio-console.rule=Host(`{{ minio_console_domain }}`)" + - "traefik.http.routers.minio-console.tls=true" + - "traefik.http.routers.minio-console.tls.certresolver=letsencrypt" + - "traefik.http.routers.minio-console.service=minio-console" + - "traefik.http.services.minio-console.loadbalancer.server.port=9001" + networks: + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "minio" + +networks: + {{ sovereign_network_name }}: + external: true diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml new file mode 100644 index 0000000..a362e0f --- /dev/null +++ b/roles/nextcloud/defaults/main.yml @@ -0,0 +1,2 @@ +--- +nextcloud_data_dir: "{{ sovereign_base_dir }}/nextcloud" diff --git a/roles/nextcloud/handlers/main.yml b/roles/nextcloud/handlers/main.yml new file mode 100644 index 0000000..b56acff --- /dev/null +++ b/roles/nextcloud/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart nextcloud + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_data_dir }}" + state: present + recreate: always diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml new file mode 100644 index 0000000..1966549 --- /dev/null +++ b/roles/nextcloud/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: Create Nextcloud directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ nextcloud_data_dir }}" + - "{{ nextcloud_data_dir }}/data" + +- name: Deploy Nextcloud docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ nextcloud_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart nextcloud + +- name: Start Nextcloud + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_data_dir }}" + state: present diff --git a/roles/nextcloud/templates/docker-compose.yml.j2 b/roles/nextcloud/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..79657f7 --- /dev/null +++ b/roles/nextcloud/templates/docker-compose.yml.j2 @@ -0,0 +1,97 @@ +services: + nextcloud-db: + image: mariadb:10.11 + container_name: nextcloud-db + restart: unless-stopped + command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW + environment: + MYSQL_ROOT_PASSWORD: "{{ nextcloud_db_root_password }}" + MYSQL_DATABASE: nextcloud + MYSQL_USER: nextcloud + MYSQL_PASSWORD: "{{ nextcloud_db_password }}" + volumes: + - {{ nextcloud_data_dir }}/db:/var/lib/mysql + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "nextcloud-db" + + nextcloud-redis: + image: redis:alpine + container_name: nextcloud-redis + restart: unless-stopped + networks: + - internal + + nextcloud: + image: nextcloud:{{ nextcloud_version }} + container_name: nextcloud + restart: unless-stopped + depends_on: + - nextcloud-db + - nextcloud-redis + environment: + MYSQL_HOST: nextcloud-db + MYSQL_DATABASE: nextcloud + MYSQL_USER: nextcloud + MYSQL_PASSWORD: "{{ nextcloud_db_password }}" + REDIS_HOST: nextcloud-redis + NEXTCLOUD_ADMIN_USER: "{{ nextcloud_admin_user }}" + NEXTCLOUD_ADMIN_PASSWORD: "{{ nextcloud_admin_password }}" + NEXTCLOUD_TRUSTED_DOMAINS: "{{ nextcloud_domain }}" + OVERWRITEPROTOCOL: https + OVERWRITECLIURL: "https://{{ nextcloud_domain }}" + SMTP_HOST: "{{ smtp_host }}" + SMTP_PORT: "{{ smtp_port }}" + SMTP_NAME: "{{ smtp_user }}" + SMTP_PASSWORD: "{{ smtp_password }}" + MAIL_FROM_ADDRESS: "noreply" + MAIL_DOMAIN: "{{ base_domain }}" + OBJECTSTORE_S3_HOST: minio + OBJECTSTORE_S3_PORT: 9000 + OBJECTSTORE_S3_SSL: "false" + OBJECTSTORE_S3_BUCKET: "{{ minio_nextcloud_bucket }}" + OBJECTSTORE_S3_KEY: "{{ minio_nextcloud_access_key }}" + OBJECTSTORE_S3_SECRET: "{{ minio_nextcloud_secret_key }}" + OBJECTSTORE_S3_USEPATH_STYLE: "true" + volumes: + - {{ nextcloud_data_dir }}/data:/var/www/html + labels: + - "traefik.enable=true" + - "traefik.http.routers.nextcloud.rule=Host(`{{ nextcloud_domain }}`)" + - "traefik.http.routers.nextcloud.tls=true" + - "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt" + - "traefik.http.services.nextcloud.loadbalancer.server.port=80" + - "traefik.http.middlewares.nextcloud-redirect.redirectregex.permanent=true" + - "traefik.http.middlewares.nextcloud-redirect.redirectregex.regex=https://(.*)/.well-known/(?:card|cal)dav" + - "traefik.http.middlewares.nextcloud-redirect.redirectregex.replacement=https://$${1}/remote.php/dav" + - "traefik.http.routers.nextcloud.middlewares=nextcloud-redirect" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "nextcloud" + + nextcloud-cron: + image: nextcloud:{{ nextcloud_version }} + container_name: nextcloud-cron + restart: unless-stopped + volumes: + - {{ nextcloud_data_dir }}/data:/var/www/html + entrypoint: /cron.sh + depends_on: + - nextcloud-db + - nextcloud-redis + networks: + - internal + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/roundcube/defaults/main.yml b/roles/roundcube/defaults/main.yml new file mode 100644 index 0000000..7c6e483 --- /dev/null +++ b/roles/roundcube/defaults/main.yml @@ -0,0 +1,2 @@ +--- +roundcube_data_dir: "{{ sovereign_base_dir }}/roundcube" diff --git a/roles/roundcube/handlers/main.yml b/roles/roundcube/handlers/main.yml new file mode 100644 index 0000000..dc342a8 --- /dev/null +++ b/roles/roundcube/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart roundcube + community.docker.docker_compose_v2: + project_src: "{{ roundcube_data_dir }}" + state: present + recreate: always diff --git a/roles/roundcube/tasks/main.yml b/roles/roundcube/tasks/main.yml new file mode 100644 index 0000000..158d8b0 --- /dev/null +++ b/roles/roundcube/tasks/main.yml @@ -0,0 +1,20 @@ +--- +- name: Create Roundcube directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ roundcube_data_dir }}" + +- name: Deploy Roundcube docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ roundcube_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart roundcube + +- name: Start Roundcube + community.docker.docker_compose_v2: + project_src: "{{ roundcube_data_dir }}" + state: present diff --git a/roles/roundcube/templates/docker-compose.yml.j2 b/roles/roundcube/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..3293b5b --- /dev/null +++ b/roles/roundcube/templates/docker-compose.yml.j2 @@ -0,0 +1,57 @@ +services: + roundcube-db: + image: postgres:16-alpine + container_name: roundcube-db + restart: unless-stopped + environment: + POSTGRES_DB: roundcube + POSTGRES_USER: roundcube + POSTGRES_PASSWORD: "{{ roundcube_db_password }}" + volumes: + - {{ roundcube_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "roundcube-db" + + roundcube: + image: roundcube/roundcubemail:{{ roundcube_version }} + container_name: roundcube + restart: unless-stopped + depends_on: + - roundcube-db + environment: + ROUNDCUBEMAIL_DB_TYPE: pgsql + ROUNDCUBEMAIL_DB_HOST: roundcube-db + ROUNDCUBEMAIL_DB_NAME: roundcube + ROUNDCUBEMAIL_DB_USER: roundcube + ROUNDCUBEMAIL_DB_PASSWORD: "{{ roundcube_db_password }}" + ROUNDCUBEMAIL_DEFAULT_HOST: "ssl://stalwart" + ROUNDCUBEMAIL_DEFAULT_PORT: 993 + ROUNDCUBEMAIL_SMTP_SERVER: "tls://stalwart" + ROUNDCUBEMAIL_SMTP_PORT: 587 + ROUNDCUBEMAIL_DES_KEY: "{{ roundcube_des_key }}" + ROUNDCUBEMAIL_PLUGINS: "archive,zipdownload,managesieve,jqueryui" + ROUNDCUBEMAIL_SKIN: elastic + labels: + - "traefik.enable=true" + - "traefik.http.routers.roundcube.rule=Host(`{{ roundcube_domain }}`)" + - "traefik.http.routers.roundcube.tls=true" + - "traefik.http.routers.roundcube.tls.certresolver=letsencrypt" + - "traefik.http.services.roundcube.loadbalancer.server.port=80" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "roundcube" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/stalwart/defaults/main.yml b/roles/stalwart/defaults/main.yml new file mode 100644 index 0000000..99a5801 --- /dev/null +++ b/roles/stalwart/defaults/main.yml @@ -0,0 +1,2 @@ +--- +stalwart_data_dir: "{{ sovereign_base_dir }}/stalwart" diff --git a/roles/stalwart/handlers/main.yml b/roles/stalwart/handlers/main.yml new file mode 100644 index 0000000..5025722 --- /dev/null +++ b/roles/stalwart/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart stalwart + community.docker.docker_compose_v2: + project_src: "{{ stalwart_data_dir }}" + state: present + recreate: always diff --git a/roles/stalwart/tasks/main.yml b/roles/stalwart/tasks/main.yml new file mode 100644 index 0000000..f32d905 --- /dev/null +++ b/roles/stalwart/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: Create Stalwart directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ stalwart_data_dir }}" + - "{{ stalwart_data_dir }}/data" + +- name: Deploy Stalwart docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ stalwart_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart stalwart + +- name: Start Stalwart + community.docker.docker_compose_v2: + project_src: "{{ stalwart_data_dir }}" + state: present diff --git a/roles/stalwart/templates/docker-compose.yml.j2 b/roles/stalwart/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..003b5b0 --- /dev/null +++ b/roles/stalwart/templates/docker-compose.yml.j2 @@ -0,0 +1,32 @@ +services: + stalwart: + image: stalwartlabs/mail-server:{{ stalwart_version }} + container_name: stalwart + restart: unless-stopped + volumes: + - {{ stalwart_data_dir }}/data:/opt/stalwart-mail + ports: + - "25:25" # SMTP + - "465:465" # SMTPS + - "587:587" # SMTP submission + - "993:993" # IMAPS + - "4190:4190" # ManageSieve + environment: + TZ: UTC + labels: + - "traefik.enable=true" + - "traefik.http.routers.stalwart.rule=Host(`{{ stalwart_domain }}`)" + - "traefik.http.routers.stalwart.tls=true" + - "traefik.http.routers.stalwart.tls.certresolver=letsencrypt" + - "traefik.http.services.stalwart.loadbalancer.server.port=8080" + networks: + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "stalwart" + +networks: + {{ sovereign_network_name }}: + external: true diff --git a/roles/vaultwarden/defaults/main.yml b/roles/vaultwarden/defaults/main.yml new file mode 100644 index 0000000..81f58e8 --- /dev/null +++ b/roles/vaultwarden/defaults/main.yml @@ -0,0 +1,2 @@ +--- +vaultwarden_data_dir: "{{ sovereign_base_dir }}/vaultwarden" diff --git a/roles/vaultwarden/handlers/main.yml b/roles/vaultwarden/handlers/main.yml new file mode 100644 index 0000000..73651d4 --- /dev/null +++ b/roles/vaultwarden/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart vaultwarden + community.docker.docker_compose_v2: + project_src: "{{ vaultwarden_data_dir }}" + state: present + recreate: always diff --git a/roles/vaultwarden/tasks/main.yml b/roles/vaultwarden/tasks/main.yml new file mode 100644 index 0000000..25effb6 --- /dev/null +++ b/roles/vaultwarden/tasks/main.yml @@ -0,0 +1,21 @@ +--- +- name: Create Vaultwarden directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ vaultwarden_data_dir }}" + - "{{ vaultwarden_data_dir }}/data" + +- name: Deploy Vaultwarden docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ vaultwarden_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart vaultwarden + +- name: Start Vaultwarden + community.docker.docker_compose_v2: + project_src: "{{ vaultwarden_data_dir }}" + state: present diff --git a/roles/vaultwarden/templates/docker-compose.yml.j2 b/roles/vaultwarden/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..7350300 --- /dev/null +++ b/roles/vaultwarden/templates/docker-compose.yml.j2 @@ -0,0 +1,63 @@ +services: + vaultwarden-db: + image: postgres:16-alpine + container_name: vaultwarden-db + restart: unless-stopped + environment: + POSTGRES_DB: vaultwarden + POSTGRES_USER: vaultwarden + POSTGRES_PASSWORD: "{{ vaultwarden_db_password }}" + volumes: + - {{ vaultwarden_data_dir }}/db:/var/lib/postgresql/data + networks: + - internal + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "vaultwarden-db" + + vaultwarden: + image: vaultwarden/server:{{ vaultwarden_version }} + container_name: vaultwarden + restart: unless-stopped + depends_on: + - vaultwarden-db + environment: + DATABASE_URL: "postgresql://vaultwarden:{{ vaultwarden_db_password }}@vaultwarden-db/vaultwarden" + ADMIN_TOKEN: "{{ vaultwarden_admin_token }}" + DOMAIN: "https://{{ vaultwarden_domain }}" + SMTP_HOST: "{{ smtp_host }}" + SMTP_FROM: "{{ smtp_from }}" + SMTP_PORT: "{{ smtp_port }}" + SMTP_SECURITY: "{{ smtp_tls }}" + SMTP_USERNAME: "{{ smtp_user }}" + SMTP_PASSWORD: "{{ smtp_password }}" + SIGNUPS_ALLOWED: "false" + SSO_ENABLED: "true" + SSO_ONLY: "false" + SSO_AUTHORITY: "https://{{ authentik_domain }}/application/o/vaultwarden/" + SSO_CLIENT_ID: "vaultwarden" + SSO_CLIENT_SECRET: "changeme_vaultwarden_oidc_secret" + LOG_LEVEL: warn + volumes: + - {{ vaultwarden_data_dir }}/data:/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.vaultwarden.rule=Host(`{{ vaultwarden_domain }}`)" + - "traefik.http.routers.vaultwarden.tls=true" + - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt" + - "traefik.http.services.vaultwarden.loadbalancer.server.port=80" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "vaultwarden" + +networks: + internal: + {{ sovereign_network_name }}: + external: true diff --git a/roles/wazuh/defaults/main.yml b/roles/wazuh/defaults/main.yml new file mode 100644 index 0000000..d1d5503 --- /dev/null +++ b/roles/wazuh/defaults/main.yml @@ -0,0 +1,3 @@ +--- +wazuh_data_dir: "{{ sovereign_base_dir }}/wazuh" +wazuh_indexer_memory: "512m" diff --git a/roles/wazuh/handlers/main.yml b/roles/wazuh/handlers/main.yml new file mode 100644 index 0000000..dc1b156 --- /dev/null +++ b/roles/wazuh/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: restart wazuh + community.docker.docker_compose_v2: + project_src: "{{ wazuh_data_dir }}" + state: present + recreate: always diff --git a/roles/wazuh/tasks/main.yml b/roles/wazuh/tasks/main.yml new file mode 100644 index 0000000..fbe6eb1 --- /dev/null +++ b/roles/wazuh/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create Wazuh directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ wazuh_data_dir }}" + - "{{ wazuh_data_dir }}/config" + +- name: Set vm.max_map_count for Wazuh indexer (OpenSearch) + ansible.posix.sysctl: + name: vm.max_map_count + value: '262144' + state: present + sysctl_set: true + +- name: Deploy Wazuh docker-compose + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ wazuh_data_dir }}/docker-compose.yml" + mode: '0644' + notify: restart wazuh + +- name: Start Wazuh + community.docker.docker_compose_v2: + project_src: "{{ wazuh_data_dir }}" + state: present diff --git a/roles/wazuh/templates/docker-compose.yml.j2 b/roles/wazuh/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..0bdbbe4 --- /dev/null +++ b/roles/wazuh/templates/docker-compose.yml.j2 @@ -0,0 +1,106 @@ +services: + wazuh-manager: + image: wazuh/wazuh-manager:{{ wazuh_version }} + container_name: wazuh-manager + restart: unless-stopped + hostname: wazuh.manager + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 655360 + hard: 655360 + environment: + INDEXER_URL: "https://wazuh-indexer:9200" + INDEXER_USERNAME: admin + INDEXER_PASSWORD: "{{ wazuh_admin_password }}" + FILEBEAT_SSL_VERIFICATION_MODE: full + SSL_CERTIFICATE_AUTHORITIES: /etc/ssl/root-ca.pem + SSL_CERTIFICATE: /etc/ssl/filebeat.pem + SSL_KEY: /etc/ssl/filebeat.key + API_USERNAME: wazuh-wui + API_PASSWORD: "{{ wazuh_api_password }}" + ports: + - "1514:1514" + - "1515:1515" + - "514:514/udp" + - "55000:55000" + volumes: + - {{ wazuh_data_dir }}/wazuh-manager-master:/var/ossec/data + - {{ wazuh_data_dir }}/wazuh-indexer-certs/root-ca-manager.pem:/etc/ssl/root-ca.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.manager.pem:/etc/ssl/filebeat.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.manager-key.pem:/etc/ssl/filebeat.key + - {{ wazuh_data_dir }}/config:/wazuh-config-mount/etc + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "wazuh-manager" + + wazuh-indexer: + image: wazuh/wazuh-indexer:{{ wazuh_version }} + container_name: wazuh-indexer + restart: unless-stopped + hostname: wazuh-indexer + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 + hard: 65536 + environment: + OPENSEARCH_JAVA_OPTS: "-Xms{{ wazuh_indexer_memory }} -Xmx{{ wazuh_indexer_memory }}" + volumes: + - {{ wazuh_data_dir }}/wazuh-indexer-data:/var/lib/wazuh-indexer + - {{ wazuh_data_dir }}/wazuh-indexer-certs/root-ca.pem:/usr/share/wazuh-indexer/certs/root-ca.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.indexer-key.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.key + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.indexer.pem:/usr/share/wazuh-indexer/certs/wazuh.indexer.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/admin.pem:/usr/share/wazuh-indexer/certs/admin.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/admin-key.pem:/usr/share/wazuh-indexer/certs/admin-key.pem + networks: + - internal + + wazuh-dashboard: + image: wazuh/wazuh-dashboard:{{ wazuh_version }} + container_name: wazuh-dashboard + restart: unless-stopped + hostname: wazuh-dashboard + depends_on: + - wazuh-indexer + environment: + INDEXER_USERNAME: admin + INDEXER_PASSWORD: "{{ wazuh_admin_password }}" + WAZUH_API_URL: https://wazuh-manager + DASHBOARD_USERNAME: kibanaserver + DASHBOARD_PASSWORD: "{{ wazuh_admin_password }}" + API_USERNAME: wazuh-wui + API_PASSWORD: "{{ wazuh_api_password }}" + volumes: + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/wazuh.dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem + - {{ wazuh_data_dir }}/wazuh-indexer-certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem + labels: + - "traefik.enable=true" + - "traefik.http.routers.wazuh.rule=Host(`{{ wazuh_domain }}`)" + - "traefik.http.routers.wazuh.tls=true" + - "traefik.http.routers.wazuh.tls.certresolver=letsencrypt" + - "traefik.http.services.wazuh.loadbalancer.server.port=5601" + - "traefik.http.services.wazuh.loadbalancer.server.scheme=https" + networks: + - internal + - {{ sovereign_network_name }} + logging: + driver: gelf + options: + gelf-address: "udp://{{ graylog_host }}:{{ graylog_gelf_port }}" + tag: "wazuh-dashboard" + +networks: + internal: + {{ sovereign_network_name }}: + external: true