Compare commits
14 commits
feature/es
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8954f525c | ||
|
|
3ace667b6c | ||
|
|
3236ca332f | ||
|
|
19864d79b2 | ||
|
|
1dcff92240 | ||
|
|
a9c33baed9 | ||
|
|
60464e6d23 | ||
|
|
f0cd8ba432 | ||
|
|
3855b3e0e7 | ||
|
|
ce50bdb4d3 | ||
|
|
6411f94cce | ||
|
|
99d8968a2e | ||
|
|
2104e5fe7d | ||
|
|
c3cf779532 |
119 changed files with 3100 additions and 2742 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,3 +3,6 @@ __pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
plugins/lookup/__pycache__/
|
plugins/lookup/__pycache__/
|
||||||
|
|
||||||
|
# Local Ansible collection cache (galaxy/collection resolver)
|
||||||
|
/.ansible/
|
||||||
|
|
|
||||||
69
README.md
69
README.md
|
|
@ -1,3 +1,68 @@
|
||||||
# Ansible Collection - digitalboard.core
|
# Ansible Collection — digitalboard.core
|
||||||
|
|
||||||
Documentation for the collection.
|
This collection bundles the Ansible roles used to deploy the
|
||||||
|
[Digitalboard](https://git.digitalboard.ch/Digitalboard) platform: a set of
|
||||||
|
self-hosted, Docker-Compose-based services running behind Traefik, with
|
||||||
|
single sign-on provided by authentik or Keycloak.
|
||||||
|
|
||||||
|
Each role provisions one service (or building block) as a self-contained
|
||||||
|
Docker Compose stack. Roles are consumed from the deployment repository
|
||||||
|
[reference-ansible](https://git.digitalboard.ch/Digitalboard/reference-ansible),
|
||||||
|
where inventories and playbooks tie the roles to concrete hosts.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
| Role | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `base` | Host baseline: Docker, apt packages and convenience tooling on Debian/Ubuntu. |
|
||||||
|
| `traefik` | Traefik v3 reverse proxy as a public DMZ proxy (file provider) or backend proxy (docker provider). |
|
||||||
|
| `authentik` | [authentik](https://goauthentik.io) IdP (server + worker + Postgres); resources via blueprints. |
|
||||||
|
| `authentik_outpost_ldap` | authentik LDAP outpost exposing an LDAP interface for apps that cannot speak OIDC. |
|
||||||
|
| `keycloak` | [Keycloak](https://www.keycloak.org/) IdP with a PostgreSQL backend. |
|
||||||
|
| `389ds` | [389 Directory Server](https://www.port389.org/) LDAP directory via Docker Compose. |
|
||||||
|
| `nextcloud` | [Nextcloud](https://nextcloud.com/) (fpm) + Postgres + Redis, optional Collabora/draw.io/notify_push. |
|
||||||
|
| `opencloud` | [OpenCloud](https://opencloud.eu/) file platform via Docker Compose. |
|
||||||
|
| `collabora` | [Collabora Online](https://www.collaboraonline.com/) (CODE), used as the WOPI backend for Nextcloud. |
|
||||||
|
| `bookstack` | [BookStack](https://www.bookstackapp.com/) wiki (LSIO + MariaDB) with OIDC SSO and daily backups. |
|
||||||
|
| `drawio` | [draw.io](https://www.drawio.com/) diagram editor, with optional authentik ForwardAuth gating. |
|
||||||
|
| `homarr` | [Homarr](https://github.com/homarr-labs/homarr) dashboard with seeded admin user and OIDC group. |
|
||||||
|
| `opnform` | [OpnForm](https://github.com/OpnForm/OpnForm) self-hosted form builder (api + ui + db + redis). |
|
||||||
|
| `send` | [Send](https://github.com/timvisee/send) (timvisee fork) file sharing with a Redis backend. |
|
||||||
|
| `garage` | [Garage](https://garagehq.deuxfleurs.fr/) S3-compatible object storage with key/bucket provisioning. |
|
||||||
|
| `httpbin` | [httpbin](https://httpbin.org/) HTTP request/response testing service for validating Traefik ingress. |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Roles are not run from this repository directly. They are consumed from the
|
||||||
|
deployment repository
|
||||||
|
[reference-ansible](https://git.digitalboard.ch/Digitalboard/reference-ansible),
|
||||||
|
which holds the inventories, group/host variables and playbooks. See that
|
||||||
|
repository's `docs/` directory for getting-started instructions, how to run
|
||||||
|
Ansible and how secrets are managed.
|
||||||
|
|
||||||
|
Per-role variables and their defaults are documented in each role's own
|
||||||
|
`README.md` and `meta/argument_specs.yml`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- A Debian/Ubuntu target host (the `base` role bootstraps Docker there).
|
||||||
|
- ansible-core 2.15 or newer on the controller.
|
||||||
|
- The `community.docker` collection (used by nearly every role) and
|
||||||
|
`community.general` (used by the `keycloak` role). Both are declared as
|
||||||
|
`dependencies` in `galaxy.yml` and pulled in automatically when this
|
||||||
|
collection is installed via `ansible-galaxy`.
|
||||||
|
|
||||||
|
The role READMEs use `community.hashi_vault` lookups in their examples to source
|
||||||
|
secrets from HashiCorp Vault. That is a documented convention, not a hard
|
||||||
|
dependency of the roles — supply the variables however you prefer.
|
||||||
|
|
||||||
|
## Role ordering
|
||||||
|
|
||||||
|
Within a play, apply the roles in dependency order: `base` first (Docker and the
|
||||||
|
host baseline), then `traefik` (the shared reverse proxy and its Docker network),
|
||||||
|
then the individual service roles (`authentik`, `keycloak`, `nextcloud`, …),
|
||||||
|
which attach to Traefik's network and expect Docker to be present.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0. See individual roles for per-role license metadata.
|
||||||
|
|
|
||||||
25
galaxy.yml
25
galaxy.yml
|
|
@ -23,12 +23,12 @@ authors:
|
||||||
|
|
||||||
### OPTIONAL but strongly recommended
|
### OPTIONAL but strongly recommended
|
||||||
# A short summary description of the collection
|
# A short summary description of the collection
|
||||||
description: your collection description
|
description: Ansible roles to deploy the Digitalboard self-hosted service platform (Docker Compose + Traefik + SSO)
|
||||||
|
|
||||||
# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
|
# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only
|
||||||
# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
|
# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file'
|
||||||
license:
|
license:
|
||||||
- GPL-2.0-or-later
|
- MIT-0
|
||||||
|
|
||||||
# The path to the license file for the collection. This path is relative to the root of the collection. This key is
|
# The path to the license file for the collection. This path is relative to the root of the collection. This key is
|
||||||
# mutually exclusive with 'license'
|
# mutually exclusive with 'license'
|
||||||
|
|
@ -36,25 +36,36 @@ license_file: ''
|
||||||
|
|
||||||
# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
|
# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character
|
||||||
# requirements as 'namespace' and 'name'
|
# requirements as 'namespace' and 'name'
|
||||||
tags: []
|
tags:
|
||||||
|
- digitalboard
|
||||||
|
- docker
|
||||||
|
- traefik
|
||||||
|
- sso
|
||||||
|
- selfhosted
|
||||||
|
|
||||||
# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
|
# Collections that this collection requires to be installed for it to be usable. The key of the dict is the
|
||||||
# collection label 'namespace.name'. The value is a version range
|
# collection label 'namespace.name'. The value is a version range
|
||||||
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
|
# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version
|
||||||
# range specifiers can be set and are separated by ','
|
# range specifiers can be set and are separated by ','
|
||||||
dependencies: {}
|
dependencies:
|
||||||
|
# Used by nearly every role: docker_compose_v2, docker_container,
|
||||||
|
# docker_container_exec, docker_network. Hard runtime dependency.
|
||||||
|
community.docker: '>=3.0.0'
|
||||||
|
# Used by the keycloak role (keycloak_realm/client/group/user and
|
||||||
|
# related modules) in roles/keycloak/tasks/provisioning.yml.
|
||||||
|
community.general: '>=7.0.0'
|
||||||
|
|
||||||
# The URL of the originating SCM repository
|
# The URL of the originating SCM repository
|
||||||
repository: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
repository: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
||||||
|
|
||||||
# The URL to any online docs
|
# The URL to any online docs
|
||||||
documentation: http://docs.example.com
|
documentation: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
||||||
|
|
||||||
# The URL to the homepage of the collection/project
|
# The URL to the homepage of the collection/project
|
||||||
homepage: http://example.com
|
homepage: https://git.digitalboard.ch/Digitalboard/digitalboard.core
|
||||||
|
|
||||||
# The URL to the collection issue tracker
|
# The URL to the collection issue tracker
|
||||||
issues: http://example.com/issue/tracker
|
issues: https://git.digitalboard.ch/Digitalboard/digitalboard.core/issues
|
||||||
|
|
||||||
# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
|
# A list of file glob-like patterns used to filter any files or directories that should not be included in the build
|
||||||
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
|
# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# Collections must specify a minimum required ansible version to upload
|
# Collections must specify a minimum required ansible version to upload
|
||||||
# to galaxy
|
# to galaxy. Aligned with the highest min_ansible_version declared by the
|
||||||
# requires_ansible: '>=2.9.10'
|
# roles (the traefik role requires ansible-core 2.15).
|
||||||
|
requires_ansible: '>=2.15.0'
|
||||||
|
|
||||||
# Content that Ansible needs to load from another location or that has
|
# Content that Ansible needs to load from another location or that has
|
||||||
# been deprecated/removed
|
# been deprecated/removed
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
# Collections Plugins Directory
|
# Collection Plugins — digitalboard.core
|
||||||
|
|
||||||
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
|
This collection ships a small number of custom plugins that support the roles.
|
||||||
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
|
They are addressed by their fully qualified name, `digitalboard.core.<name>`.
|
||||||
would contain module utils and modules respectively.
|
|
||||||
|
|
||||||
Here is an example directory of the majority of plugins currently supported by Ansible:
|
## Filter plugins (`filter/`)
|
||||||
|
|
||||||
```
|
`homarr_layout` — computes Homarr dashboard grid layouts (desktop / tablet /
|
||||||
└── plugins
|
mobile breakpoints) from a list of apps, returning a ready-to-render data
|
||||||
├── action
|
structure for the SQL seed. Used by the `homarr` role.
|
||||||
├── become
|
|
||||||
├── cache
|
```yaml
|
||||||
├── callback
|
- name: Compute Homarr app layouts
|
||||||
├── cliconf
|
ansible.builtin.set_fact:
|
||||||
├── connection
|
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
||||||
├── filter
|
|
||||||
├── httpapi
|
|
||||||
├── inventory
|
|
||||||
├── lookup
|
|
||||||
├── module_utils
|
|
||||||
├── modules
|
|
||||||
├── netconf
|
|
||||||
├── shell
|
|
||||||
├── strategy
|
|
||||||
├── terminal
|
|
||||||
├── test
|
|
||||||
└── vars
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.19/plugins/plugins.html).
|
## Lookup plugins (`lookup/`)
|
||||||
|
|
||||||
|
`garage_credentials` — returns S3 credentials (`key_id`, `secret_key`) for a
|
||||||
|
named Garage key by executing a docker command on the target host. Used to wire
|
||||||
|
Garage object storage into consuming roles such as `nextcloud`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
nextcloud_s3_key: >-
|
||||||
|
{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='backend')['key_id'] }}
|
||||||
|
nextcloud_s3_secret: >-
|
||||||
|
{{ lookup('digitalboard.core.garage_credentials', 'nextcloud', host='backend')['secret_key'] }}
|
||||||
|
```
|
||||||
|
|
||||||
|
No other plugin types (modules, action, callback, inventory, etc.) are currently
|
||||||
|
shipped by this collection.
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,43 @@
|
||||||
Role Name
|
# 389ds
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploys [389 Directory Server](https://www.port389.org/) (`389ds/dirsrv`)
|
||||||
|
as an LDAP directory via Docker Compose. After the container starts, the
|
||||||
|
role creates the configured suffix and a set of base organizational
|
||||||
|
units (e.g. `users`, `groups`).
|
||||||
|
|
||||||
Requirements
|
## Requirements
|
||||||
------------
|
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
- Docker and Docker Compose on the target host (e.g. via
|
||||||
|
`digitalboard.core.base`)
|
||||||
|
- Ansible collection: `community.docker`
|
||||||
|
|
||||||
Role Variables
|
## Role variables
|
||||||
--------------
|
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ds389_image` | `docker.io/389ds/dirsrv:3.1` | Container image. |
|
||||||
|
| `ds389_suffix` | `dc=example,dc=com` | Root suffix of the directory. |
|
||||||
|
| `ds389_root_dn` | `cn=Directory Manager` | Directory Manager bind DN. |
|
||||||
|
| `ds389_root_password` | `changeme` | Directory Manager password — **override this**. |
|
||||||
|
| `ds389_instance_name` | `localhost` | Directory server instance name (slapd config dir). |
|
||||||
|
| `ds389_hostname` | `389ds` | Container hostname (defaults to `ds389_service_name`). |
|
||||||
|
| `ds389_backend_network` | `backend` | Docker network LDAP clients connect over (created by Compose). |
|
||||||
|
| `ds389_ldap_port` | `3389` | Published LDAP port (container port 3389). |
|
||||||
|
| `ds389_ldaps_port` | `3636` | Published LDAPS port (container port 3636). |
|
||||||
|
| `ds389_base_ous` | `[users, groups]` | Base OUs created after startup. |
|
||||||
|
|
||||||
Dependencies
|
## Example
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
```yaml
|
||||||
|
- hosts: directory
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.389ds
|
||||||
|
vars:
|
||||||
|
ds389_suffix: "dc=example,dc=org"
|
||||||
|
ds389_root_password: "{{ vault_ds389_root_password }}"
|
||||||
|
```
|
||||||
|
|
||||||
Example Playbook
|
## License
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
MIT-0
|
||||||
|
|
||||||
- hosts: servers
|
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,26 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy 389 Directory Server (LDAP) via Docker Compose
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- 389ds
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- ldap
|
||||||
# min_ansible_container_version:
|
- directory
|
||||||
|
- docker
|
||||||
galaxy_tags: []
|
- digitalboard
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,136 @@
|
||||||
# Authentik
|
# Authentik
|
||||||
|
|
||||||
Deploys Authentik identity provider with Docker Compose.
|
Deploys [authentik](https://goauthentik.io) (server + worker + Postgres)
|
||||||
|
as a Docker Compose stack behind Traefik, with all resources provisioned
|
||||||
|
via templated blueprints.
|
||||||
|
|
||||||
|
## What this role does
|
||||||
|
|
||||||
|
- Renders the Compose stack with traefik labels and an optional
|
||||||
|
split-horizon host rewrite (see below)
|
||||||
|
- Provisions local users, groups, OIDC apps, Proxy/ForwardAuth apps,
|
||||||
|
LDAP apps and outposts, and Entra ID OAuth sources via blueprints
|
||||||
|
- Configures the login screen (visible sources, local login fields)
|
||||||
|
- Supports declarative cleanup via `authentik_removed_*` lists
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
See `defaults/main.yml` for all available variables.
|
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
||||||
|
common overrides:
|
||||||
|
|
||||||
## Blueprints
|
### Service
|
||||||
|
|
||||||
|
- `authentik_domains` (required, list): FQDNs the router accepts. First
|
||||||
|
entry is the canonical hostname; further entries cover internal
|
||||||
|
`*.int.*` names for server-to-server traffic.
|
||||||
|
- `authentik_secret_key` (required): PG fernet / signing secret.
|
||||||
|
Generate with `openssl rand -base64 60`.
|
||||||
|
- `authentik_postgres_password` (required).
|
||||||
|
- `authentik_image`, `authentik_port`, `authentik_log_level`.
|
||||||
|
|
||||||
|
### Split-horizon host rewrite
|
||||||
|
|
||||||
|
`authentik_host_rewrite_domains` lists hostnames that should reach the
|
||||||
|
authentik container but make it generate URLs (OIDC issuer, password
|
||||||
|
reset links, etc.) as if the request had arrived on
|
||||||
|
`authentik_domains[0]`.
|
||||||
|
|
||||||
|
For each entry the role:
|
||||||
|
|
||||||
|
- Creates a dedicated traefik router on that hostname
|
||||||
|
- Routes it to a URL-based loadbalancer service that disables
|
||||||
|
`passHostHeader`, so the upstream Host header becomes the canonical
|
||||||
|
FQDN
|
||||||
|
- Pins `X-Forwarded-Host` via middleware so the iss claim stays aligned
|
||||||
|
with the public hostname browsers see
|
||||||
|
|
||||||
|
Use case: an internal `auth.int.example.com` keeps server-to-server
|
||||||
|
traffic in the LAN, but Keycloak/Nextcloud/etc. still receive issuer
|
||||||
|
URLs matching `auth.example.com`.
|
||||||
|
|
||||||
|
### Blueprints
|
||||||
|
|
||||||
The role renders blueprints for:
|
The role renders blueprints for:
|
||||||
|
|
||||||
- Local users (`authentik_local_users`)
|
- Local users (`authentik_local_users`)
|
||||||
|
- Groups (`authentik_groups`)
|
||||||
- OIDC applications (`authentik_oidc_apps`)
|
- OIDC applications (`authentik_oidc_apps`)
|
||||||
- Proxy applications (`authentik_proxy_apps`)
|
- Proxy applications (`authentik_proxy_apps`)
|
||||||
- Proxy outposts (`authentik_proxy_outposts`)
|
- Proxy outposts (`authentik_proxy_outposts`)
|
||||||
|
- LDAP applications (`authentik_ldap_apps`)
|
||||||
|
- LDAP outpost (`authentik_ldap_outpost`)
|
||||||
- Entra ID sources (`authentik_entra_sources`)
|
- Entra ID sources (`authentik_entra_sources`)
|
||||||
- Login screen sources (`authentik_login_source_ids`)
|
- Login-screen source visibility (`authentik_login_sources`)
|
||||||
|
|
||||||
Secrets are passed via `authentik_blueprint_env` using environment variable references.
|
Secrets are passed via the `authentik_blueprint_env` env-var indirection
|
||||||
|
so they never land in rendered blueprint YAML on disk.
|
||||||
|
|
||||||
|
#### Proxy apps: mode and group restrictions
|
||||||
|
|
||||||
|
Each entry in `authentik_proxy_apps` supports:
|
||||||
|
|
||||||
|
- `mode` (default `forward_single`): one of `proxy`, `forward_single`,
|
||||||
|
`forward_domain`
|
||||||
|
- `allowed_groups`: when set, a `PolicyBinding` is emitted per group on
|
||||||
|
the application. authentik OR-evaluates bindings, so users in any
|
||||||
|
listed group pass and users in none are denied.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
authentik_proxy_apps:
|
||||||
|
- slug: drawio
|
||||||
|
name: drawio
|
||||||
|
external_host: "https://drawio.example.com"
|
||||||
|
mode: forward_single
|
||||||
|
allowed_groups:
|
||||||
|
- drawio-users
|
||||||
|
- admins
|
||||||
|
```
|
||||||
|
|
||||||
## Removing resources
|
## Removing resources
|
||||||
|
|
||||||
To remove resources from Authentik, move slugs to the removal lists:
|
Move slugs from the active list to the matching removal list:
|
||||||
|
|
||||||
- `authentik_removed_oidc_apps`
|
- `authentik_removed_oidc_apps`
|
||||||
- `authentik_removed_proxy_apps`
|
- `authentik_removed_proxy_apps`
|
||||||
- `authentik_removed_local_users`
|
- `authentik_removed_local_users`
|
||||||
|
|
||||||
After confirming deletion, remove the slug from the list.
|
After authentik has applied the deletion blueprint, remove the slug
|
||||||
|
from the list to keep state clean.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Run `digitalboard.core.base` first (Docker) and have the `community.docker`
|
||||||
|
collection installed; the role drives the stack via
|
||||||
|
`community.docker.docker_compose_v2`.
|
||||||
|
- Traefik network (`authentik_traefik_network`, default `proxy`) must exist
|
||||||
|
beforehand (e.g. created by the traefik role); it is referenced as an
|
||||||
|
external network in the Compose file.
|
||||||
|
- Internal backend network (`authentik_backend_network`, default `backend`).
|
||||||
|
|
||||||
|
## Example playbook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- hosts: identity_servers
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.authentik
|
||||||
|
vars:
|
||||||
|
authentik_domains:
|
||||||
|
- "auth.example.com"
|
||||||
|
- "auth.int.example.com"
|
||||||
|
authentik_host_rewrite_domains:
|
||||||
|
- "auth.int.example.com"
|
||||||
|
authentik_secret_key: "{{ vault_authentik_secret_key }}"
|
||||||
|
authentik_postgres_password: "{{ vault_authentik_pg_password }}"
|
||||||
|
authentik_proxy_apps:
|
||||||
|
- slug: drawio
|
||||||
|
name: drawio
|
||||||
|
external_host: "https://drawio.example.com"
|
||||||
|
mode: forward_single
|
||||||
|
allowed_groups: [drawio-users]
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,20 @@ authentik_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_servic
|
||||||
authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_name }}"
|
authentik_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ authentik_service_name }}"
|
||||||
|
|
||||||
# Authentik service configuration
|
# Authentik service configuration
|
||||||
authentik_domain: "authentik.local.test"
|
# FQDNs the authentik router accepts. The first entry is the canonical
|
||||||
|
# domain; further entries cover internal *.int.* names used for
|
||||||
|
# server-to-server traffic so backend calls don't hairpin via DMZ.
|
||||||
|
authentik_domains:
|
||||||
|
- "authentik.local.test"
|
||||||
|
|
||||||
|
# Hostnames that should reach authentik but make it generate URLs (OIDC
|
||||||
|
# issuer, password reset links, etc.) as if requested from the canonical
|
||||||
|
# `authentik_domains[0]` instead. Used for split-horizon setups where an
|
||||||
|
# internal FQDN (e.g. `auth.int.example.com`) keeps server-to-server
|
||||||
|
# traffic in the LAN but the iss claim must still match the public
|
||||||
|
# hostname that browsers see. Traefik handles each entry via a separate
|
||||||
|
# router that rewrites the Host header before forwarding to authentik.
|
||||||
|
authentik_host_rewrite_domains: []
|
||||||
authentik_image: "ghcr.io/goauthentik/server:2026.2.2"
|
authentik_image: "ghcr.io/goauthentik/server:2026.2.2"
|
||||||
authentik_port: 9000
|
authentik_port: 9000
|
||||||
authentik_secret_key: "changeme-generate-a-random-string"
|
authentik_secret_key: "changeme-generate-a-random-string"
|
||||||
|
|
|
||||||
193
roles/authentik/meta/argument_specs.yml
Normal file
193
roles/authentik/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy authentik (server + worker + Postgres) via Docker Compose.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for authentik with traefik labels, optional
|
||||||
|
TLS and a configurable split-horizon host-rewrite that keeps the OIDC
|
||||||
|
issuer URL on the canonical public hostname even when traffic enters
|
||||||
|
on an internal FQDN.
|
||||||
|
- Provisions resources through templated blueprints
|
||||||
|
(local users, groups, OIDC/Proxy/LDAP apps, outposts, OAuth sources).
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
docker_volume_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /srv/data
|
||||||
|
authentik_service_name:
|
||||||
|
type: str
|
||||||
|
default: authentik
|
||||||
|
authentik_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_compose_base_dir }}/{{ authentik_service_name }}).
|
||||||
|
authentik_docker_volume_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_volume_base_dir }}/{{ authentik_service_name }}).
|
||||||
|
|
||||||
|
authentik_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
- FQDNs the authentik router accepts. The first entry is the
|
||||||
|
canonical (public) hostname and is used for the network alias,
|
||||||
|
the X-Forwarded-Host rewrite target, and as the default OIDC
|
||||||
|
issuer. Further entries cover internal C(*.int.*) names used
|
||||||
|
for server-to-server traffic.
|
||||||
|
authentik_host_rewrite_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Hostnames that should reach authentik but make it generate URLs
|
||||||
|
(OIDC issuer, password reset links, etc.) as if the request had
|
||||||
|
arrived on C(authentik_domains[0]).
|
||||||
|
- Each entry gets its own traefik router and a URL-based
|
||||||
|
loadbalancer service that disables passHostHeader and pins
|
||||||
|
X-Forwarded-Host via middleware. Used for split-horizon setups
|
||||||
|
where the LAN keeps server-to-server traffic but the iss claim
|
||||||
|
must match the public hostname browsers see.
|
||||||
|
authentik_image:
|
||||||
|
type: str
|
||||||
|
default: ghcr.io/goauthentik/server:2026.2.2
|
||||||
|
authentik_port:
|
||||||
|
type: int
|
||||||
|
default: 9000
|
||||||
|
authentik_secret_key:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: PG fernet key / signing secret. Generate with C(openssl rand -base64 60).
|
||||||
|
|
||||||
|
authentik_postgres_image:
|
||||||
|
type: str
|
||||||
|
default: postgres:16-alpine
|
||||||
|
authentik_postgres_db:
|
||||||
|
type: str
|
||||||
|
default: authentik
|
||||||
|
authentik_postgres_user:
|
||||||
|
type: str
|
||||||
|
default: authentik
|
||||||
|
authentik_postgres_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
|
||||||
|
authentik_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
authentik_backend_network:
|
||||||
|
type: str
|
||||||
|
default: backend
|
||||||
|
authentik_use_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
||||||
|
authentik_log_level:
|
||||||
|
type: str
|
||||||
|
choices: [trace, debug, info, warning, error]
|
||||||
|
default: info
|
||||||
|
authentik_error_reporting_enabled:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
|
||||||
|
authentik_proxy_apps:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Proxy/ForwardAuth applications rendered via the
|
||||||
|
C(blueprint-proxy-app.yaml.j2) template.
|
||||||
|
options:
|
||||||
|
slug:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
name:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
internal_host:
|
||||||
|
type: str
|
||||||
|
description: Required when C(mode=proxy).
|
||||||
|
external_host:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
mode:
|
||||||
|
type: str
|
||||||
|
choices: [proxy, forward_single, forward_domain]
|
||||||
|
default: forward_single
|
||||||
|
description:
|
||||||
|
- "C(proxy): the outpost itself proxies traffic to internal_host."
|
||||||
|
- "C(forward_single): a single app behind an external reverse
|
||||||
|
proxy via ForwardAuth."
|
||||||
|
- "C(forward_domain): wildcard mode — one provider guards every
|
||||||
|
host on a cookie domain."
|
||||||
|
allowed_groups:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
description:
|
||||||
|
- If set, PolicyBindings are emitted (one per group, OR-evaluated).
|
||||||
|
Users in none of the listed groups are denied.
|
||||||
|
skip_path_regex:
|
||||||
|
type: str
|
||||||
|
flows:
|
||||||
|
type: dict
|
||||||
|
description: Authentication / authorization / invalidation flow slugs.
|
||||||
|
|
||||||
|
authentik_proxy_outposts:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
|
||||||
|
authentik_ldap_apps:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
authentik_ldap_outpost:
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
|
||||||
|
authentik_oidc_apps:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
|
||||||
|
authentik_entra_sources:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
authentik_login_sources:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
authentik_identification_stage_name:
|
||||||
|
type: str
|
||||||
|
default: default-authentication-identification
|
||||||
|
authentik_login_user_fields:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
choices: [username, email, upn]
|
||||||
|
default: [username, email]
|
||||||
|
description: Local login fields shown on the login screen. Empty list hides local login.
|
||||||
|
|
||||||
|
authentik_groups:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
authentik_local_users:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
|
||||||
|
authentik_removed_oidc_apps:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description: OIDC application slugs scheduled for deletion.
|
||||||
|
authentik_removed_proxy_apps:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
authentik_removed_local_users:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
|
@ -1,35 +1,28 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy authentik (server + worker + Postgres) via Docker Compose with blueprint-provisioned resources
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- authentik
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- oidc
|
||||||
# min_ansible_container_version:
|
- sso
|
||||||
|
- idp
|
||||||
galaxy_tags: []
|
- docker
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- traefik
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
- digitalboard
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ entries:
|
||||||
{% for field in authentik_login_user_fields %}
|
{% for field in authentik_login_user_fields %}
|
||||||
- {{ field }}
|
- {{ field }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if authentik_login_sources %}
|
||||||
# OAuth/social login sources (use !Find to reference sources from other blueprints)
|
# OAuth/social login sources (use !Find to reference sources from other blueprints)
|
||||||
sources:
|
sources:
|
||||||
{% for src in authentik_login_sources %}
|
{% for src in authentik_login_sources %}
|
||||||
- !Find [authentik_sources_oauth.oauthsource, [slug, {{ src.slug }}]]
|
- !Find [authentik_sources_oauth.oauthsource, [slug, {{ src.slug }}]]
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,16 @@ entries:
|
||||||
|
|
||||||
internal_host: "{{ item.internal_host }}"
|
internal_host: "{{ item.internal_host }}"
|
||||||
external_host: "{{ item.external_host }}"
|
external_host: "{{ item.external_host }}"
|
||||||
|
{# Provider mode controls how authentik treats the proxy app:
|
||||||
|
- proxy : the outpost itself proxies traffic to internal_host
|
||||||
|
- forward_single : a single app behind an external reverse proxy
|
||||||
|
(traefik forwardauth talks to authentik per-domain)
|
||||||
|
- forward_domain : wildcard mode — one provider guards every host on a
|
||||||
|
cookie domain; configure forward_auth_mode=domain on
|
||||||
|
the outpost in that case. Default to forward_single
|
||||||
|
since that's the common ForwardAuth-with-traefik
|
||||||
|
pattern. #}
|
||||||
|
mode: {{ item.mode | default('forward_single') }}
|
||||||
|
|
||||||
{% if item.skip_path_regex is defined and item.skip_path_regex|length > 0 %}
|
{% if item.skip_path_regex is defined and item.skip_path_regex|length > 0 %}
|
||||||
skip_path_regex: |
|
skip_path_regex: |
|
||||||
|
|
@ -34,3 +44,20 @@ entries:
|
||||||
name: "{{ item.name | default(item.slug) }}"
|
name: "{{ item.name | default(item.slug) }}"
|
||||||
slug: {{ item.slug }}
|
slug: {{ item.slug }}
|
||||||
provider: !KeyOf proxy-provider-{{ item.slug }}
|
provider: !KeyOf proxy-provider-{{ item.slug }}
|
||||||
|
|
||||||
|
{% if item.allowed_groups is defined and item.allowed_groups | length > 0 %}
|
||||||
|
{# Restrict access to listed groups: one PolicyBinding per group, all bound
|
||||||
|
to the application. Authentik treats multiple bindings on the same target
|
||||||
|
as OR (a user matching any binding passes), and a request from a user in
|
||||||
|
none of the bound groups is denied. #}
|
||||||
|
{% for group_name in item.allowed_groups %}
|
||||||
|
- model: authentik_policies.policybinding
|
||||||
|
identifiers:
|
||||||
|
target: !KeyOf app-{{ item.slug }}
|
||||||
|
order: {{ loop.index0 }}
|
||||||
|
group: !Find [authentik_core.group, [name, "{{ group_name }}"]]
|
||||||
|
attrs:
|
||||||
|
enabled: true
|
||||||
|
negate: false
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -43,19 +43,58 @@ services:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- {{ authentik_backend_network }}
|
{{ authentik_backend_network }}: {}
|
||||||
- {{ authentik_traefik_network }}
|
# No alias for the public FQDN here: that would shadow `/etc/hosts`
|
||||||
|
# pins (extra_hosts) in other containers sharing this network and
|
||||||
|
# break OIDC discovery for Node-based clients (c-ares-based
|
||||||
|
# resolvers consult Docker DNS before /etc/hosts). The URL-based
|
||||||
|
# service below addresses this container by its compose service
|
||||||
|
# name `server`, which Docker exposes as an alias on every network
|
||||||
|
# the container joins.
|
||||||
|
{{ authentik_traefik_network }}: {}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ authentik_traefik_network }}
|
- traefik.docker.network={{ authentik_traefik_network }}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.rule=Host(`{{ authentik_domain }}`)
|
- traefik.http.routers.{{ authentik_service_name }}.rule={% for d in authentik_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}.service={{ authentik_service_name }}
|
||||||
{% if authentik_use_ssl %}
|
{% if authentik_use_ssl %}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.tls=true
|
- traefik.http.routers.{{ authentik_service_name }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ authentik_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- traefik.http.services.{{ authentik_service_name }}.loadbalancer.server.port={{ authentik_port }}
|
- traefik.http.services.{{ authentik_service_name }}.loadbalancer.server.port={{ authentik_port }}
|
||||||
|
{% if authentik_host_rewrite_domains | length > 0 %}
|
||||||
|
# Server-to-server entry: a separate service points at this very
|
||||||
|
# container by its compose service name `server` and disables
|
||||||
|
# passHostHeader so the upstream Host header becomes
|
||||||
|
# `{{ authentik_domains[0] }}`. Authentik builds OIDC issuer URLs
|
||||||
|
# from X-Forwarded-Host (not Host), so we also pin that header via
|
||||||
|
# middleware. Together this keeps the iss claim aligned with the
|
||||||
|
# public hostname browsers see during login, even when the request
|
||||||
|
# itself arrived on an internal *.int.* FQDN.
|
||||||
|
- traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.server.url=http://server:{{ authentik_port }}
|
||||||
|
- traefik.http.services.{{ authentik_service_name }}-rewrite.loadbalancer.passhostheader=false
|
||||||
|
- traefik.http.middlewares.{{ authentik_service_name }}-xfh-rewrite.headers.customrequestheaders.X-Forwarded-Host={{ authentik_domains[0] }}
|
||||||
|
{% for d in authentik_host_rewrite_domains %}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.rule=Host(`{{ d }}`)
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.priority=100
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.service={{ authentik_service_name }}-rewrite
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.middlewares={{ authentik_service_name }}-xfh-rewrite
|
||||||
|
{% if authentik_use_ssl %}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.entrypoints=websecure
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
- traefik.http.routers.{{ authentik_service_name }}-rewrite-{{ loop.index0 }}.entrypoints=web
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
image: {{ authentik_image }}
|
image: {{ authentik_image }}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,44 @@
|
||||||
Role Name
|
# authentik_outpost_ldap
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploys an [authentik](https://goauthentik.io) LDAP outpost via Docker
|
||||||
|
Compose. The outpost exposes an LDAP interface backed by authentik, so
|
||||||
|
applications that cannot speak OIDC (e.g. Nextcloud or OpenCloud LDAP
|
||||||
|
backends) can still authenticate against the central IdP.
|
||||||
|
|
||||||
Requirements
|
The outpost connects back to an authentik server using an outpost token
|
||||||
------------
|
issued in the authentik admin interface. The image version must match
|
||||||
|
the authentik server version.
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
## Requirements
|
||||||
|
|
||||||
Role Variables
|
- Docker and Docker Compose on the target host (e.g. via
|
||||||
--------------
|
`digitalboard.core.base`)
|
||||||
|
- Ansible collection: `community.docker`
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
## Role variables
|
||||||
|
|
||||||
Dependencies
|
| Variable | Default | Description |
|
||||||
------------
|
| --- | --- | --- |
|
||||||
|
| `authentik_outpost_ldap_image` | `ghcr.io/goauthentik/ldap:2026.2.2` | Outpost image (match the server version). |
|
||||||
|
| `authentik_outpost_ldap_host` | `https://authentik.local.test` | URL of the authentik server. |
|
||||||
|
| `authentik_outpost_ldap_token` | `changeme` | Outpost token — **override this**. |
|
||||||
|
| `authentik_outpost_ldap_insecure` | `"true"` | Skip TLS verification toward the authentik server. |
|
||||||
|
| `authentik_outpost_ldap_network` | `ldap` | Docker network LDAP clients connect over (created by the role). |
|
||||||
|
| `authentik_outpost_ldap_authentik_network` | _unset_ | Optional extra external network to the authentik server. |
|
||||||
|
| `authentik_outpost_ldap_extra_hosts` | `[]` | Extra `host:ip` entries for in-container DNS. |
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
## Example
|
||||||
|
|
||||||
Example Playbook
|
```yaml
|
||||||
----------------
|
- hosts: directory
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.authentik_outpost_ldap
|
||||||
|
vars:
|
||||||
|
authentik_outpost_ldap_host: "https://auth.example.com"
|
||||||
|
authentik_outpost_ldap_token: "{{ vault_authentik_ldap_outpost_token }}"
|
||||||
|
```
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
## License
|
||||||
|
|
||||||
- hosts: servers
|
MIT-0
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy an authentik LDAP outpost via Docker Compose for applications that cannot use OIDC
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- authentik
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- ldap
|
||||||
# min_ansible_container_version:
|
- outpost
|
||||||
|
- sso
|
||||||
galaxy_tags: []
|
- docker
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,45 @@
|
||||||
Role Name
|
# base
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Host baseline for the Digitalboard platform. Installs Docker (engine,
|
||||||
|
CLI, containerd, buildx, compose plugin) and a small set of apt and
|
||||||
|
convenience packages on Debian/Ubuntu, and sets the shared directory
|
||||||
|
layout every other role builds on.
|
||||||
|
|
||||||
Requirements
|
This role is intended to run first on every host, before any
|
||||||
------------
|
service role.
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
## What it does
|
||||||
|
|
||||||
Role Variables
|
- Installs Docker prerequisites (`apt-transport-https`, `ca-certificates`,
|
||||||
--------------
|
`curl`, `gnupg`, `lsb-release`, `apache2-utils` for `htpasswd`) plus
|
||||||
|
convenience packages (`htop`, `ncdu`, `vim`) and Docker itself
|
||||||
|
(`docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`,
|
||||||
|
`docker-compose-plugin`).
|
||||||
|
- Optionally configures Docker registry mirrors via `/etc/docker/daemon.json`.
|
||||||
|
- Starts and enables the Docker service and writes a custom `/etc/motd`.
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
This role defines the shared directory-layout variables
|
||||||
|
(`docker_compose_base_dir`, `docker_volume_base_dir`) that every service
|
||||||
|
role consumes, but the per-service subdirectories are created by the
|
||||||
|
respective service roles, not here.
|
||||||
|
|
||||||
Dependencies
|
## Role variables
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `docker_compose_base_dir` | `/etc/docker/compose` | Root directory for per-service Compose projects. |
|
||||||
|
| `docker_volume_base_dir` | `/srv/data` | Root directory for per-service persistent volumes. |
|
||||||
|
| `docker_registry_mirrors` | `[]` | Optional list of registry mirror URLs; empty disables mirrors. |
|
||||||
|
|
||||||
Example Playbook
|
## Example
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
```yaml
|
||||||
|
- hosts: all
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- digitalboard.core.base
|
||||||
|
```
|
||||||
|
|
||||||
- hosts: servers
|
## License
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
MIT-0
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,25 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Host baseline — install Docker, required apt packages and convenience tooling on Debian/Ubuntu
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
galaxy_tags:
|
||||||
|
- base
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- docker
|
||||||
# min_ansible_container_version:
|
- bootstrap
|
||||||
|
- digitalboard
|
||||||
galaxy_tags: []
|
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,14 @@ The role asserts these are set; the play fails fast if any is empty:
|
||||||
| `bookstack_db_root_password` | MariaDB root password |
|
| `bookstack_db_root_password` | MariaDB root password |
|
||||||
| `bookstack_db_password` | MariaDB user password |
|
| `bookstack_db_password` | MariaDB user password |
|
||||||
| `bookstack_admin_password` | Initial local admin password |
|
| `bookstack_admin_password` | Initial local admin password |
|
||||||
| `bookstack_oidc_client_id` | Entra ID App Registration ID (if OIDC on) |
|
| `bookstack_oidc_client_id` | OIDC client ID (if OIDC on) |
|
||||||
| `bookstack_oidc_client_secret` | Entra ID client secret (if OIDC on) |
|
| `bookstack_oidc_client_secret` | OIDC client secret (if OIDC on) |
|
||||||
| `bookstack_entra_tenant_id` | Entra tenant UUID (if OIDC on) |
|
|
||||||
|
When OIDC is on, the role also asserts that `bookstack_oidc_issuer`
|
||||||
|
resolves to a concrete URL. For Entra ID this means setting
|
||||||
|
`bookstack_entra_tenant_id` (the default issuer interpolates it; an unset
|
||||||
|
tenant leaves `//v2.0` and fails the assert). For other IdPs (Authentik,
|
||||||
|
Keycloak) set `bookstack_oidc_issuer` directly instead.
|
||||||
|
|
||||||
Provide via OpenBao lookup, Ansible Vault or `--extra-vars`. Never commit
|
Provide via OpenBao lookup, Ansible Vault or `--extra-vars`. Never commit
|
||||||
real secrets.
|
real secrets.
|
||||||
|
|
@ -34,6 +39,10 @@ real secrets.
|
||||||
See `defaults/main.yml`. Frequently overridden:
|
See `defaults/main.yml`. Frequently overridden:
|
||||||
|
|
||||||
- `bookstack_domain`, `bookstack_base_url`
|
- `bookstack_domain`, `bookstack_base_url`
|
||||||
|
- `bookstack_extra_domains` (extra Host-rule hostnames, e.g. an internal
|
||||||
|
`*.int.*` FQDN for a DMZ reverseproxy)
|
||||||
|
- `bookstack_extra_hosts` (container `/etc/hosts` overrides for
|
||||||
|
split-horizon IdP access; entries as `host:ip`)
|
||||||
- `bookstack_image`, `bookstack_db_image` (pin in production)
|
- `bookstack_image`, `bookstack_db_image` (pin in production)
|
||||||
- `bookstack_oidc_enabled` (set `false` to disable OIDC entirely)
|
- `bookstack_oidc_enabled` (set `false` to disable OIDC entirely)
|
||||||
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
|
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
|
||||||
|
|
@ -142,4 +151,4 @@ Restore procedure:
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ bookstack_backup_dir: "{{ bookstack_docker_volume_dir }}/backup"
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
bookstack_domain: "wiki.local.test"
|
bookstack_domain: "wiki.local.test"
|
||||||
|
# Additional hostnames the bookstack router answers on (e.g. an internal
|
||||||
|
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
|
||||||
|
# by the cert).
|
||||||
|
bookstack_extra_domains: []
|
||||||
|
# Container-level /etc/hosts overrides — useful in split-horizon setups
|
||||||
|
# where the BookStack container needs to reach an IdP's public FQDN
|
||||||
|
# (used in the OIDC `iss` claim) over the LAN rather than via the DMZ.
|
||||||
|
bookstack_extra_hosts: []
|
||||||
bookstack_base_url: "https://{{ bookstack_domain }}"
|
bookstack_base_url: "https://{{ bookstack_domain }}"
|
||||||
|
|
||||||
# Images — pin via inventory in production
|
# Images — pin via inventory in production
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,24 @@ argument_specs:
|
||||||
type: str
|
type: str
|
||||||
default: wiki.local.test
|
default: wiki.local.test
|
||||||
description: Hostname used in the Traefik Host rule.
|
description: Hostname used in the Traefik Host rule.
|
||||||
|
bookstack_extra_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Additional hostnames the Traefik router answers on, OR-combined
|
||||||
|
with C(bookstack_domain). Useful for an internal C(*.int.*) FQDN
|
||||||
|
so a DMZ reverseproxy can reach a backend hostname covered by the
|
||||||
|
cert.
|
||||||
|
bookstack_extra_hosts:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Container-level C(/etc/hosts) overrides (Compose C(extra_hosts)
|
||||||
|
entries, C("host:ip")). Useful in split-horizon setups where the
|
||||||
|
BookStack container must reach an IdP's public FQDN (used in the
|
||||||
|
OIDC C(iss) claim) over the LAN rather than via the DMZ.
|
||||||
bookstack_base_url:
|
bookstack_base_url:
|
||||||
type: str
|
type: str
|
||||||
description: Defaults to C("https://{{ bookstack_domain }}").
|
description: Defaults to C("https://{{ bookstack_domain }}").
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ galaxy_info:
|
||||||
author: digitalboard
|
author: digitalboard
|
||||||
description: Deploy BookStack as a self-contained Docker Compose stack behind Traefik
|
description: Deploy BookStack as a self-contained Docker Compose stack behind Traefik
|
||||||
company: digitalboard
|
company: digitalboard
|
||||||
license: MIT
|
license: MIT-0
|
||||||
|
|
||||||
min_ansible_version: "2.14"
|
min_ansible_version: "2.14"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,13 @@
|
||||||
- bookstack_admin_password | length > 0
|
- bookstack_admin_password | length > 0
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_id | length > 0)
|
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_id | length > 0)
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_secret | length > 0)
|
- (not bookstack_oidc_enabled) or (bookstack_oidc_client_secret | length > 0)
|
||||||
- (not bookstack_oidc_enabled) or (bookstack_entra_tenant_id | length > 0)
|
# Issuer URL must resolve to something concrete. The Entra default
|
||||||
|
# interpolates bookstack_entra_tenant_id; an unset tenant leaves
|
||||||
|
# "//v2.0" in the URL. Allow non-Entra IdPs (Authentik, Keycloak)
|
||||||
|
# that override bookstack_oidc_issuer directly.
|
||||||
|
- (not bookstack_oidc_enabled) or
|
||||||
|
(bookstack_oidc_issuer | length > 0 and
|
||||||
|
'//v2.0' not in bookstack_oidc_issuer)
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
One or more required secrets are unset. Provide them via OpenBao
|
One or more required secrets are unset. Provide them via OpenBao
|
||||||
lookup, Ansible Vault or --extra-vars. See README for the full list.
|
lookup, Ansible Vault or --extra-vars. See README for the full list.
|
||||||
|
|
|
||||||
|
|
@ -45,13 +45,19 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- {{ bookstack_traefik_network }}
|
- {{ bookstack_traefik_network }}
|
||||||
- internal
|
- internal
|
||||||
|
{% if bookstack_extra_hosts | length > 0 %}
|
||||||
|
extra_hosts:
|
||||||
|
{% for host in bookstack_extra_hosts %}
|
||||||
|
- "{{ host }}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
depends_on:
|
depends_on:
|
||||||
{{ bookstack_service_name }}-db:
|
{{ bookstack_service_name }}-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network={{ bookstack_traefik_network }}"
|
- "traefik.docker.network={{ bookstack_traefik_network }}"
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.rule=Host(`{{ bookstack_domain }}`)"
|
- "traefik.http.routers.{{ bookstack_service_name }}.rule={% set _all_domains = [bookstack_domain] + (bookstack_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}"
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.entrypoints=websecure"
|
- "traefik.http.routers.{{ bookstack_service_name }}.entrypoints=websecure"
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls=true"
|
- "traefik.http.routers.{{ bookstack_service_name }}.tls=true"
|
||||||
- "traefik.http.routers.{{ bookstack_service_name }}.tls.certresolver={{ bookstack_traefik_certresolver }}"
|
- "traefik.http.routers.{{ bookstack_service_name }}.tls.certresolver={{ bookstack_traefik_certresolver }}"
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,42 @@
|
||||||
Role Name
|
# collabora
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploys [Collabora Online](https://www.collaboraonline.com/) (CODE,
|
||||||
|
`collabora/code`) via Docker Compose behind Traefik. Collabora is the
|
||||||
|
WOPI backend that renders office documents for Nextcloud and OpenCloud.
|
||||||
|
|
||||||
Requirements
|
The role templates `coolwsd.xml` to declare which WOPI hosts may call
|
||||||
------------
|
Collabora and which origins may embed it in an iframe.
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
## Role variables
|
||||||
|
|
||||||
Role Variables
|
| Variable | Default | Description |
|
||||||
--------------
|
| --- | --- | --- |
|
||||||
|
| `collabora_domains` | `[office.local.test]` | FQDNs the router accepts; first is canonical. |
|
||||||
|
| `collabora_image` | `collabora/code:latest` | Container image. |
|
||||||
|
| `collabora_port` | `9980` | Container port Traefik forwards to. |
|
||||||
|
| `collabora_traefik_network` | `proxy` | Docker network shared with Traefik. |
|
||||||
|
| `collabora_use_ssl` | `true` | Enable the TLS resolver on the router. |
|
||||||
|
| `collabora_ssl_verification` | `true` | Verify TLS on WOPI callbacks (false for self-signed). |
|
||||||
|
| `collabora_allowed_domains` | `[nextcloud.local.test]` | WOPI hosts allowed to call Collabora (regex). |
|
||||||
|
| `collabora_frame_ancestors` | `[nextcloud.local.test]` | Origins allowed to embed Collabora in an iframe. |
|
||||||
|
| `collabora_extra_hosts` | `[]` | Extra `host:ip` entries for in-container DNS. |
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
## Example
|
||||||
|
|
||||||
Dependencies
|
```yaml
|
||||||
------------
|
- hosts: services
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.collabora
|
||||||
|
vars:
|
||||||
|
collabora_domains:
|
||||||
|
- "office.example.com"
|
||||||
|
collabora_allowed_domains:
|
||||||
|
- "cloud.example.com"
|
||||||
|
collabora_frame_ancestors:
|
||||||
|
- "cloud.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
## License
|
||||||
|
|
||||||
Example Playbook
|
MIT-0
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
|
||||||
|
|
||||||
- hosts: servers
|
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ collabora_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ collabora_servic
|
||||||
collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}"
|
collabora_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ collabora_service_name }}"
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
collabora_domain: "office.local.test"
|
# FQDNs the collabora router accepts. The first entry is the canonical
|
||||||
|
# domain; further entries cover internal *.int.* names used for
|
||||||
|
# server-to-server WOPI discovery.
|
||||||
|
collabora_domains:
|
||||||
|
- "office.local.test"
|
||||||
collabora_image: "collabora/code:latest"
|
collabora_image: "collabora/code:latest"
|
||||||
collabora_port: 9980
|
collabora_port: 9980
|
||||||
collabora_extra_hosts: []
|
collabora_extra_hosts: []
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart collabora
|
- name: restart collabora
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ collabora_docker_compose_dir }}"
|
project_src: "{{ collabora_docker_compose_dir }}"
|
||||||
state: restarted
|
state: present
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy Collabora Online (CODE) as a WOPI backend via Docker Compose behind Traefik
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- collabora
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- office
|
||||||
# min_ansible_container_version:
|
- wopi
|
||||||
|
- nextcloud
|
||||||
galaxy_tags: []
|
- docker
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,14 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ collabora_traefik_network }}
|
- traefik.docker.network={{ collabora_traefik_network }}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.rule=Host(`{{ collabora_domain }}`)
|
- traefik.http.routers.{{ collabora_service_name }}.rule={% for d in collabora_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
- traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }}
|
- traefik.http.services.{{ collabora_service_name }}.loadbalancer.server.port={{ collabora_port }}
|
||||||
{% if collabora_use_ssl %}
|
{% if collabora_use_ssl %}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.tls=true
|
- traefik.http.routers.{{ collabora_service_name }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ collabora_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ collabora_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,60 @@
|
||||||
Role Name
|
# Drawio
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Ansible role to deploy [draw.io](https://www.drawio.com/) (the
|
||||||
|
self-hosted `jgraph/drawio` container) via Docker Compose behind
|
||||||
|
Traefik, with optional authentik ForwardAuth gating.
|
||||||
|
|
||||||
Requirements
|
## Requirements
|
||||||
------------
|
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
- Docker and Docker Compose installed on the target host
|
||||||
|
- Ansible collection: `community.docker`
|
||||||
|
- Traefik with a shared `drawio_traefik_network` (default `proxy`)
|
||||||
|
- For ForwardAuth: a reachable authentik embedded outpost endpoint
|
||||||
|
|
||||||
Role Variables
|
## Role variables
|
||||||
--------------
|
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
||||||
|
common overrides:
|
||||||
|
|
||||||
Dependencies
|
### Service
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
- `drawio_domain`: canonical hostname used in the traefik Host rule
|
||||||
|
(default `drawio.local.test`).
|
||||||
|
- `drawio_extra_domains`: additional hostnames the same container
|
||||||
|
should answer on (e.g. an internal `*.int.*` FQDN so a DMZ proxy
|
||||||
|
can reach drawio via a backend hostname).
|
||||||
|
- `drawio_image`, `drawio_port`, `drawio_use_ssl`.
|
||||||
|
|
||||||
Example Playbook
|
### Authentik ForwardAuth
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
- `drawio_authentik_forward_auth`: set to `true` to gate the editor
|
||||||
|
behind authentik.
|
||||||
|
- `drawio_authentik_forward_auth_url`: full URL of the embedded
|
||||||
|
outpost ForwardAuth endpoint, e.g.
|
||||||
|
`https://auth.example.com/outpost.goauthentik.io/auth/traefik`.
|
||||||
|
|
||||||
- hosts: servers
|
When enabled, traefik redirects unauthenticated requests to authentik
|
||||||
roles:
|
for login and forwards the resulting `X-Authentik-*` identity headers
|
||||||
- { role: username.rolename, x: 42 }
|
downstream.
|
||||||
|
|
||||||
License
|
## Dependencies
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
- Traefik network (`drawio_traefik_network`, default `proxy`)
|
||||||
|
- Optional: authentik with a Proxy/ForwardAuth provider for drawio
|
||||||
|
(see the `authentik` role's `authentik_proxy_apps`).
|
||||||
|
|
||||||
Author Information
|
## Example playbook
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
```yaml
|
||||||
|
- hosts: app_servers
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.drawio
|
||||||
|
vars:
|
||||||
|
drawio_domain: "drawio.example.com"
|
||||||
|
drawio_authentik_forward_auth: true
|
||||||
|
drawio_authentik_forward_auth_url: "https://auth.example.com/outpost.goauthentik.io/auth/traefik"
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,21 @@ drawio_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ drawio_service_name
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
drawio_domain: "drawio.local.test"
|
drawio_domain: "drawio.local.test"
|
||||||
|
# Additional hostnames the same drawio container should answer on
|
||||||
|
# (e.g. an internal *.int.* FQDN so a DMZ reverseproxy can reach
|
||||||
|
# drawio via a backend hostname covered by the local traefik cert).
|
||||||
|
drawio_extra_domains: []
|
||||||
drawio_image: "jgraph/drawio:latest"
|
drawio_image: "jgraph/drawio:latest"
|
||||||
drawio_port: 8080
|
drawio_port: 8080
|
||||||
drawio_extra_hosts: []
|
drawio_extra_hosts: []
|
||||||
|
|
||||||
# Traefik configuration
|
# Traefik configuration
|
||||||
drawio_traefik_network: "proxy"
|
drawio_traefik_network: "proxy"
|
||||||
drawio_use_ssl: true
|
drawio_use_ssl: true
|
||||||
|
|
||||||
|
# Optional Authentik ForwardAuth (set to true and provide the URL to gate
|
||||||
|
# drawio behind an authentik proxy provider). Expects the authentik
|
||||||
|
# embedded outpost to expose the /outpost.goauthentik.io/auth/traefik
|
||||||
|
# endpoint on the configured URL (typically the public auth.* FQDN).
|
||||||
|
drawio_authentik_forward_auth: false
|
||||||
|
drawio_authentik_forward_auth_url: ""
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart drawio
|
- name: restart drawio
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ drawio_docker_compose_dir }}"
|
project_src: "{{ drawio_docker_compose_dir }}"
|
||||||
state: restarted
|
state: present
|
||||||
64
roles/drawio/meta/argument_specs.yml
Normal file
64
roles/drawio/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy draw.io diagram editor via Docker Compose behind Traefik.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for jgraph/drawio with traefik labels, optional
|
||||||
|
TLS and optional authentik ForwardAuth gating.
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
drawio_service_name:
|
||||||
|
type: str
|
||||||
|
default: drawio
|
||||||
|
drawio_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_compose_base_dir }}/{{ drawio_service_name }}).
|
||||||
|
|
||||||
|
drawio_domain:
|
||||||
|
type: str
|
||||||
|
default: drawio.local.test
|
||||||
|
description: Canonical hostname used in the traefik Host rule.
|
||||||
|
drawio_extra_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Additional hostnames the same drawio container should answer on,
|
||||||
|
e.g. an internal C(*.int.*) FQDN so a DMZ reverse-proxy can reach
|
||||||
|
drawio via a backend hostname covered by the local traefik cert.
|
||||||
|
drawio_image:
|
||||||
|
type: str
|
||||||
|
default: jgraph/drawio:latest
|
||||||
|
drawio_port:
|
||||||
|
type: int
|
||||||
|
default: 8080
|
||||||
|
drawio_extra_hosts:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
description: C(extra_hosts) entries injected into the container (Docker C(host:ip) syntax).
|
||||||
|
|
||||||
|
drawio_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
drawio_use_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
||||||
|
drawio_authentik_forward_auth:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description:
|
||||||
|
- When true, traefik attaches a ForwardAuth middleware pointing at
|
||||||
|
the authentik embedded outpost. Unauthenticated requests are
|
||||||
|
redirected to authentik for login and the resulting
|
||||||
|
C(X-Authentik-*) identity headers are forwarded downstream.
|
||||||
|
drawio_authentik_forward_auth_url:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- URL of the authentik ForwardAuth endpoint, typically
|
||||||
|
C(https://auth.example.com/outpost.goauthentik.io/auth/traefik).
|
||||||
|
Required when C(drawio_authentik_forward_auth=true).
|
||||||
|
|
@ -1,35 +1,26 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy the draw.io diagram editor via Docker Compose behind Traefik, with optional authentik ForwardAuth
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- drawio
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- diagrams
|
||||||
# min_ansible_container_version:
|
- docker
|
||||||
|
- traefik
|
||||||
galaxy_tags: []
|
- digitalboard
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,26 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ drawio_traefik_network }}
|
- traefik.docker.network={{ drawio_traefik_network }}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.rule=Host(`{{ drawio_domain }}`)
|
- traefik.http.routers.{{ drawio_service_name }}.rule={% set _all_domains = [drawio_domain] + (drawio_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
- traefik.http.services.{{ drawio_service_name }}.loadbalancer.server.port={{ drawio_port }}
|
- traefik.http.services.{{ drawio_service_name }}.loadbalancer.server.port={{ drawio_port }}
|
||||||
{% if drawio_use_ssl %}
|
{% if drawio_use_ssl %}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.tls=true
|
- traefik.http.routers.{{ drawio_service_name }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ drawio_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ drawio_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if drawio_authentik_forward_auth | default(false) %}
|
||||||
|
# ForwardAuth via the authentik embedded outpost. Unauthenticated
|
||||||
|
# requests get redirected to authentik to log in; authentik then
|
||||||
|
# sets X-Authentik-* headers traefik forwards downstream.
|
||||||
|
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.address={{ drawio_authentik_forward_auth_url }}
|
||||||
|
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.trustForwardHeader=true
|
||||||
|
- traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,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
|
||||||
|
- traefik.http.routers.{{ drawio_service_name }}.middlewares={{ drawio_service_name }}-authentik
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
{{ drawio_traefik_network }}:
|
{{ drawio_traefik_network }}:
|
||||||
|
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
# Ansible Role: ess_pro_compose
|
|
||||||
|
|
||||||
Deploys the full **Element Server Suite Pro v26.5.1** stack as a single docker
|
|
||||||
compose project, modelled 1:1 on the official `matrix-stack` Helm chart from
|
|
||||||
Element. Fronted by the existing DMZ Traefik, secrets sourced from OpenBao
|
|
||||||
(plus locally-generated cryptographic material), same conventions as the
|
|
||||||
other `digitalboard.core` roles.
|
|
||||||
|
|
||||||
> **Licensing note:** ESS Pro is distributed as a Helm/Kubernetes product.
|
|
||||||
> Running the Pro images under docker compose requires explicit vendor
|
|
||||||
> agreement, which is in place for this deployment.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
12 services, mirroring the chart:
|
|
||||||
|
|
||||||
```
|
|
||||||
┌───────────────┐
|
|
||||||
┌──────────────────────HTTP──▶│ element-web │
|
|
||||||
│ └───────────────┘
|
|
||||||
│ ┌───────────────┐
|
|
||||||
│ ┌──────────────────HTTP──▶│ element-admin │
|
|
||||||
│ │ └───────────────┘
|
|
||||||
│ │ ┌───────────────┐
|
|
||||||
│ │ ┌───────────────HTTP──▶│ mas │ ─┐
|
|
||||||
DMZ Traefik ──┤ │ │ └───────────────┘ │ ┌──────────┐
|
|
||||||
│ │ │ ┌───────────────┐ ├─▶│ postgres │
|
|
||||||
│ │ │ ┌────────────HTTP──▶│ haproxy │ │ └──────────┘
|
|
||||||
│ │ │ │ │ (Pro Image) │ │ ┌──────────┐
|
|
||||||
│ │ │ │ └───┬─────────┬─┘ │ │ redis │
|
|
||||||
│ │ │ │ │ │ │ └──────────┘
|
|
||||||
│ │ │ │ ┌─────────────────┘ │ │
|
|
||||||
│ │ │ │ ▼ ▼ │
|
|
||||||
│ │ │ │ ┌──────────────┐ ┌────────────────┴───────┐
|
|
||||||
│ │ │ │ │ synapse-main │◀──▶│ synapse-fed-reader-0..N│
|
|
||||||
│ │ │ │ │ (Python) │ │ (Rust Pro worker) │
|
|
||||||
│ │ │ │ └──────────────┘ └────────────────────────┘
|
|
||||||
│ │ │ │
|
|
||||||
│ │ │ └──HTTP(/.well-known)──▶ haproxy (same instance)
|
|
||||||
│ │ │
|
|
||||||
│ │ └─────HTTP(/sfu/get)──────▶┌──────────────────┐
|
|
||||||
│ │ │ matrix-rtc-auth │ (lk-jwt)
|
|
||||||
│ │ └──────────┬───────┘
|
|
||||||
│ └─HTTP+TCP/30001+UDP/30002───▶┌──────────▼───────┐
|
|
||||||
│ │ matrix-rtc-sfu │ (LiveKit)
|
|
||||||
│ └──────────────────┘
|
|
||||||
│
|
|
||||||
└─ HTTPS termination on Traefik, plain HTTP downstream
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hostnames
|
|
||||||
|
|
||||||
| Component | Hostname |
|
|
||||||
| --------------------- | ------------------------------------ |
|
|
||||||
| Matrix `serverName` | `digitalboard.ch` |
|
|
||||||
| Synapse (via HAProxy) | `matrix.digitalboard.ch` |
|
|
||||||
| MAS | `account.digitalboard.ch` |
|
|
||||||
| Element Web | `chat.digitalboard.ch` |
|
|
||||||
| Element Admin | `admin.digitalboard.ch` |
|
|
||||||
| Matrix RTC / Element Call | `mrtc.digitalboard.ch` |
|
|
||||||
| `.well-known/matrix/` | `digitalboard.ch` (apex) |
|
|
||||||
|
|
||||||
Naming follows Element's official docs (`account.*`, `mrtc.*`). Keycloak on
|
|
||||||
`auth.digitalboard.ch` is untouched.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. Collections on the control node:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ansible-galaxy collection install community.docker community.hashi_vault
|
|
||||||
pip install docker hvac
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Target host: Debian bookworm with Docker CE + compose plugin (the shared
|
|
||||||
digitalboard docker role handles this) and `python3-cryptography`.
|
|
||||||
|
|
||||||
3. DMZ Traefik attached to the `proxy` network with a `websecure` entrypoint
|
|
||||||
and a `letsencrypt` certresolver.
|
|
||||||
|
|
||||||
4. DNS A/AAAA records for the apex + five subdomains.
|
|
||||||
|
|
||||||
5. DMZ firewall NAT-forwards TCP/`30001` and UDP/`30002` to the host (Element
|
|
||||||
Call media ports — fixed by the chart, not the wide 50k–60k range).
|
|
||||||
|
|
||||||
6. ESS Pro registry credentials (and Authentik OIDC client secret) bootstrapped
|
|
||||||
in OpenBao at `kv/digitalboard/ess-compose` via
|
|
||||||
`examples/openbao-bootstrap.sh`.
|
|
||||||
|
|
||||||
## How secrets work
|
|
||||||
|
|
||||||
Two layers:
|
|
||||||
|
|
||||||
- **From OpenBao:** Element registry username/token and Authentik OIDC client
|
|
||||||
secret. Pulled at playbook time via `community.hashi_vault.vault_kv2_get`
|
|
||||||
lookups, same pattern as the other digitalboard.core roles.
|
|
||||||
|
|
||||||
- **Generated locally:** The 14 cryptographic secrets the chart's
|
|
||||||
`init-secrets` job normally produces (Synapse signing key, MAS RSA/ECDSA
|
|
||||||
keys, Synapse↔MAS shared secret, replication secret, Postgres passwords,
|
|
||||||
LiveKit secret, admin user password). A Python script bundled with the role
|
|
||||||
generates them on first run into `/opt/ess/secrets/` and never overwrites
|
|
||||||
existing files — runs of the playbook are idempotent. All containers mount
|
|
||||||
this directory read-only as `/secrets/ess-generated/` (matches the chart's
|
|
||||||
mount path).
|
|
||||||
|
|
||||||
The MAS RSA key is generated in DER PKCS8 format, ECDSA in PEM PKCS8, and the
|
|
||||||
Synapse signing key in Synapse's native `ed25519 <keyid> <base64>` format.
|
|
||||||
All formats verified against what the chart's `matrix-tools generate-secrets`
|
|
||||||
produces.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# site.yml
|
|
||||||
- hosts: ess_servers
|
|
||||||
become: true
|
|
||||||
roles:
|
|
||||||
- digitalboard.core.ess_pro_compose
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# inventory/group_vars/ess_servers.yml -- see examples/
|
|
||||||
ess_server_name: "digitalboard.ch"
|
|
||||||
ess_synapse_fed_reader_replicas: 5
|
|
||||||
ess_oidc_enabled: true
|
|
||||||
ess_oidc_issuer: "https://authentik.digitalboard.ch/application/o/ess/"
|
|
||||||
ess_rtc_external_ip: "203.0.113.42"
|
|
||||||
|
|
||||||
ess_registry_username: "{{ lookup('community.hashi_vault.vault_kv2_get', ...).data.data.registry_username }}"
|
|
||||||
ess_registry_token: "{{ lookup('community.hashi_vault.vault_kv2_get', ...).data.data.registry_token }}"
|
|
||||||
ess_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get', ...).data.data.oidc_client_secret }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
Run: `ansible-playbook -i inventories/digitalboard site.yml`
|
|
||||||
|
|
||||||
The role creates `@localadmin:digitalboard.ch` via `mas-cli` and prints the
|
|
||||||
location of the generated password (`/opt/ess/secrets/ADMIN_USER_PASSWORD` on
|
|
||||||
the host).
|
|
||||||
|
|
||||||
## Post-deploy verification
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All containers healthy
|
|
||||||
docker compose -f /opt/ess/compose.yml ps
|
|
||||||
|
|
||||||
# Synapse + MAS<-->Synapse wiring
|
|
||||||
curl -sS https://matrix.digitalboard.ch/_matrix/client/versions | jq .versions
|
|
||||||
curl -sS https://digitalboard.ch/.well-known/matrix/server | jq
|
|
||||||
curl -sS https://digitalboard.ch/.well-known/matrix/client | jq
|
|
||||||
|
|
||||||
# MAS sanity
|
|
||||||
docker compose -f /opt/ess/compose.yml exec mas \
|
|
||||||
mas-cli --config /conf/mas-config.yaml doctor
|
|
||||||
|
|
||||||
# HAProxy stats (internally)
|
|
||||||
docker compose -f /opt/ess/compose.yml exec haproxy \
|
|
||||||
wget -qO- http://localhost:8405/metrics | head
|
|
||||||
```
|
|
||||||
|
|
||||||
## Operations
|
|
||||||
|
|
||||||
- **Config change:** re-run the playbook. Changed templates trigger
|
|
||||||
per-component `docker compose restart` via handlers.
|
|
||||||
- **Image upgrade:** bump `ess_images.<component>` in defaults or group_vars,
|
|
||||||
re-run.
|
|
||||||
- **Scale federation-reader:** change `ess_synapse_fed_reader_replicas`, re-run
|
|
||||||
(HAProxy backend list is rendered from the same variable).
|
|
||||||
- **Logs:** `docker compose -f /opt/ess/compose.yml logs -f synapse-main`
|
|
||||||
- **Tear down:** `docker compose -f /opt/ess/compose.yml down -v`
|
|
||||||
|
|
||||||
## What's faithful to the chart, what's adapted
|
|
||||||
|
|
||||||
**Faithful to chart v26.5.1:**
|
|
||||||
- All image paths from `registry.element.io` (correct repos: `synapse-onprem`,
|
|
||||||
`synapse-pro-worker`, `matrix-authentication-service`, `element-web-pro`,
|
|
||||||
`element-admin`, `haproxy`, `livekit-server-distroless`, `lk-jwt-service`,
|
|
||||||
`postgres`, `redis-distroless`).
|
|
||||||
- HAProxy config 1:1 from the chart (path-based routing to fed-reader for
|
|
||||||
`/event`, `/state`, `/state_ids`, admin IP allow-list, well-known
|
|
||||||
serving on port 8010, 429.http for queue overflow).
|
|
||||||
- Synapse `homeserver.yaml` merged from the chart's four fragments
|
|
||||||
(underrides + overrides + main listeners + log config) with both Pro
|
|
||||||
modules loaded (`synapse_ess_pro.EssPro`,
|
|
||||||
`synapse_mass_local_room_upgrades.MassLocalRoomUpgradesModule`).
|
|
||||||
- MAS config with all four listeners (web 8080, internal 8081, root 8082,
|
|
||||||
synapse 8083) and `kind: synapse_modern` for delegated auth.
|
|
||||||
- federation-reader (Rust worker) config in its native schema, not
|
|
||||||
Synapse-Python-worker syntax.
|
|
||||||
- LiveKit on TCP 30001 + UDP 30002 muxed, with `node_ip` set for ICE.
|
|
||||||
- Element Web config with Pro features (`use_exclusively`,
|
|
||||||
`element-pro` mobile variant).
|
|
||||||
- Init-secrets bundle generated with matching key types and formats
|
|
||||||
(rand32 url-safe / hex32 / rsa:4096:der / ecdsaprime256v1 PEM /
|
|
||||||
Synapse ed25519 signing key).
|
|
||||||
|
|
||||||
**Adapted for compose:**
|
|
||||||
- K8s DNS-SRV service discovery (`_synapse-http._tcp.X.svc.cluster.local`)
|
|
||||||
replaced with direct compose service names + the embedded DNS resolver
|
|
||||||
(`127.0.0.11:53`). HAProxy backend entries use plain hostnames.
|
|
||||||
- StatefulSet PVCs replaced with named docker volumes.
|
|
||||||
- The chart's `matrix-tools render-config` init-container is replaced by
|
|
||||||
Ansible Jinja2 template rendering on the control node — same merge order,
|
|
||||||
no Python interpreter in init-containers.
|
|
||||||
- The chart's `init-secrets` K8s job is replaced by the local
|
|
||||||
generate-secrets script.
|
|
||||||
- Postgres `postgres-ess-updater` sidecar (which re-runs the init script
|
|
||||||
in case of password changes) is omitted; first-boot init via
|
|
||||||
`/docker-entrypoint-initdb.d/` is sufficient for compose, since the
|
|
||||||
generated passwords don't rotate on re-run (idempotent secrets).
|
|
||||||
- No Synapse Pro autoscaler (K8s HPA only); replica count is static via
|
|
||||||
`ess_synapse_fed_reader_replicas`.
|
|
||||||
|
|
||||||
## Things not yet wired (optional Pro components)
|
|
||||||
|
|
||||||
The chart can also deploy these — not included in this role's first pass,
|
|
||||||
add as needed:
|
|
||||||
|
|
||||||
- Hookshot (Matrix bot framework for GitHub/GitLab/JIRA bridges)
|
|
||||||
- Secure Border Gateway (Federation app-firewall — only relevant if you
|
|
||||||
federate with strict-control orgs / German TI-Messenger)
|
|
||||||
- Advanced Identity Management (LDAP/SCIM provisioning)
|
|
||||||
- AuditBot, AdminBot, supervision
|
|
||||||
- Sygnal (mobile push gateway)
|
|
||||||
- Telemetry service (chart deploys this by default; here it's optional)
|
|
||||||
- Content scanner
|
|
||||||
|
|
||||||
Each maps to its own template directory in `charts/matrix-stack/templates/`
|
|
||||||
and can be added later as additional compose services.
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# =============================================================================
|
|
||||||
# ess_pro_compose role — defaults
|
|
||||||
# =============================================================================
|
|
||||||
# Deploys the full ESS Pro stack (matrix-stack chart v26.5.1) as a docker
|
|
||||||
# compose project, including the Pro federation-reader worker. Same conventions
|
|
||||||
# as the other digitalboard.core roles. Secrets are sourced from OpenBao.
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Chart version we're modelling
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ess_chart_version: "26.5.1"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Project layout on the target host
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ess_compose_dir: "/opt/ess"
|
|
||||||
ess_compose_project_name: "ess"
|
|
||||||
|
|
||||||
# Where rendered configs and runtime data live (mounted into containers)
|
|
||||||
ess_compose_conf_dir: "{{ ess_compose_dir }}/conf" # rendered configs
|
|
||||||
ess_compose_secrets_dir: "{{ ess_compose_dir }}/secrets" # generated secrets (0600)
|
|
||||||
ess_compose_data_dir: "{{ ess_compose_dir }}/data" # volumes
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Docker networks
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Public-facing Traefik network (external, managed by the shared traefik role).
|
|
||||||
ess_compose_traefik_network: "proxy"
|
|
||||||
ess_compose_traefik_entrypoint: "websecure"
|
|
||||||
ess_compose_traefik_certresolver: "letsencrypt"
|
|
||||||
|
|
||||||
# Internal network for service-to-service traffic only.
|
|
||||||
ess_compose_internal_network: "ess_internal"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Matrix identity
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Matrix serverName is the domain part of @user:serverName. Immutable.
|
|
||||||
ess_server_name: "digitalboard.ch"
|
|
||||||
|
|
||||||
# Hostnames. Convention follows the official Element docs (account.*, mrtc.*).
|
|
||||||
# Override per environment in group_vars if you want different prefixes.
|
|
||||||
ess_hostnames:
|
|
||||||
synapse: "matrix.{{ ess_server_name }}" # client + federation, fronts HAProxy
|
|
||||||
mas: "account.{{ ess_server_name }}" # Matrix Authentication Service
|
|
||||||
element_web: "chat.{{ ess_server_name }}"
|
|
||||||
element_admin: "admin.{{ ess_server_name }}"
|
|
||||||
matrix_rtc: "mrtc.{{ ess_server_name }}" # Element Call SFU + auth
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Image references (Pro images from registry.element.io, chart 26.5.1)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Pin to specific tags for production. The chart bundles digests; we use
|
|
||||||
# version-aligned tags so they're readable. Override individually as needed.
|
|
||||||
ess_images:
|
|
||||||
synapse: "registry.element.io/synapse-onprem:sha-63110a4"
|
|
||||||
synapse_pro_worker: "registry.element.io/synapse-pro-worker:0.4.0"
|
|
||||||
mas: "registry.element.io/matrix-authentication-service:1.17.0"
|
|
||||||
element_web: "registry.element.io/element-web-pro:1.12.18"
|
|
||||||
element_admin: "registry.element.io/element-admin:1.5.0"
|
|
||||||
haproxy: "registry.element.io/haproxy:3.2-alpine"
|
|
||||||
livekit: "registry.element.io/livekit-server-distroless:1.9.1"
|
|
||||||
lk_jwt: "registry.element.io/lk-jwt-service:0.3.0"
|
|
||||||
postgres: "registry.element.io/postgres:16-alpine"
|
|
||||||
postgres_exporter: "registry.element.io/postgres-exporter:0.18.1"
|
|
||||||
redis: "registry.element.io/redis-distroless:7.4"
|
|
||||||
matrix_tools: "registry.element.io/matrix-tools:0.17.8"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Element registry credentials (from customer.element.io)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ess_registry_url: "registry.element.io"
|
|
||||||
ess_registry_username: "" # OpenBao lookup in group_vars
|
|
||||||
ess_registry_token: "" # OpenBao lookup in group_vars
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Federation reader worker
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# The Rust-based Pro worker that handles /state, /state_ids, /event federation
|
|
||||||
# reads. The chart deploys this with 20 replicas; for compose we run it as
|
|
||||||
# scaled instances.
|
|
||||||
ess_synapse_fed_reader_replicas: 1
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Delegated authentication via the digitalboard IdP
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Authentik in the demo environment, Keycloak in production. Discover the
|
|
||||||
# exact issuer with:
|
|
||||||
# curl -s <issuer>/.well-known/openid-configuration | jq .issuer
|
|
||||||
ess_oidc_enabled: false
|
|
||||||
ess_oidc_issuer: ""
|
|
||||||
ess_oidc_client_id: "ess-mas"
|
|
||||||
ess_oidc_client_secret: "" # OpenBao
|
|
||||||
ess_oidc_provider_name: "Digitalboard"
|
|
||||||
ess_oidc_provider_ulid: "01JBADAUTHENTIKDIGITALBOARD01"
|
|
||||||
ess_oidc_scopes: "openid profile email"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Matrix RTC / Element Call (LiveKit SFU)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Element's Pro chart fixes RTC to TCP 30001 + UDP 30002 (muxed). Forward
|
|
||||||
# those on the DMZ firewall to this host.
|
|
||||||
ess_rtc_tcp_port: 30001
|
|
||||||
ess_rtc_udp_port: 30002
|
|
||||||
|
|
||||||
# Public IP for ICE candidates (the DMZ NAT address). Required.
|
|
||||||
ess_rtc_external_ip: ""
|
|
||||||
# LiveKit non-secret key id (the secret comes from the generated bundle).
|
|
||||||
ess_livekit_key: "matrix-rtc"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Registration / federation policy
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ess_enable_registration: false
|
|
||||||
ess_enable_federation: true # internet federation; turn off for isolated POCs
|
|
||||||
ess_admin_contact: "mailto:admin@{{ ess_server_name }}"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Initial admin user
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# A localadmin user is created on first deploy via mas-cli. The generated
|
|
||||||
# password lands in {{ ess_compose_secrets_dir }}/ADMIN_USER_PASSWORD.
|
|
||||||
ess_admin_localpart: "localadmin"
|
|
||||||
ess_create_admin_user: true
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Element Admin / Synapse Admin allow-list
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Source IPs (CIDR) allowed to hit /_synapse/admin/. Default: everyone. Lock
|
|
||||||
# this down for production (e.g. just the office network + bastion).
|
|
||||||
ess_admin_allow_ips:
|
|
||||||
- "0.0.0.0/0"
|
|
||||||
- "::/0"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Resources / sizing (Postgres args)
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Chart defaults assume a fairly beefy node. Adjust for your VM.
|
|
||||||
ess_postgres_max_connections: 256
|
|
||||||
ess_postgres_shared_buffers: "1024MB"
|
|
||||||
ess_postgres_effective_cache_size: "3840MB"
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Synapse media store
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ess_synapse_max_upload_size: "100M"
|
|
||||||
ess_synapse_url_previews_enabled: true
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# inventory/group_vars/ess_servers.yml
|
|
||||||
# Production config: full Pro stack, secrets from OpenBao.
|
|
||||||
|
|
||||||
# ---- Matrix identity ----------------------------------------------------
|
|
||||||
ess_server_name: "digitalboard.ch"
|
|
||||||
# Default hostnames (matrix./account./chat./admin./mrtc.) inherit from
|
|
||||||
# ess_server_name. Override `ess_hostnames` here if you need different prefixes.
|
|
||||||
|
|
||||||
# ---- Pro worker scaling -------------------------------------------------
|
|
||||||
# Federation-reader workers (Rust). Chart deploys 20 in K8s with HPA.
|
|
||||||
# For a 500-700 user vocational school deployment, 3-5 is plenty.
|
|
||||||
ess_synapse_fed_reader_replicas: 5
|
|
||||||
|
|
||||||
# ---- DMZ Traefik integration --------------------------------------------
|
|
||||||
ess_compose_traefik_network: "proxy"
|
|
||||||
ess_compose_traefik_entrypoint: "websecure"
|
|
||||||
ess_compose_traefik_certresolver: "letsencrypt"
|
|
||||||
|
|
||||||
# ---- Registration / federation policy -----------------------------------
|
|
||||||
ess_enable_registration: false
|
|
||||||
ess_enable_federation: true
|
|
||||||
|
|
||||||
# ---- Delegated auth via Authentik (demo) / Keycloak (prod) --------------
|
|
||||||
ess_oidc_enabled: true
|
|
||||||
# Verify the actual issuer with:
|
|
||||||
# curl -s <issuer>/.well-known/openid-configuration | jq .issuer
|
|
||||||
ess_oidc_issuer: "https://authentik.digitalboard.ch/application/o/ess/"
|
|
||||||
ess_oidc_client_id: "ess-mas"
|
|
||||||
ess_oidc_provider_name: "Digitalboard"
|
|
||||||
|
|
||||||
# ---- Matrix RTC / Element Call ------------------------------------------
|
|
||||||
ess_rtc_external_ip: "203.0.113.42" # DMZ public IP — set for your env
|
|
||||||
|
|
||||||
# ---- Admin allow-list (lock down for prod!) -----------------------------
|
|
||||||
ess_admin_allow_ips:
|
|
||||||
- "10.0.0.0/8" # internal RFC1918
|
|
||||||
- "172.16.0.0/12"
|
|
||||||
- "192.168.0.0/16"
|
|
||||||
- "203.0.113.5/32" # bastion IP
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Secrets — from OpenBao (same pattern as bookstack/opnform/homarr)
|
|
||||||
# =============================================================================
|
|
||||||
#
|
|
||||||
# Stored at kv/digitalboard/ess-compose with two keys (registry creds only —
|
|
||||||
# the cryptographic material is generated locally by the role's
|
|
||||||
# generate-secrets script and lives in {{ ess_compose_secrets_dir }} on the
|
|
||||||
# host). The OIDC client secret also lives in OpenBao because it's shared
|
|
||||||
# with the IdP side.
|
|
||||||
|
|
||||||
ess_registry_username: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/ess-compose',
|
|
||||||
mount_point='kv').data.data.registry_username }}"
|
|
||||||
|
|
||||||
ess_registry_token: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/ess-compose',
|
|
||||||
mount_point='kv').data.data.registry_token }}"
|
|
||||||
|
|
||||||
ess_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
||||||
'digitalboard/ess-compose',
|
|
||||||
mount_point='kv').data.data.oidc_client_secret }}"
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Bootstrap the OpenBao entry for ess_pro_compose.
|
|
||||||
# Only stores the registry credentials and the OIDC client secret —
|
|
||||||
# the rest of the cryptographic material is generated by the role locally
|
|
||||||
# on first deploy (and persists in {{ ess_compose_secrets_dir }} on the host).
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
MOUNT="${MOUNT:-kv}"
|
|
||||||
PATH_="${PATH_:-digitalboard/ess-compose}"
|
|
||||||
|
|
||||||
read -p "Element registry username (from customer.element.io): " REG_USER
|
|
||||||
read -sp "Element registry token: " REG_TOKEN; echo
|
|
||||||
read -sp "Authentik OIDC client_secret for ess-mas: " OIDC_SECRET; echo
|
|
||||||
|
|
||||||
bao kv put "${MOUNT}/${PATH_}" \
|
|
||||||
registry_username="${REG_USER}" \
|
|
||||||
registry_token="${REG_TOKEN}" \
|
|
||||||
oidc_client_secret="${OIDC_SECRET}"
|
|
||||||
|
|
||||||
echo "Done. Verify: bao kv get ${MOUNT}/${PATH_}"
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- name: Deploy ESS Pro v26.5.1 (full stack with federation-reader worker)
|
|
||||||
hosts: ess_servers
|
|
||||||
become: true
|
|
||||||
roles:
|
|
||||||
- digitalboard.core.ess_pro_compose
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- name: Restart haproxy
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [haproxy]
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart synapse-main
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [synapse-main]
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart synapse-fed-reader
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart mas
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [mas]
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart matrix-rtc-sfu
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [matrix-rtc-sfu, matrix-rtc-authorisation]
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart element-web
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [element-web]
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- name: Restart redis
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
services: [redis]
|
|
||||||
state: restarted
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
galaxy_info:
|
|
||||||
role_name: ess_pro_compose
|
|
||||||
author: digitalboard
|
|
||||||
description: Full ESS Pro stack (matrix-stack v26.5.1) via docker compose, with federation-reader worker
|
|
||||||
license: MIT
|
|
||||||
min_ansible_version: "2.14"
|
|
||||||
platforms:
|
|
||||||
- name: Debian
|
|
||||||
versions:
|
|
||||||
- bookworm
|
|
||||||
|
|
||||||
dependencies: []
|
|
||||||
|
|
||||||
collections:
|
|
||||||
- community.docker
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# Render every component's configuration. Each template uses _ess_secrets
|
|
||||||
# facts (loaded in secrets.yml) for password substitution.
|
|
||||||
|
|
||||||
- name: Render HAProxy config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/haproxy/{{ item.dest }}"
|
|
||||||
mode: "0640"
|
|
||||||
loop:
|
|
||||||
- { src: haproxy/haproxy.cfg.j2, dest: haproxy.cfg }
|
|
||||||
- { src: haproxy/429.http.j2, dest: 429.http }
|
|
||||||
- { src: haproxy/path_map_file.j2, dest: path_map_file }
|
|
||||||
- { src: haproxy/path_map_file_get.j2, dest: path_map_file_get }
|
|
||||||
- { src: haproxy/admin-allow-ips.lst.j2, dest: admin-allow-ips.lst }
|
|
||||||
notify: Restart haproxy
|
|
||||||
|
|
||||||
- name: Render well-known files
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "haproxy/well-known/{{ item }}.j2"
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/haproxy/well-known/{{ item }}"
|
|
||||||
mode: "0644"
|
|
||||||
loop:
|
|
||||||
- server
|
|
||||||
- client
|
|
||||||
- support
|
|
||||||
- element.json
|
|
||||||
notify: Restart haproxy
|
|
||||||
|
|
||||||
- name: Render Synapse configs
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: "{{ item.src }}"
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/synapse/{{ item.dest }}"
|
|
||||||
mode: "0640"
|
|
||||||
loop:
|
|
||||||
- { src: synapse/homeserver.yaml.j2, dest: homeserver.yaml }
|
|
||||||
- { src: synapse/log_config.yaml.j2, dest: log_config.yaml }
|
|
||||||
- { src: synapse/federation-reader.yaml.j2, dest: federation-reader.yaml }
|
|
||||||
no_log: true
|
|
||||||
notify:
|
|
||||||
- Restart synapse-main
|
|
||||||
- Restart synapse-fed-reader
|
|
||||||
|
|
||||||
- name: Render MAS config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: mas/config.yaml.j2
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/mas/config.yaml"
|
|
||||||
mode: "0640"
|
|
||||||
no_log: true
|
|
||||||
notify: Restart mas
|
|
||||||
|
|
||||||
- name: Render SFU config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: sfu/config.yaml.j2
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/sfu/config.yaml"
|
|
||||||
mode: "0640"
|
|
||||||
no_log: true
|
|
||||||
notify: Restart matrix-rtc-sfu
|
|
||||||
|
|
||||||
- name: Render Element Web config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: element-web/config.json.j2
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/element-web/config.json"
|
|
||||||
mode: "0644"
|
|
||||||
notify: Restart element-web
|
|
||||||
|
|
||||||
- name: Render Postgres init script
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: postgres/configure-dbs.sh.j2
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/postgres/configure-dbs.sh"
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Render Redis config
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: redis/redis.conf.j2
|
|
||||||
dest: "{{ ess_compose_conf_dir }}/redis/redis.conf"
|
|
||||||
mode: "0644"
|
|
||||||
notify: Restart redis
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- name: Render compose project file
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: compose.yml.j2
|
|
||||||
dest: "{{ _ess_compose_file }}"
|
|
||||||
mode: "0640"
|
|
||||||
|
|
||||||
- name: Pull all images
|
|
||||||
community.docker.docker_compose_v2_pull:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
register: ess_pull_result
|
|
||||||
|
|
||||||
- name: Bring the stack up
|
|
||||||
community.docker.docker_compose_v2:
|
|
||||||
project_src: "{{ ess_compose_dir }}"
|
|
||||||
state: present
|
|
||||||
wait: true
|
|
||||||
wait_timeout: 300
|
|
||||||
register: ess_up_result
|
|
||||||
|
|
||||||
- name: Show running services
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: "{{ ess_up_result.services | default([]) | map(attribute='Service') | list }}"
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- name: Validate required variables
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- ess_server_name | length > 0
|
|
||||||
- ess_registry_username | length > 0
|
|
||||||
- ess_registry_token | length > 0
|
|
||||||
- ess_rtc_external_ip | length > 0
|
|
||||||
fail_msg: >-
|
|
||||||
Required variables are missing. Provide ess_server_name,
|
|
||||||
ess_registry_username, ess_registry_token (OpenBao) and
|
|
||||||
ess_rtc_external_ip in group_vars/ess_servers.yml.
|
|
||||||
quiet: true
|
|
||||||
|
|
||||||
- name: Validate OIDC variables when OIDC is enabled
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- ess_oidc_issuer | length > 0
|
|
||||||
- ess_oidc_client_secret | length > 0
|
|
||||||
fail_msg: OIDC enabled but issuer / client_secret missing.
|
|
||||||
quiet: true
|
|
||||||
when: ess_oidc_enabled | bool
|
|
||||||
|
|
||||||
- name: Prerequisites (docker, networks, dirs, registry login)
|
|
||||||
ansible.builtin.import_tasks: prereq.yml
|
|
||||||
|
|
||||||
- name: Generate / verify the ess-generated secret bundle
|
|
||||||
ansible.builtin.import_tasks: secrets.yml
|
|
||||||
|
|
||||||
- name: Render all component configuration files
|
|
||||||
ansible.builtin.import_tasks: config.yml
|
|
||||||
|
|
||||||
- name: Render compose project file and start the stack
|
|
||||||
ansible.builtin.import_tasks: deploy.yml
|
|
||||||
|
|
||||||
- name: Post-install (create admin user)
|
|
||||||
ansible.builtin.import_tasks: postinstall.yml
|
|
||||||
when: ess_create_admin_user | bool
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# Create @localadmin via mas-cli, using the ADMIN_USER_PASSWORD generated
|
|
||||||
# by secrets.yml. Idempotent: mas-cli rejects duplicates, we ignore that.
|
|
||||||
|
|
||||||
- name: Read generated admin password
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ ess_compose_secrets_dir }}/ADMIN_USER_PASSWORD"
|
|
||||||
register: _ess_admin_pw_slurp
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Check whether the admin user already exists
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: >
|
|
||||||
docker compose -f {{ _ess_compose_file }}
|
|
||||||
exec -T mas
|
|
||||||
mas-cli --config /conf/mas-config.yaml
|
|
||||||
manage list-users --filter username={{ ess_admin_localpart }}
|
|
||||||
register: _ess_admin_check
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Register admin user (mas-cli)
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: >
|
|
||||||
docker compose -f {{ _ess_compose_file }}
|
|
||||||
exec -T mas
|
|
||||||
mas-cli --config /conf/mas-config.yaml
|
|
||||||
manage register-user --yes
|
|
||||||
--password {{ (_ess_admin_pw_slurp.content | b64decode).strip() | quote }}
|
|
||||||
--admin
|
|
||||||
{{ ess_admin_localpart }}
|
|
||||||
register: _ess_admin_create
|
|
||||||
changed_when: "'created' in (_ess_admin_create.stdout + _ess_admin_create.stderr) | lower"
|
|
||||||
failed_when:
|
|
||||||
- _ess_admin_create.rc != 0
|
|
||||||
- "'already exists' not in (_ess_admin_create.stdout + _ess_admin_create.stderr) | lower"
|
|
||||||
no_log: true
|
|
||||||
when: ess_admin_localpart not in _ess_admin_check.stdout
|
|
||||||
|
|
||||||
- name: Login hint
|
|
||||||
ansible.builtin.debug:
|
|
||||||
msg: |
|
|
||||||
Stack is up.
|
|
||||||
Admin user: @{{ ess_admin_localpart }}:{{ ess_server_name }}
|
|
||||||
Password is in {{ ess_compose_secrets_dir }}/ADMIN_USER_PASSWORD on this host.
|
|
||||||
Element Web: https://{{ ess_hostnames.element_web }}
|
|
||||||
Element Admin: https://{{ ess_hostnames.element_admin }}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
- name: Ensure prerequisite packages on the control target
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- ca-certificates
|
|
||||||
- python3-docker
|
|
||||||
- python3-cryptography
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Verify docker compose plugin is available
|
|
||||||
ansible.builtin.command: docker compose version
|
|
||||||
register: ess_compose_check
|
|
||||||
changed_when: false
|
|
||||||
failed_when: ess_compose_check.rc != 0
|
|
||||||
|
|
||||||
- name: Create project directory tree
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0750"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
loop: "{{ _ess_dirs }}"
|
|
||||||
|
|
||||||
- name: Tighten secrets directory permissions
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ ess_compose_secrets_dir }}"
|
|
||||||
state: directory
|
|
||||||
mode: "0700"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
|
|
||||||
- name: Ensure the external Traefik proxy network exists
|
|
||||||
community.docker.docker_network:
|
|
||||||
name: "{{ ess_compose_traefik_network }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Authenticate against the Element container registry
|
|
||||||
community.docker.docker_login:
|
|
||||||
registry_url: "{{ ess_registry_url }}"
|
|
||||||
username: "{{ ess_registry_username }}"
|
|
||||||
password: "{{ ess_registry_token }}"
|
|
||||||
no_log: true
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# Generate the ess-generated secret bundle. Mirrors the chart's `init-secrets`
|
|
||||||
# job, but runs locally on the host. Idempotent — only writes missing files.
|
|
||||||
|
|
||||||
- name: Render generate-secrets script
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: generate-secrets.py.j2
|
|
||||||
dest: "{{ ess_compose_dir }}/.generate-secrets.py"
|
|
||||||
mode: "0700"
|
|
||||||
|
|
||||||
- name: Run generate-secrets (creates only what's missing)
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: "/usr/bin/python3 {{ ess_compose_dir }}/.generate-secrets.py"
|
|
||||||
register: ess_secrets_run
|
|
||||||
changed_when: "'CREATED:' in ess_secrets_run.stdout"
|
|
||||||
|
|
||||||
- name: Verify every required secret exists
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ ess_compose_secrets_dir }}/{{ item }}"
|
|
||||||
register: ess_secret_stat
|
|
||||||
loop: "{{ _ess_secret_names }}"
|
|
||||||
failed_when: not ess_secret_stat.stat.exists
|
|
||||||
|
|
||||||
- name: Read postgres passwords for config templates (not persisted)
|
|
||||||
ansible.builtin.slurp:
|
|
||||||
src: "{{ ess_compose_secrets_dir }}/{{ item }}"
|
|
||||||
register: ess_password_slurp
|
|
||||||
loop:
|
|
||||||
- POSTGRES_ADMIN_PASSWORD
|
|
||||||
- POSTGRES_SYNAPSE_PASSWORD
|
|
||||||
- POSTGRES_MATRIX_AUTHENTICATION_SERVICE_PASSWORD
|
|
||||||
- SYNAPSE_MACAROON
|
|
||||||
- SYNAPSE_REGISTRATION_SHARED_SECRET
|
|
||||||
- SYNAPSE_WORKERS_REPLICATION_SECRET
|
|
||||||
- MAS_SYNAPSE_SHARED_SECRET
|
|
||||||
- MAS_MATRIX_TOOLS_OIDC_CLIENT_SECRET
|
|
||||||
- ELEMENT_CALL_LIVEKIT_SECRET
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Expose passwords as facts for templates
|
|
||||||
ansible.builtin.set_fact:
|
|
||||||
_ess_secrets: "{{ _ess_secrets | default({}) | combine({item.item: (item.content | b64decode).strip()}) }}"
|
|
||||||
loop: "{{ ess_password_slurp.results }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.item }}"
|
|
||||||
no_log: true
|
|
||||||
|
|
@ -1,304 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
# ESS Pro v{{ ess_chart_version }} on docker compose — rendered by ess_pro_compose.
|
|
||||||
# Topology mirrors the Helm chart: HAProxy fronts all Synapse traffic,
|
|
||||||
# synapse-main is the Python homeserver, synapse-fed-reader is the Rust Pro
|
|
||||||
# worker handling federation reads, MAS handles all auth, LiveKit + lk-jwt
|
|
||||||
# serve Element Call.
|
|
||||||
|
|
||||||
name: {{ ess_compose_project_name }}
|
|
||||||
|
|
||||||
networks:
|
|
||||||
{{ ess_compose_traefik_network }}:
|
|
||||||
external: true
|
|
||||||
{{ ess_compose_internal_network }}:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
synapse_media:
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# Data plane
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: {{ ess_images.postgres }}
|
|
||||||
container_name: postgres
|
|
||||||
restart: unless-stopped
|
|
||||||
networks: [ {{ ess_compose_internal_network }} ]
|
|
||||||
environment:
|
|
||||||
LC_COLLATE: "C"
|
|
||||||
LC_CTYPE: "C"
|
|
||||||
PGDATA: /var/lib/postgresql/data/pgdata
|
|
||||||
POSTGRES_INITDB_ARGS: "-E UTF8"
|
|
||||||
POSTGRES_PASSWORD_FILE: /secrets/ess-generated/POSTGRES_ADMIN_PASSWORD
|
|
||||||
command:
|
|
||||||
- postgres
|
|
||||||
- "-c"
|
|
||||||
- "max_connections={{ ess_postgres_max_connections }}"
|
|
||||||
- "-c"
|
|
||||||
- "shared_buffers={{ ess_postgres_shared_buffers }}"
|
|
||||||
- "-c"
|
|
||||||
- "effective_cache_size={{ ess_postgres_effective_cache_size }}"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/postgres/configure-dbs.sh:/docker-entrypoint-initdb.d/init-ess-dbs.sh:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: {{ ess_images.redis }}
|
|
||||||
container_name: redis
|
|
||||||
restart: unless-stopped
|
|
||||||
networks: [ {{ ess_compose_internal_network }} ]
|
|
||||||
command: ["/usr/local/etc/redis/redis.conf"]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# Synapse (Python main + Rust federation-reader worker)
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
synapse-main:
|
|
||||||
image: {{ ess_images.synapse }}
|
|
||||||
container_name: synapse-main
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
postgres: { condition: service_healthy }
|
|
||||||
redis: { condition: service_healthy }
|
|
||||||
networks: [ {{ ess_compose_internal_network }} ]
|
|
||||||
command: ["python3", "-m", "synapse.app.homeserver", "-c", "/conf/homeserver.yaml"]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/synapse/homeserver.yaml:/conf/homeserver.yaml:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/synapse/log_config.yaml:/conf/log_config.yaml:ro
|
|
||||||
- {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro
|
|
||||||
- synapse_media:/media
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 60s
|
|
||||||
|
|
||||||
{% for i in range(ess_synapse_fed_reader_replicas | int) %}
|
|
||||||
synapse-fed-reader-{{ i }}:
|
|
||||||
image: {{ ess_images.synapse_pro_worker }}
|
|
||||||
container_name: synapse-fed-reader-{{ i }}
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
synapse-main: { condition: service_healthy }
|
|
||||||
networks: [ {{ ess_compose_internal_network }} ]
|
|
||||||
environment:
|
|
||||||
APP_CONFIG_FILEPATH: /conf/federation-reader.yaml
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/synapse/federation-reader.yaml:/conf/federation-reader.yaml:ro
|
|
||||||
- {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
# ===========================================================================
|
|
||||||
# Matrix Authentication Service (4 listeners)
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
mas:
|
|
||||||
image: {{ ess_images.mas }}
|
|
||||||
container_name: mas
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
postgres: { condition: service_healthy }
|
|
||||||
networks:
|
|
||||||
- {{ ess_compose_internal_network }}
|
|
||||||
- {{ ess_compose_traefik_network }}
|
|
||||||
environment:
|
|
||||||
MAS_CONFIG: /conf/mas-config.yaml
|
|
||||||
command: ["server", "--no-migrate"]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/mas/config.yaml:/conf/mas-config.yaml:ro
|
|
||||||
- {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-fsS", "http://localhost:8081/health"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
start_period: 30s
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-mas.rule=Host(`{{ ess_hostnames.mas }}`)"
|
|
||||||
- "traefik.http.routers.ess-mas.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-mas.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-mas.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.services.ess-mas.loadbalancer.server.port=8080"
|
|
||||||
|
|
||||||
# MAS root listener (port 8082) is mounted as a separate Traefik router so
|
|
||||||
# /.well-known/openid-configuration on the apex of the mas host is reachable.
|
|
||||||
# We attach a second router on the same service via a path rule.
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# HAProxy — fronts all Synapse + well-known traffic
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
haproxy:
|
|
||||||
image: {{ ess_images.haproxy }}
|
|
||||||
container_name: haproxy
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
synapse-main: { condition: service_healthy }
|
|
||||||
networks:
|
|
||||||
- {{ ess_compose_internal_network }}
|
|
||||||
- {{ ess_compose_traefik_network }}
|
|
||||||
command: ["-f", "/usr/local/etc/haproxy/haproxy.cfg", "-dW"]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/path_map_file:/synapse/path_map_file:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/path_map_file_get:/synapse/path_map_file_get:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/429.http:/synapse/429.http:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/admin-allow-ips.lst:/synapse/admin-allow-ips.lst:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/well-known/server:/well-known/server:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/well-known/client:/well-known/client:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/well-known/support:/well-known/support:ro
|
|
||||||
- {{ ess_compose_conf_dir }}/haproxy/well-known/element.json:/well-known/element.json:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8406/synapse_ready"]
|
|
||||||
interval: 15s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
start_period: 90s
|
|
||||||
labels:
|
|
||||||
# matrix.<server> -> HAProxy frontend synapse-http-in (port 8008)
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-synapse.rule=Host(`{{ ess_hostnames.synapse }}`)"
|
|
||||||
- "traefik.http.routers.ess-synapse.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-synapse.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-synapse.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.routers.ess-synapse.service=ess-synapse"
|
|
||||||
- "traefik.http.services.ess-synapse.loadbalancer.server.port=8008"
|
|
||||||
# <server>/.well-known/matrix -> HAProxy well-known-in (port 8010)
|
|
||||||
- "traefik.http.routers.ess-wellknown.rule=Host(`{{ ess_server_name }}`) && PathPrefix(`/.well-known/matrix`)"
|
|
||||||
- "traefik.http.routers.ess-wellknown.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-wellknown.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-wellknown.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.routers.ess-wellknown.service=ess-wellknown"
|
|
||||||
- "traefik.http.services.ess-wellknown.loadbalancer.server.port=8010"
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# Element Web (browser client)
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
element-web:
|
|
||||||
image: {{ ess_images.element_web }}
|
|
||||||
container_name: element-web
|
|
||||||
restart: unless-stopped
|
|
||||||
networks: [ {{ ess_compose_traefik_network }} ]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/element-web/config.json:/app/config.json:ro
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-element-web.rule=Host(`{{ ess_hostnames.element_web }}`)"
|
|
||||||
- "traefik.http.routers.ess-element-web.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-element-web.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-element-web.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.services.ess-element-web.loadbalancer.server.port=8080"
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# Element Admin (admin panel)
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
element-admin:
|
|
||||||
image: {{ ess_images.element_admin }}
|
|
||||||
container_name: element-admin
|
|
||||||
restart: unless-stopped
|
|
||||||
networks: [ {{ ess_compose_traefik_network }} ]
|
|
||||||
environment:
|
|
||||||
SERVER_NAME: "{{ ess_server_name }}"
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-element-admin.rule=Host(`{{ ess_hostnames.element_admin }}`)"
|
|
||||||
- "traefik.http.routers.ess-element-admin.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-element-admin.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-element-admin.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.services.ess-element-admin.loadbalancer.server.port=8080"
|
|
||||||
|
|
||||||
# ===========================================================================
|
|
||||||
# Matrix RTC / Element Call (LiveKit SFU + lk-jwt)
|
|
||||||
# ===========================================================================
|
|
||||||
|
|
||||||
matrix-rtc-sfu:
|
|
||||||
image: {{ ess_images.livekit }}
|
|
||||||
container_name: matrix-rtc-sfu
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- {{ ess_compose_internal_network }}
|
|
||||||
- {{ ess_compose_traefik_network }}
|
|
||||||
command: ["--config", "/conf/sfu-config.yaml"]
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_conf_dir }}/sfu/config.yaml:/conf/sfu-config.yaml:ro
|
|
||||||
# WebRTC media ports — DMZ firewall must NAT-forward these to this host.
|
|
||||||
ports:
|
|
||||||
- "{{ ess_rtc_tcp_port }}:{{ ess_rtc_tcp_port }}/tcp"
|
|
||||||
- "{{ ess_rtc_udp_port }}:{{ ess_rtc_udp_port }}/udp"
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc.rule=Host(`{{ ess_hostnames.matrix_rtc }}`)"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc.tls=true"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc.service=ess-matrix-rtc"
|
|
||||||
- "traefik.http.services.ess-matrix-rtc.loadbalancer.server.port=7880"
|
|
||||||
|
|
||||||
matrix-rtc-authorisation:
|
|
||||||
image: {{ ess_images.lk_jwt }}
|
|
||||||
container_name: matrix-rtc-authorisation
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
matrix-rtc-sfu: { condition: service_started }
|
|
||||||
networks:
|
|
||||||
- {{ ess_compose_internal_network }}
|
|
||||||
- {{ ess_compose_traefik_network }}
|
|
||||||
environment:
|
|
||||||
LIVEKIT_URL: "wss://{{ ess_hostnames.matrix_rtc }}"
|
|
||||||
LIVEKIT_KEY: "{{ ess_livekit_key }}"
|
|
||||||
LIVEKIT_SECRET_FROM_FILE: /secrets/ess-generated/ELEMENT_CALL_LIVEKIT_SECRET
|
|
||||||
LIVEKIT_FULL_ACCESS_HOMESERVERS: "{{ ess_server_name }}"
|
|
||||||
volumes:
|
|
||||||
- {{ ess_compose_secrets_dir }}:/secrets/ess-generated:ro
|
|
||||||
labels:
|
|
||||||
# /sfu/get is the JWT token endpoint Element Call hits to join calls.
|
|
||||||
# It lives on the same host as the SFU but on a different backend.
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network={{ ess_compose_traefik_network }}"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.rule=Host(`{{ ess_hostnames.matrix_rtc }}`) && PathPrefix(`/sfu/get`)"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.entrypoints={{ ess_compose_traefik_entrypoint }}"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.tls=true"
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.priority=200"
|
|
||||||
{% if ess_compose_traefik_certresolver | length > 0 %}
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.tls.certresolver={{ ess_compose_traefik_certresolver }}"
|
|
||||||
{% endif %}
|
|
||||||
- "traefik.http.routers.ess-matrix-rtc-auth.service=ess-matrix-rtc-auth"
|
|
||||||
- "traefik.http.services.ess-matrix-rtc-auth.loadbalancer.server.port=8080"
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"bug_report_endpoint_url": "local",
|
|
||||||
"default_server_config": {
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://{{ ess_hostnames.synapse }}",
|
|
||||||
"server_name": "{{ ess_server_name }}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"element_call": {
|
|
||||||
"use_exclusively": true
|
|
||||||
},
|
|
||||||
"embedded_pages": {
|
|
||||||
"login_for_welcome": true
|
|
||||||
},
|
|
||||||
"features": {
|
|
||||||
"feature_element_call_video_rooms": true,
|
|
||||||
"feature_group_calls": true,
|
|
||||||
"feature_new_room_decoration_ui": true,
|
|
||||||
"feature_video_rooms": true
|
|
||||||
},
|
|
||||||
"mobile_guide_app_variant": "element-pro",
|
|
||||||
"setting_defaults": {
|
|
||||||
"UIFeature.deactivate": false,
|
|
||||||
"UIFeature.passwordReset": false,
|
|
||||||
"UIFeature.registration": {{ ess_enable_registration | bool | lower }},
|
|
||||||
"feature_group_calls": true,
|
|
||||||
"urlPreviewsEnabled": {{ ess_synapse_url_previews_enabled | bool | lower }},
|
|
||||||
"urlPreviewsEnabled_e2ee": {{ ess_synapse_url_previews_enabled | bool | lower }}
|
|
||||||
},
|
|
||||||
"sso_redirect_options": {
|
|
||||||
"immediate": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# {{ ansible_managed }}
|
|
||||||
"""
|
|
||||||
Generate the ess-generated secret bundle the way the Helm chart's
|
|
||||||
init-secrets job does. Idempotent: only writes files that don't exist.
|
|
||||||
|
|
||||||
Mirrors `matrix-tools generate-secrets` arguments from chart v{{ ess_chart_version }}.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa
|
|
||||||
|
|
||||||
SECRETS_DIR = Path("{{ ess_compose_secrets_dir }}")
|
|
||||||
SECRETS_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def write_if_missing(name, content_bytes):
|
|
||||||
p = SECRETS_DIR / name
|
|
||||||
if p.exists():
|
|
||||||
return False
|
|
||||||
# Atomic-ish write
|
|
||||||
tmp = p.with_suffix(p.suffix + ".tmp")
|
|
||||||
tmp.write_bytes(content_bytes)
|
|
||||||
os.chmod(tmp, 0o600)
|
|
||||||
tmp.rename(p)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def rand32():
|
|
||||||
# `matrix-tools rand32` produces 32 url-safe characters
|
|
||||||
return secrets.token_urlsafe(24)[:32].encode()
|
|
||||||
|
|
||||||
|
|
||||||
def hex32():
|
|
||||||
return secrets.token_hex(32).encode()
|
|
||||||
|
|
||||||
|
|
||||||
def rsa_der():
|
|
||||||
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
|
|
||||||
return key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.DER,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ecdsa_prime256v1():
|
|
||||||
key = ec.generate_private_key(ec.SECP256R1())
|
|
||||||
return key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def synapse_signing_key():
|
|
||||||
# Synapse expects: ed25519 <keyid> <unpadded-base64-seed>
|
|
||||||
import base64
|
|
||||||
key = ed25519.Ed25519PrivateKey.generate()
|
|
||||||
seed = key.private_bytes(
|
|
||||||
encoding=serialization.Encoding.Raw,
|
|
||||||
format=serialization.PrivateFormat.Raw,
|
|
||||||
encryption_algorithm=serialization.NoEncryption(),
|
|
||||||
)
|
|
||||||
# 4-char keyid like Synapse generates
|
|
||||||
keyid = secrets.token_hex(2)
|
|
||||||
b64 = base64.b64encode(seed).rstrip(b"=").decode()
|
|
||||||
return f"ed25519 a_{keyid} {b64}\n".encode()
|
|
||||||
|
|
||||||
|
|
||||||
SPEC = {
|
|
||||||
"POSTGRES_ADMIN_PASSWORD": rand32,
|
|
||||||
"POSTGRES_SYNAPSE_PASSWORD": rand32,
|
|
||||||
"POSTGRES_MATRIX_AUTHENTICATION_SERVICE_PASSWORD": rand32,
|
|
||||||
"SYNAPSE_MACAROON": rand32,
|
|
||||||
"SYNAPSE_REGISTRATION_SHARED_SECRET": rand32,
|
|
||||||
"SYNAPSE_WORKERS_REPLICATION_SECRET": rand32,
|
|
||||||
"SYNAPSE_SIGNING_KEY": synapse_signing_key,
|
|
||||||
"MAS_SYNAPSE_SHARED_SECRET": rand32,
|
|
||||||
"MAS_MATRIX_TOOLS_OIDC_CLIENT_SECRET": rand32,
|
|
||||||
"MAS_ENCRYPTION_SECRET": hex32,
|
|
||||||
"MAS_RSA_PRIVATE_KEY": rsa_der,
|
|
||||||
"MAS_ECDSA_PRIME256V1_PRIVATE_KEY": ecdsa_prime256v1,
|
|
||||||
"ELEMENT_CALL_LIVEKIT_SECRET": rand32,
|
|
||||||
"ADMIN_USER_PASSWORD": rand32,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
created = []
|
|
||||||
for name, fn in SPEC.items():
|
|
||||||
if write_if_missing(name, fn()):
|
|
||||||
created.append(name)
|
|
||||||
|
|
||||||
if created:
|
|
||||||
print("CREATED:", " ".join(created))
|
|
||||||
else:
|
|
||||||
print("NOCHANGE")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
HTTP/1.0 429 Too Many Requests
|
|
||||||
Cache-Control: no-cache
|
|
||||||
Connection: close
|
|
||||||
Content-Type: application/json
|
|
||||||
access-control-allow-origin: *
|
|
||||||
access-control-allow-methods: GET, POST, PUT, DELETE, OPTIONS
|
|
||||||
access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept, Authorization
|
|
||||||
|
|
||||||
{"errcode":"M_UNKNOWN","error":"Server is unavailable"}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
{% for cidr in ess_admin_allow_ips %}
|
|
||||||
{{ cidr }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
# Adapted from ess-helm chart {{ ess_chart_version }} (ess-haproxy ConfigMap).
|
|
||||||
# K8s DNS-SRV-based service discovery replaced with direct compose hostnames.
|
|
||||||
|
|
||||||
global
|
|
||||||
maxconn 20000
|
|
||||||
log stdout format raw local0 info
|
|
||||||
tune.maxrewrite 4096
|
|
||||||
stats socket ipv4@127.0.0.1:1999 level admin
|
|
||||||
dns-accept-family ipv4
|
|
||||||
|
|
||||||
defaults
|
|
||||||
mode http
|
|
||||||
fullconn 10000
|
|
||||||
maxconn 10000
|
|
||||||
log global
|
|
||||||
option forwardfor if-none
|
|
||||||
option forwarded
|
|
||||||
timeout connect 5s
|
|
||||||
timeout queue 60s
|
|
||||||
timeout client 900s
|
|
||||||
timeout http-keep-alive 900s
|
|
||||||
timeout http-request 10s
|
|
||||||
timeout server 180s
|
|
||||||
http-reuse aggressive
|
|
||||||
default-server maxconn 500
|
|
||||||
option redispatch
|
|
||||||
compression algo gzip
|
|
||||||
compression type text/plain text/html text/xml application/json text/css
|
|
||||||
hash-type consistent sdbm
|
|
||||||
|
|
||||||
# Compose resolves service names via the embedded DNS (127.0.0.11). We point
|
|
||||||
# HAProxy at it so backend health-checks pick up restarts properly.
|
|
||||||
resolvers compose-dns
|
|
||||||
nameserver dns1 127.0.0.11:53
|
|
||||||
accepted_payload_size 8192
|
|
||||||
hold timeout 600s
|
|
||||||
hold refused 600s
|
|
||||||
|
|
||||||
frontend prometheus
|
|
||||||
bind *:8405
|
|
||||||
http-request use-service prometheus-exporter if { path /metrics }
|
|
||||||
monitor-uri /haproxy_test
|
|
||||||
no log
|
|
||||||
|
|
||||||
frontend http-blackhole
|
|
||||||
bind *:8009
|
|
||||||
http-request deny content-type application/json string '{"errcode": "M_FORBIDDEN", "error": "Blocked"}'
|
|
||||||
|
|
||||||
frontend startup
|
|
||||||
bind *:8406
|
|
||||||
acl synapse_dead nbsrv(synapse-main) lt 1
|
|
||||||
monitor-uri /synapse_ready
|
|
||||||
monitor fail if synapse_dead
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Synapse traffic — main entrypoint that the DMZ Traefik points at for matrix.*
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
frontend synapse-http-in
|
|
||||||
bind *:8008
|
|
||||||
errorfile 503 /synapse/429.http
|
|
||||||
http-request capture hdr(host) len 32
|
|
||||||
http-request capture req.fhdr(x-forwarded-for) len 64
|
|
||||||
http-request capture req.fhdr(user-agent) len 200
|
|
||||||
|
|
||||||
http-request set-header X-Forwarded-Proto https if !{ hdr(X-Forwarded-Proto) -m found }
|
|
||||||
http-request set-var(txn.x_forwarded_proto) hdr(x-forwarded-proto)
|
|
||||||
http-response add-header Strict-Transport-Security max-age=31536000 if { var(txn.x_forwarded_proto) -m str -i "https" }
|
|
||||||
|
|
||||||
# Access token extraction (used by upstream rate-limit decisions)
|
|
||||||
http-request set-var(req.access_token) urlp("access_token") if { urlp("access_token") -m found }
|
|
||||||
http-request set-var(req.access_token) req.fhdr(Authorization),word(2," ") if { hdr_beg("Authorization") -i "Bearer " }
|
|
||||||
http-request set-header X-Access-Token %[var(req.access_token)]
|
|
||||||
|
|
||||||
http-response set-header Permissions-Policy "interest-cohort=()"
|
|
||||||
|
|
||||||
# Admin endpoint IP allow-list
|
|
||||||
acl is_admin path_reg ^/_synapse/admin/.*
|
|
||||||
http-request set-var(txn.user_ip) req.fhdr(x-forwarded-for) if { hdr(x-forwarded-for) -m found }
|
|
||||||
http-request set-var(txn.user_ip) src if !{ hdr(x-forwarded-for) -m found }
|
|
||||||
acl allow_ip_admin var(txn.user_ip) -m ip -f /synapse/admin-allow-ips.lst
|
|
||||||
http-request deny if !allow_ip_admin is_admin
|
|
||||||
|
|
||||||
# FOSS-worker path maps (empty by default; reserved for advanced worker splits)
|
|
||||||
acl has_get_map path -m reg -M -f /synapse/path_map_file_get
|
|
||||||
http-request set-var(req.backend) path,map_reg(/synapse/path_map_file_get,main) if has_get_map METH_GET
|
|
||||||
http-request set-var(req.backend) path,map_reg(/synapse/path_map_file,main) unless { var(req.backend) -m found }
|
|
||||||
|
|
||||||
# Pro federation-reader worker: takes /event, /state, /state_ids reads
|
|
||||||
acl has_available_pro_fed nbsrv('synapse-pro-federation-api-requests') ge 1
|
|
||||||
http-request set-var(req.backend) str('pro-federation-api-requests') if has_available_pro_fed { path -m reg ^/_matrix/federation/v1/event/ }
|
|
||||||
http-request set-var(req.backend) str('pro-federation-api-requests') if has_available_pro_fed { path -m reg ^/_matrix/federation/v1/state/ }
|
|
||||||
http-request set-var(req.backend) str('pro-federation-api-requests') if has_available_pro_fed { path -m reg ^/_matrix/federation/v1/state_ids/ }
|
|
||||||
|
|
||||||
# CORS preflight short-circuits
|
|
||||||
acl rendezvous path_beg /_matrix/client/unstable/org.matrix.msc4108/rendezvous
|
|
||||||
acl rendezvous path_beg /_synapse/client/rendezvous
|
|
||||||
use_backend return_204_rendezvous if { method OPTIONS } rendezvous
|
|
||||||
use_backend return_204_synapse if { method OPTIONS }
|
|
||||||
|
|
||||||
# Failover from pro-fed-reader to main if the worker is unavailable
|
|
||||||
acl has_failover var(req.backend) -m str "pro-federation-api-requests"
|
|
||||||
acl backend_unavailable str(),concat('synapse-',req.backend),nbsrv lt 1
|
|
||||||
use_backend synapse-main-failover if has_failover backend_unavailable
|
|
||||||
|
|
||||||
use_backend synapse-%[var(req.backend)]
|
|
||||||
|
|
||||||
backend synapse-main
|
|
||||||
default-server maxconn 250
|
|
||||||
option httpchk
|
|
||||||
http-check connect port 8080
|
|
||||||
http-check send meth GET uri /health
|
|
||||||
server main synapse-main:8008 check port 8080 resolvers compose-dns
|
|
||||||
|
|
||||||
backend synapse-main-failover
|
|
||||||
default-server maxconn 250
|
|
||||||
option httpchk
|
|
||||||
http-check connect port 8080
|
|
||||||
http-check send meth GET uri /health
|
|
||||||
server main synapse-main:8008 check port 8080 resolvers compose-dns
|
|
||||||
|
|
||||||
backend synapse-pro-federation-api-requests
|
|
||||||
option httpchk
|
|
||||||
http-check connect port 8008
|
|
||||||
http-check send meth GET uri /health/alive
|
|
||||||
balance uri whole
|
|
||||||
# The federation-reader worker is a Rust service speaking h2c.
|
|
||||||
{% for i in range(ess_synapse_fed_reader_replicas | int) %}
|
|
||||||
server fed-reader-{{ i }} synapse-fed-reader-{{ i }}:8008 check resolvers compose-dns proto h2
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
backend return_204_synapse
|
|
||||||
http-request return status 204 hdr "Access-Control-Allow-Origin" "*" hdr "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, DELETE, OPTIONS" hdr "Access-Control-Allow-Headers" "Origin, X-Requested-With, Content-Type, Accept, Authorization, Date" hdr "Access-Control-Expose-Headers" "Synapse-Trace-Id, Server"
|
|
||||||
|
|
||||||
backend return_204_rendezvous
|
|
||||||
http-request return status 204 hdr "Access-Control-Allow-Origin" "*" hdr "Access-Control-Allow-Methods" "GET, HEAD, POST, PUT, DELETE, OPTIONS" hdr "Access-Control-Allow-Headers" "Origin, Content-Type, Accept, Content-Type, If-Match, If-None-Match" hdr "Access-Control-Expose-Headers" "Synapse-Trace-Id, Server, ETag"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Well-known — served at the apex domain via the same HAProxy.
|
|
||||||
# DMZ Traefik routes Host=`{{ ess_server_name }}` && PathPrefix(/.well-known) here.
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
frontend well-known-in
|
|
||||||
bind *:8010
|
|
||||||
acl is_delete_put_post_method method DELETE POST PUT
|
|
||||||
http-request deny status 405 if is_delete_put_post_method
|
|
||||||
|
|
||||||
acl well-known path /.well-known/matrix/server
|
|
||||||
acl well-known path /.well-known/matrix/client
|
|
||||||
acl well-known path /.well-known/matrix/support
|
|
||||||
acl well-known path /.well-known/element/element.json
|
|
||||||
http-request redirect code 301 location https://{{ ess_hostnames.element_web }} unless well-known
|
|
||||||
|
|
||||||
use_backend well-known-static if well-known
|
|
||||||
default_backend well-known-no-match
|
|
||||||
|
|
||||||
backend well-known-static
|
|
||||||
mode http
|
|
||||||
http-after-response set-header X-Frame-Options SAMEORIGIN
|
|
||||||
http-after-response set-header X-Content-Type-Options nosniff
|
|
||||||
http-after-response set-header X-XSS-Protection "1; mode=block"
|
|
||||||
http-after-response set-header Content-Security-Policy "frame-ancestors 'self'"
|
|
||||||
http-after-response set-header X-Robots-Tag "noindex, nofollow, noarchive, noimageindex"
|
|
||||||
http-after-response set-header Access-Control-Allow-Origin *
|
|
||||||
http-after-response set-header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
|
||||||
http-after-response set-header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization"
|
|
||||||
|
|
||||||
http-request return status 200 content-type "application/json" file "/well-known/server" if { path /.well-known/matrix/server }
|
|
||||||
http-request return status 200 content-type "application/json" file "/well-known/client" if { path /.well-known/matrix/client }
|
|
||||||
http-request return status 200 content-type "application/json" file "/well-known/support" if { path /.well-known/matrix/support }
|
|
||||||
http-request return status 200 content-type "application/json" file "/well-known/element.json" if { path /.well-known/element/element.json }
|
|
||||||
|
|
||||||
backend well-known-no-match
|
|
||||||
mode http
|
|
||||||
http-request deny status 404
|
|
||||||
|
|
||||||
backend return_500
|
|
||||||
http-request deny deny_status 500
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
# Map matrix paths to worker backends. Format: path_regexp backend_name
|
|
||||||
# Chart default: empty (no FOSS-worker splits). Reserved for advanced
|
|
||||||
# worker topologies; the Pro federation-reader routing is hard-coded in
|
|
||||||
# haproxy.cfg via the synapse-pro-federation-api-requests backend.
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
# GET-only worker path map. See path_map_file for context.
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"m.homeserver": {
|
|
||||||
"base_url": "https://{{ ess_hostnames.synapse }}"
|
|
||||||
},
|
|
||||||
"org.matrix.msc4143.rtc_foci": [
|
|
||||||
{
|
|
||||||
"livekit_service_url": "https://{{ ess_hostnames.matrix_rtc }}",
|
|
||||||
"type": "livekit"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"m.server": "{{ ess_hostnames.synapse }}:443"}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
## {{ ansible_managed }}
|
|
||||||
## Matrix Authentication Service — merged from chart fragments.
|
|
||||||
## Adapted from ess-helm {{ ess_chart_version }} for docker compose.
|
|
||||||
|
|
||||||
http:
|
|
||||||
public_base: "https://{{ ess_hostnames.mas }}/"
|
|
||||||
issuer: "https://{{ ess_hostnames.mas }}/"
|
|
||||||
listeners:
|
|
||||||
# Public web UI + OAuth + GraphQL + admin API. Fronted by DMZ Traefik.
|
|
||||||
- name: web
|
|
||||||
binds:
|
|
||||||
- host: 0.0.0.0
|
|
||||||
port: 8080
|
|
||||||
resources:
|
|
||||||
- name: human
|
|
||||||
- name: oauth
|
|
||||||
- name: assets
|
|
||||||
- name: graphql
|
|
||||||
undocumented_oauth2_access: true
|
|
||||||
- name: adminapi
|
|
||||||
# Internal — never exposed publicly. Used for healthchecks and metrics.
|
|
||||||
- name: internal
|
|
||||||
binds:
|
|
||||||
- host: 0.0.0.0
|
|
||||||
port: 8081
|
|
||||||
resources:
|
|
||||||
- name: health
|
|
||||||
- name: prometheus
|
|
||||||
- name: connection-info
|
|
||||||
# Root domain — serves .well-known/openid-configuration et al. on
|
|
||||||
# https://{{ ess_hostnames.mas }} root. Mounted as the public listener
|
|
||||||
# since DMZ Traefik strips paths.
|
|
||||||
- name: root
|
|
||||||
binds:
|
|
||||||
- host: 0.0.0.0
|
|
||||||
port: 8082
|
|
||||||
resources:
|
|
||||||
- name: discovery
|
|
||||||
- name: compat
|
|
||||||
# Talks to Synapse on the internal network only.
|
|
||||||
- name: synapse
|
|
||||||
binds:
|
|
||||||
- host: 0.0.0.0
|
|
||||||
port: 8083
|
|
||||||
resources:
|
|
||||||
- name: discovery
|
|
||||||
- name: oauth
|
|
||||||
|
|
||||||
database:
|
|
||||||
uri: "postgresql://matrixauthenticationservice_user:{{ _ess_secrets.POSTGRES_MATRIX_AUTHENTICATION_SERVICE_PASSWORD }}@postgres:5432/matrixauthenticationservice?sslmode=prefer&application_name=matrix-authentication-service"
|
|
||||||
|
|
||||||
telemetry:
|
|
||||||
metrics:
|
|
||||||
exporter: prometheus
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
homeserver: "{{ ess_server_name }}"
|
|
||||||
secret_file: {{ _ess_secret_mount }}/MAS_SYNAPSE_SHARED_SECRET
|
|
||||||
endpoint: "http://synapse-main:8008"
|
|
||||||
kind: synapse_modern
|
|
||||||
|
|
||||||
# ---- OAuth2 clients -------------------------------------------------------
|
|
||||||
clients:
|
|
||||||
# Matrix-tools admin client used by mas-cli operations.
|
|
||||||
- client_id: "000000000000000MATR1XT001S"
|
|
||||||
client_auth_method: client_secret_basic
|
|
||||||
client_secret_file: {{ _ess_secret_mount }}/MAS_MATRIX_TOOLS_OIDC_CLIENT_SECRET
|
|
||||||
|
|
||||||
# ---- Signing keys & encryption (file-mounted) ----------------------------
|
|
||||||
secrets:
|
|
||||||
encryption_file: {{ _ess_secret_mount }}/MAS_ENCRYPTION_SECRET
|
|
||||||
keys:
|
|
||||||
- key_file: {{ _ess_secret_mount }}/MAS_RSA_PRIVATE_KEY
|
|
||||||
- key_file: {{ _ess_secret_mount }}/MAS_ECDSA_PRIME256V1_PRIVATE_KEY
|
|
||||||
|
|
||||||
# ---- Policy ---------------------------------------------------------------
|
|
||||||
policy:
|
|
||||||
data:
|
|
||||||
admin_clients:
|
|
||||||
- "000000000000000MATR1XT001S"
|
|
||||||
admin_users: []
|
|
||||||
client_registration:
|
|
||||||
allow_host_mismatch: false
|
|
||||||
allow_insecure_uris: false
|
|
||||||
|
|
||||||
account:
|
|
||||||
password_registration_enabled: {{ ess_enable_registration | bool | lower }}
|
|
||||||
|
|
||||||
passwords:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
{% if ess_oidc_enabled %}
|
|
||||||
# ---- Upstream OIDC (Authentik for demo, Keycloak for prod) ----------------
|
|
||||||
upstream_oauth2:
|
|
||||||
providers:
|
|
||||||
- id: "{{ ess_oidc_provider_ulid }}"
|
|
||||||
human_name: "{{ ess_oidc_provider_name }}"
|
|
||||||
issuer: "{{ ess_oidc_issuer }}"
|
|
||||||
client_id: "{{ ess_oidc_client_id }}"
|
|
||||||
client_secret: "{{ ess_oidc_client_secret }}"
|
|
||||||
token_endpoint_auth_method: client_secret_basic
|
|
||||||
scope: "{{ ess_oidc_scopes }}"
|
|
||||||
claims_imports:
|
|
||||||
localpart:
|
|
||||||
action: require
|
|
||||||
template: "{{ '{{ user.preferred_username }}' }}"
|
|
||||||
displayname:
|
|
||||||
action: suggest
|
|
||||||
template: "{{ '{{ user.name }}' }}"
|
|
||||||
email:
|
|
||||||
action: suggest
|
|
||||||
template: "{{ '{{ user.email }}' }}"
|
|
||||||
set_email_verification: always
|
|
||||||
{% endif %}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
# {{ ansible_managed }}
|
|
||||||
# Postgres init script — chart-equivalent of configure-dbs.sh.
|
|
||||||
# Reads password files from /secrets/ess-generated and creates two DBs.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
create_or_ensure_db() {
|
|
||||||
user="$1"
|
|
||||||
db="$2"
|
|
||||||
password="$3"
|
|
||||||
admin_password="$4"
|
|
||||||
|
|
||||||
if echo -n "$admin_password" | psql -W -U postgres -tc "SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '$user'" | grep -q 1; then
|
|
||||||
echo -n "$admin_password" | psql -W -U postgres -c "ALTER USER $user PASSWORD '$password'"
|
|
||||||
else
|
|
||||||
echo -n "$admin_password" | psql -W -U postgres -c "CREATE ROLE $user LOGIN PASSWORD '$password'"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! echo -n "$admin_password" | psql -W -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = '$db'" | grep -q 1; then
|
|
||||||
echo -n "$admin_password" | createdb --encoding=UTF8 --locale=C --template=template0 --owner=$user $db -U postgres
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
POSTGRES_PASSWORD="$(cat /secrets/ess-generated/POSTGRES_ADMIN_PASSWORD)"
|
|
||||||
ESS_SYNAPSE_PW="$(cat /secrets/ess-generated/POSTGRES_SYNAPSE_PASSWORD)"
|
|
||||||
ESS_MAS_PW="$(cat /secrets/ess-generated/POSTGRES_MATRIX_AUTHENTICATION_SERVICE_PASSWORD)"
|
|
||||||
|
|
||||||
create_or_ensure_db "matrixauthenticationservice_user" "matrixauthenticationservice" "$ESS_MAS_PW" "$POSTGRES_PASSWORD"
|
|
||||||
create_or_ensure_db "synapse_user" "synapse" "$ESS_SYNAPSE_PW" "$POSTGRES_PASSWORD"
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# {{ ansible_managed }}
|
|
||||||
# Redis config — adapted from ess-helm {{ ess_chart_version }}. Used as
|
|
||||||
# pub/sub for Synapse worker replication; no persistence needed.
|
|
||||||
|
|
||||||
protected-mode no
|
|
||||||
port 6379
|
|
||||||
tcp-backlog 511
|
|
||||||
tcp-keepalive 300
|
|
||||||
timeout 0
|
|
||||||
daemonize no
|
|
||||||
supervised no
|
|
||||||
loglevel notice
|
|
||||||
logfile ''
|
|
||||||
databases 16
|
|
||||||
always-show-logo no
|
|
||||||
stop-writes-on-bgsave-error yes
|
|
||||||
save ''
|
|
||||||
|
|
||||||
# Disable persistence — Synapse uses Redis only for pub/sub between workers.
|
|
||||||
appendonly no
|
|
||||||
|
|
||||||
maxmemory 256mb
|
|
||||||
maxmemory-policy allkeys-lru
|
|
||||||
|
|
||||||
hz 1
|
|
||||||
dynamic-hz yes
|
|
||||||
jemalloc-bg-thread yes
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
## {{ ansible_managed }}
|
|
||||||
## LiveKit SFU — adapted from ess-helm {{ ess_chart_version }}.
|
|
||||||
|
|
||||||
port: 7880
|
|
||||||
|
|
||||||
prometheus:
|
|
||||||
port: 6789
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level: info
|
|
||||||
pion_level: error
|
|
||||||
json: false
|
|
||||||
|
|
||||||
rtc:
|
|
||||||
use_external_ip: false
|
|
||||||
tcp_port: {{ ess_rtc_tcp_port }}
|
|
||||||
udp_port: {{ ess_rtc_udp_port }}
|
|
||||||
# Public IP that LiveKit advertises in ICE candidates. The DMZ NAT forwards
|
|
||||||
# {{ ess_rtc_tcp_port }}/TCP and {{ ess_rtc_udp_port }}/UDP to this host.
|
|
||||||
node_ip: "{{ ess_rtc_external_ip }}"
|
|
||||||
|
|
||||||
# Keys are embedded directly (rendered at compose-up time). The single key
|
|
||||||
# `{{ ess_livekit_key }}` matches what the authorisation service issues
|
|
||||||
# tokens against.
|
|
||||||
keys:
|
|
||||||
{{ ess_livekit_key }}: "{{ _ess_secrets.ELEMENT_CALL_LIVEKIT_SECRET }}"
|
|
||||||
|
|
||||||
room:
|
|
||||||
auto_create: false
|
|
||||||
|
|
||||||
turn:
|
|
||||||
enabled: false
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
## {{ ansible_managed }}
|
|
||||||
## synapse-pro-worker (Rust) federation reader.
|
|
||||||
## This is a different config schema than Python Synapse.
|
|
||||||
|
|
||||||
http:
|
|
||||||
bind_addr: "::"
|
|
||||||
bind_port: 8008
|
|
||||||
|
|
||||||
metrics:
|
|
||||||
bind_addr: "::"
|
|
||||||
bind_port: 9001
|
|
||||||
|
|
||||||
synapse:
|
|
||||||
server_name: "{{ ess_server_name }}"
|
|
||||||
|
|
||||||
database:
|
|
||||||
connection_string: "postgresql://synapse_user:{{ _ess_secrets.POSTGRES_SYNAPSE_PASSWORD }}@postgres:5432/synapse?sslmode=prefer"
|
|
||||||
|
|
||||||
redis:
|
|
||||||
host: redis
|
|
||||||
port: 6379
|
|
||||||
|
|
||||||
logging: basic
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
## {{ ansible_managed }}
|
|
||||||
## Synapse homeserver config — merged from chart fragments
|
|
||||||
## 01-homeserver-underrides + 04-homeserver-overrides + 05-main.
|
|
||||||
## Adapted from ess-helm {{ ess_chart_version }} for docker compose.
|
|
||||||
|
|
||||||
server_name: "{{ ess_server_name }}"
|
|
||||||
public_baseurl: "https://{{ ess_hostnames.synapse }}/"
|
|
||||||
web_client_location: "https://{{ ess_hostnames.element_web }}/"
|
|
||||||
admin_contact: "{{ ess_admin_contact }}"
|
|
||||||
|
|
||||||
pid_file: /data/homeserver.pid
|
|
||||||
signing_key_path: {{ _ess_secret_mount }}/SYNAPSE_SIGNING_KEY
|
|
||||||
macaroon_secret_key_path: {{ _ess_secret_mount }}/SYNAPSE_MACAROON
|
|
||||||
registration_shared_secret_path: {{ _ess_secret_mount }}/SYNAPSE_REGISTRATION_SHARED_SECRET
|
|
||||||
worker_replication_secret_path: {{ _ess_secret_mount }}/SYNAPSE_WORKERS_REPLICATION_SECRET
|
|
||||||
|
|
||||||
log_config: "/conf/log_config.yaml"
|
|
||||||
enable_metrics: true
|
|
||||||
report_stats: false
|
|
||||||
|
|
||||||
# ---- Listeners (from 05-main.yaml) ----------------------------------------
|
|
||||||
listeners:
|
|
||||||
- port: 8008
|
|
||||||
tls: false
|
|
||||||
type: http
|
|
||||||
bind_addresses: ['0.0.0.0', '::']
|
|
||||||
x_forwarded: true
|
|
||||||
resources:
|
|
||||||
- names: [client, federation]
|
|
||||||
compress: false
|
|
||||||
- port: 9093
|
|
||||||
tls: false
|
|
||||||
type: http
|
|
||||||
bind_addresses: ['0.0.0.0', '::']
|
|
||||||
x_forwarded: false
|
|
||||||
resources:
|
|
||||||
- names: [replication]
|
|
||||||
compress: false
|
|
||||||
- port: 8080
|
|
||||||
tls: false
|
|
||||||
type: http
|
|
||||||
bind_addresses: ['0.0.0.0', '::']
|
|
||||||
x_forwarded: false
|
|
||||||
resources:
|
|
||||||
- names: [health]
|
|
||||||
compress: false
|
|
||||||
- type: metrics
|
|
||||||
port: 9001
|
|
||||||
bind_addresses: ['::']
|
|
||||||
|
|
||||||
enable_media_repo: true
|
|
||||||
media_store_path: "/media/media_store"
|
|
||||||
max_upload_size: "{{ ess_synapse_max_upload_size }}"
|
|
||||||
|
|
||||||
# ---- Pro modules ----------------------------------------------------------
|
|
||||||
modules:
|
|
||||||
- module: "synapse_ess_pro.EssPro"
|
|
||||||
config:
|
|
||||||
version_path: /ess/version
|
|
||||||
- module: "synapse_mass_local_room_upgrades.MassLocalRoomUpgradesModule"
|
|
||||||
config: {}
|
|
||||||
|
|
||||||
# ---- Database -------------------------------------------------------------
|
|
||||||
database:
|
|
||||||
name: psycopg2
|
|
||||||
args:
|
|
||||||
user: synapse_user
|
|
||||||
password: "{{ _ess_secrets.POSTGRES_SYNAPSE_PASSWORD }}"
|
|
||||||
dbname: synapse
|
|
||||||
host: postgres
|
|
||||||
port: 5432
|
|
||||||
sslmode: prefer
|
|
||||||
keepalives: 1
|
|
||||||
keepalives_idle: 10
|
|
||||||
keepalives_interval: 10
|
|
||||||
keepalives_count: 3
|
|
||||||
cp_min: 5
|
|
||||||
cp_max: 10
|
|
||||||
|
|
||||||
# ---- Redis (required for workers) -----------------------------------------
|
|
||||||
redis:
|
|
||||||
enabled: true
|
|
||||||
host: redis
|
|
||||||
port: 6379
|
|
||||||
|
|
||||||
# Replication topology — fed-reader connects back to the main on 9093.
|
|
||||||
instance_map:
|
|
||||||
main:
|
|
||||||
host: synapse-main
|
|
||||||
port: 9093
|
|
||||||
|
|
||||||
# ---- Matrix 2.0 features (MSC4108 QR login, MSC4222 syncv2, MSC4143 RTC) --
|
|
||||||
experimental_features:
|
|
||||||
msc4143_enabled: true
|
|
||||||
msc4222_enabled: true
|
|
||||||
msc4108_enabled: true
|
|
||||||
msc4028_push_encrypted_events: true
|
|
||||||
|
|
||||||
# ---- Delegated auth to MAS (stable since Synapse 1.118) -------------------
|
|
||||||
matrix_authentication_service:
|
|
||||||
enabled: true
|
|
||||||
secret_path: {{ _ess_secret_mount }}/MAS_SYNAPSE_SHARED_SECRET
|
|
||||||
endpoint: "http://mas:8083/"
|
|
||||||
force_http2: true
|
|
||||||
|
|
||||||
password_config:
|
|
||||||
localdb_enabled: false
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# ---- Matrix RTC (Element Call discovery) ----------------------------------
|
|
||||||
matrix_rtc:
|
|
||||||
transports:
|
|
||||||
- type: livekit
|
|
||||||
livekit_service_url: "https://{{ ess_hostnames.matrix_rtc }}"
|
|
||||||
|
|
||||||
# ---- URL previews ---------------------------------------------------------
|
|
||||||
url_preview_enabled: {{ ess_synapse_url_previews_enabled | bool | lower }}
|
|
||||||
url_preview_ip_range_whitelist: []
|
|
||||||
url_preview_ip_range_blacklist:
|
|
||||||
- '127.0.0.0/8'
|
|
||||||
- '10.0.0.0/8'
|
|
||||||
- '172.16.0.0/12'
|
|
||||||
- '192.168.0.0/16'
|
|
||||||
- '100.64.0.0/10'
|
|
||||||
- '169.254.0.0/16'
|
|
||||||
- '::1/128'
|
|
||||||
- 'fe80::/10'
|
|
||||||
- 'fc00::/7'
|
|
||||||
|
|
||||||
# ---- Federation -----------------------------------------------------------
|
|
||||||
{% if ess_enable_federation %}
|
|
||||||
send_federation: true
|
|
||||||
federation_client_minimum_tls_version: '1.2'
|
|
||||||
{% else %}
|
|
||||||
send_federation: false
|
|
||||||
federation_domain_whitelist: []
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# ---- Other defaults from chart underrides ---------------------------------
|
|
||||||
require_auth_for_profile_requests: true
|
|
||||||
presence:
|
|
||||||
enabled: false
|
|
||||||
start_pushers: true
|
|
||||||
max_event_delay_duration: 24h
|
|
||||||
|
|
||||||
room_list_publication_rules:
|
|
||||||
- action: allow
|
|
||||||
user_id: "@*:{{ ess_server_name }}"
|
|
||||||
|
|
||||||
rc_message:
|
|
||||||
per_second: 0.5
|
|
||||||
burst_count: 30
|
|
||||||
rc_delayed_event_mgmt:
|
|
||||||
per_second: 1
|
|
||||||
burst_count: 20
|
|
||||||
|
|
||||||
trusted_key_servers:
|
|
||||||
- server_name: "matrix.org"
|
|
||||||
suppress_key_server_warning: true
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
## {{ ansible_managed }}
|
|
||||||
version: 1
|
|
||||||
formatters:
|
|
||||||
precise:
|
|
||||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
|
||||||
handlers:
|
|
||||||
console:
|
|
||||||
class: logging.StreamHandler
|
|
||||||
formatter: precise
|
|
||||||
loggers:
|
|
||||||
synapse.storage.SQL:
|
|
||||||
level: INFO
|
|
||||||
root:
|
|
||||||
level: INFO
|
|
||||||
handlers: [console]
|
|
||||||
disable_existing_loggers: false
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
# SPDX-License-Identifier: MIT-0
|
|
||||||
---
|
|
||||||
# Internal — do not override in inventory.
|
|
||||||
|
|
||||||
# Mount points inside containers (Element Pro convention)
|
|
||||||
_ess_secret_mount: "/secrets/ess-generated"
|
|
||||||
_ess_conf_mount: "/conf"
|
|
||||||
_ess_well_known_mount: "/well-known"
|
|
||||||
|
|
||||||
# Compose file path
|
|
||||||
_ess_compose_file: "{{ ess_compose_dir }}/compose.yml"
|
|
||||||
_ess_env_file: "{{ ess_compose_dir }}/.env"
|
|
||||||
|
|
||||||
# Directory tree to create on the host
|
|
||||||
_ess_dirs:
|
|
||||||
- "{{ ess_compose_dir }}"
|
|
||||||
- "{{ ess_compose_conf_dir }}"
|
|
||||||
- "{{ ess_compose_conf_dir }}/haproxy"
|
|
||||||
- "{{ ess_compose_conf_dir }}/haproxy/well-known"
|
|
||||||
- "{{ ess_compose_conf_dir }}/synapse"
|
|
||||||
- "{{ ess_compose_conf_dir }}/mas"
|
|
||||||
- "{{ ess_compose_conf_dir }}/sfu"
|
|
||||||
- "{{ ess_compose_conf_dir }}/element-web"
|
|
||||||
- "{{ ess_compose_conf_dir }}/postgres"
|
|
||||||
- "{{ ess_compose_conf_dir }}/redis"
|
|
||||||
- "{{ ess_compose_secrets_dir }}"
|
|
||||||
- "{{ ess_compose_data_dir }}"
|
|
||||||
- "{{ ess_compose_data_dir }}/postgres"
|
|
||||||
- "{{ ess_compose_data_dir }}/synapse-media"
|
|
||||||
|
|
||||||
# All Element Pro secret-file names (matches the init-secrets job in the chart)
|
|
||||||
_ess_secret_names:
|
|
||||||
- POSTGRES_ADMIN_PASSWORD
|
|
||||||
- POSTGRES_SYNAPSE_PASSWORD
|
|
||||||
- POSTGRES_MATRIX_AUTHENTICATION_SERVICE_PASSWORD
|
|
||||||
- SYNAPSE_MACAROON
|
|
||||||
- SYNAPSE_REGISTRATION_SHARED_SECRET
|
|
||||||
- SYNAPSE_WORKERS_REPLICATION_SECRET
|
|
||||||
- SYNAPSE_SIGNING_KEY
|
|
||||||
- MAS_SYNAPSE_SHARED_SECRET
|
|
||||||
- MAS_MATRIX_TOOLS_OIDC_CLIENT_SECRET
|
|
||||||
- MAS_ENCRYPTION_SECRET
|
|
||||||
- MAS_RSA_PRIVATE_KEY
|
|
||||||
- MAS_ECDSA_PRIME256V1_PRIVATE_KEY
|
|
||||||
- ELEMENT_CALL_LIVEKIT_SECRET
|
|
||||||
- ADMIN_USER_PASSWORD
|
|
||||||
|
|
@ -1,113 +1,118 @@
|
||||||
Garage
|
# Garage
|
||||||
======
|
|
||||||
|
|
||||||
Ansible role to deploy Garage S3-compatible object storage using Docker Compose.
|
Ansible role to deploy [Garage](https://garagehq.deuxfleurs.fr/) S3-compatible
|
||||||
|
object storage via Docker Compose, with declarative key/bucket
|
||||||
|
provisioning and an optional WebUI behind htpasswd or authentik
|
||||||
|
ForwardAuth.
|
||||||
|
|
||||||
Requirements
|
## Requirements
|
||||||
------------
|
|
||||||
|
|
||||||
- Docker and Docker Compose installed on the target host
|
- Docker and Docker Compose installed on the target host
|
||||||
- Ansible collection: `community.docker`
|
- Ansible collection: `community.docker`
|
||||||
- Traefik reverse proxy (for external access)
|
- `htpasswd` (from `apache2-utils` / `httpd-tools`) when the WebUI is
|
||||||
|
enabled and authentik ForwardAuth is *not* used
|
||||||
|
- Traefik with a shared `garage_traefik_network` (default `proxy`)
|
||||||
|
|
||||||
Role Variables
|
## Role variables
|
||||||
--------------
|
|
||||||
|
|
||||||
Key variables defined in `defaults/main.yml`:
|
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
||||||
|
common overrides:
|
||||||
|
|
||||||
**Base Configuration:**
|
### Service
|
||||||
- `docker_compose_base_dir`: Base directory for Docker Compose files (default: `/etc/docker/compose`)
|
|
||||||
- `docker_volume_base_dir`: Base directory for Docker volumes (default: `/srv/data`)
|
|
||||||
|
|
||||||
**Garage Configuration:**
|
- `garage_s3_domains`: FQDNs the S3 router accepts. The first entry is the
|
||||||
- `garage_service_name`: Service name (default: `garage`)
|
canonical hostname; `garage.toml` derives the virtual-hosted-style S3
|
||||||
- `garage_image`: Garage Docker image (default: `dxflrs/garage:v2.1.0`)
|
`root_domain` from it as `.s3.<first-entry>` (so buckets resolve under
|
||||||
- `garage_s3_domain`: Domain for S3 API endpoint (default: `storage.local.test`)
|
`<bucket>.s3.<first-entry>`).
|
||||||
- `garage_web_domain`: Domain for S3 web endpoint (default: `web.storage.local.test`)
|
- `garage_web_domain`, `garage_webui_domain`: separate hostnames for
|
||||||
- `garage_webui_domain`: Domain for web console (default: `console.storage.local.test`)
|
the S3-website endpoint and the console.
|
||||||
|
- `garage_image`, `garage_replication_factor`, `garage_db_engine`,
|
||||||
|
`garage_s3_region`.
|
||||||
|
|
||||||
**Garage Storage Configuration:**
|
### Required secrets
|
||||||
- `garage_replication_factor`: Replication factor (default: `1`)
|
|
||||||
- `garage_compression_level`: Compression level (default: `1`)
|
|
||||||
- `garage_db_engine`: Database engine (default: `lmdb`)
|
|
||||||
- `garage_s3_region`: S3 region (default: `us-east-1`)
|
|
||||||
|
|
||||||
**Garage Ports:**
|
Generate with `openssl rand -hex 32` (32 bytes / 64 hex chars):
|
||||||
- `garage_s3_api_port`: S3 API port (default: `3900`)
|
|
||||||
- `garage_s3_web_port`: S3 web port (default: `3902`)
|
|
||||||
- `garage_admin_port`: Admin API port (default: `3903`)
|
|
||||||
- `garage_rpc_port`: RPC port (default: `3901`)
|
|
||||||
|
|
||||||
**Garage Security:**
|
- `garage_rpc_secret`: node-to-node RPC secret
|
||||||
- `garage_rpc_secret`: RPC secret for node communication
|
- `garage_admin_token`: admin API token
|
||||||
- `garage_admin_token`: Admin API token
|
- `garage_metrics_token`: metrics endpoint token
|
||||||
- `garage_metrics_token`: Metrics API token
|
|
||||||
|
|
||||||
**Garage WebUI Configuration:**
|
### WebUI authentication
|
||||||
- `garage_webui_enabled`: Enable web UI (default: `true`)
|
|
||||||
- `garage_webui_image`: WebUI Docker image (default: `khairul169/garage-webui:latest`)
|
|
||||||
- `garage_webui_port`: WebUI port (default: `3909`)
|
|
||||||
- `garage_webui_username`: WebUI username (default: `admin`)
|
|
||||||
- `garage_webui_password`: WebUI password in plaintext (default: `admin`)
|
|
||||||
|
|
||||||
**Traefik Configuration:**
|
Three modes:
|
||||||
- `garage_traefik_network`: Traefik network name (default: `proxy`)
|
|
||||||
- `garage_internal_network`: Internal network name (default: `internal`)
|
|
||||||
- `garage_use_ssl`: Enable SSL (default: `true`)
|
|
||||||
|
|
||||||
Dependencies
|
1. **htpasswd** (default): `garage_webui_username` / `garage_webui_password`
|
||||||
------------
|
in plaintext. The role hashes the password with
|
||||||
|
`htpasswd -nbBC 10`, persists the hash on disk, and re-verifies with
|
||||||
|
`htpasswd -vbB` so unchanged passwords don't churn the play.
|
||||||
|
2. **authentik ForwardAuth**: set
|
||||||
|
`garage_webui_authentik_forward_auth: true` and
|
||||||
|
`garage_webui_authentik_forward_auth_url:
|
||||||
|
"https://auth.example.com/outpost.goauthentik.io/auth/traefik"`.
|
||||||
|
`AUTH_USER_PASS` is dropped from the container env so authentik is
|
||||||
|
the only gate.
|
||||||
|
3. **Disabled**: `garage_webui_enabled: false`.
|
||||||
|
|
||||||
This role requires:
|
### Layout bootstrap
|
||||||
- Traefik reverse proxy to be configured and the `proxy` network to be created
|
|
||||||
- `htpasswd` utility (from `apache2-utils` package) for generating bcrypt password hashes
|
|
||||||
|
|
||||||
Example Playbook
|
Setting `garage_bootstrap_enabled: true` runs the bootstrap task, which
|
||||||
----------------
|
joins the local node to the layout (`zone: garage_bootstrap_zone`,
|
||||||
|
capacity: `garage_bootstrap_capacity`) on the first run. The check
|
||||||
|
tolerates the 16-char truncation that `garage layout show` performs.
|
||||||
|
|
||||||
|
### Declarative S3 keys and buckets
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
garage_s3_keys:
|
||||||
|
- name: nextcloud
|
||||||
|
buckets:
|
||||||
|
- name: nextcloud-data
|
||||||
|
permissions: [read, write]
|
||||||
|
- name: backup
|
||||||
|
buckets:
|
||||||
|
- name: restic-prod
|
||||||
|
permissions: [read, write, owner]
|
||||||
|
```
|
||||||
|
|
||||||
|
The role:
|
||||||
|
|
||||||
|
- Lists existing keys (`garage key list`), creates missing ones
|
||||||
|
- Lists existing buckets (`garage bucket list`), creates missing ones
|
||||||
|
- Reads current permissions via `garage bucket info` and runs
|
||||||
|
`garage bucket allow` only when the current RWO flags for the key
|
||||||
|
don't already match the desired permissions
|
||||||
|
|
||||||
|
`stdout` parsing is hardened against ANSI escapes and interleaved INFO
|
||||||
|
log lines, so probe noise no longer produces spurious changes.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Traefik network (`garage_traefik_network`, default `proxy`)
|
||||||
|
- Internal network (`garage_internal_network`, default `internal`)
|
||||||
|
|
||||||
|
## Example playbook
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- hosts: storage_servers
|
- hosts: storage_servers
|
||||||
roles:
|
roles:
|
||||||
- role: garage
|
- role: digitalboard.core.garage
|
||||||
vars:
|
vars:
|
||||||
garage_s3_domain: "storage.example.com"
|
garage_s3_domains:
|
||||||
garage_rpc_secret: "your-secure-rpc-secret"
|
- "storage.example.com"
|
||||||
garage_admin_token: "your-admin-token"
|
- "storage.int.example.com"
|
||||||
garage_webui_enabled: true
|
garage_rpc_secret: "{{ vault_garage_rpc_secret }}"
|
||||||
garage_webui_username: "admin"
|
garage_admin_token: "{{ vault_garage_admin_token }}"
|
||||||
garage_webui_password: "secure-password"
|
garage_metrics_token: "{{ vault_garage_metrics_token }}"
|
||||||
|
garage_bootstrap_enabled: true
|
||||||
|
garage_webui_authentik_forward_auth: true
|
||||||
|
garage_webui_authentik_forward_auth_url: "https://auth.example.com/outpost.goauthentik.io/auth/traefik"
|
||||||
|
garage_s3_keys:
|
||||||
|
- name: nextcloud
|
||||||
|
buckets:
|
||||||
|
- name: nextcloud-data
|
||||||
|
permissions: [read, write]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** The WebUI password is specified in plaintext and will be automatically hashed using bcrypt during deployment. The role uses `htpasswd` to generate a secure bcrypt hash that is then properly escaped for use in Docker Compose.
|
## License
|
||||||
|
|
||||||
Post-Installation
|
MIT-0
|
||||||
-----------------
|
|
||||||
|
|
||||||
After deployment, you need to configure the Garage cluster:
|
|
||||||
|
|
||||||
1. Connect to the node and get the node ID:
|
|
||||||
```bash
|
|
||||||
docker exec -ti garage /garage node id
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Configure the node layout:
|
|
||||||
```bash
|
|
||||||
docker exec -ti garage /garage layout assign -z dc1 -c 1G <node-id>
|
|
||||||
docker exec -ti garage /garage layout apply --version 1
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Create a key for S3 access:
|
|
||||||
```bash
|
|
||||||
docker exec -ti garage /garage key create my-key
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Create a bucket:
|
|
||||||
```bash
|
|
||||||
docker exec -ti garage /garage bucket create my-bucket
|
|
||||||
docker exec -ti garage /garage bucket allow my-bucket --read --write --key my-key
|
|
||||||
```
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
MIT-0
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ garage_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ garage_service_name }
|
||||||
|
|
||||||
# Garage service configuration
|
# Garage service configuration
|
||||||
garage_image: "dxflrs/garage:v2.1.0"
|
garage_image: "dxflrs/garage:v2.1.0"
|
||||||
garage_s3_domain: "storage.local.test"
|
# FQDNs the garage S3 router accepts. The first entry is the canonical
|
||||||
|
# domain; garage.toml derives the virtual-hosted-style S3 root_domain
|
||||||
|
# from it as ".s3.<first-entry>"; further entries cover internal
|
||||||
|
# *.int.* names.
|
||||||
|
garage_s3_domains:
|
||||||
|
- "storage.local.test"
|
||||||
garage_web_domain: "web.storage.local.test"
|
garage_web_domain: "web.storage.local.test"
|
||||||
garage_webui_domain: "console.storage.local.test"
|
garage_webui_domain: "console.storage.local.test"
|
||||||
|
|
||||||
|
|
@ -21,10 +26,20 @@ garage_webui_domain: "console.storage.local.test"
|
||||||
garage_webui_enabled: true
|
garage_webui_enabled: true
|
||||||
garage_webui_image: "khairul169/garage-webui:latest"
|
garage_webui_image: "khairul169/garage-webui:latest"
|
||||||
garage_webui_port: 3909
|
garage_webui_port: 3909
|
||||||
# WebUI basic auth credentials (plaintext, will be hashed automatically)
|
# WebUI basic auth credentials (plaintext, will be hashed automatically).
|
||||||
|
# Ignored when garage_webui_authentik_forward_auth is true — in that case
|
||||||
|
# authentik handles authentication via the ForwardAuth middleware below.
|
||||||
garage_webui_username: "admin"
|
garage_webui_username: "admin"
|
||||||
garage_webui_password: "admin"
|
garage_webui_password: "admin"
|
||||||
|
|
||||||
|
# Optional Authentik ForwardAuth in front of the WebUI. When true:
|
||||||
|
# - the AUTH_USER_PASS env-var is dropped from the container so htpasswd
|
||||||
|
# isn't enforced; authentik is the only gate.
|
||||||
|
# - traefik attaches a ForwardAuth middleware pointing at the URL below.
|
||||||
|
# Leave false to keep classic htpasswd protection.
|
||||||
|
garage_webui_authentik_forward_auth: false
|
||||||
|
garage_webui_authentik_forward_auth_url: ""
|
||||||
|
|
||||||
# Garage ports
|
# Garage ports
|
||||||
garage_s3_api_port: 3900
|
garage_s3_api_port: 3900
|
||||||
garage_s3_web_port: 3902
|
garage_s3_web_port: 3902
|
||||||
|
|
|
||||||
169
roles/garage/meta/argument_specs.yml
Normal file
169
roles/garage/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy Garage S3-compatible object storage via Docker Compose.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for Garage with traefik labels, configures the
|
||||||
|
node layout on first run, and (optionally) provisions S3 keys, buckets
|
||||||
|
and per-key permissions declaratively.
|
||||||
|
- The optional WebUI can be protected by classic htpasswd or by
|
||||||
|
authentik ForwardAuth.
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
docker_volume_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /srv/data
|
||||||
|
garage_service_name:
|
||||||
|
type: str
|
||||||
|
default: garage
|
||||||
|
garage_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_compose_base_dir }}/{{ garage_service_name }}).
|
||||||
|
garage_docker_volume_dir:
|
||||||
|
type: path
|
||||||
|
description: Defaults to C({{ docker_volume_base_dir }}/{{ garage_service_name }}).
|
||||||
|
|
||||||
|
garage_image:
|
||||||
|
type: str
|
||||||
|
default: dxflrs/garage:v2.1.0
|
||||||
|
|
||||||
|
garage_s3_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: ['storage.local.test']
|
||||||
|
description:
|
||||||
|
- FQDNs the garage S3 router accepts. The first entry is the
|
||||||
|
canonical domain; C(garage.toml) derives the virtual-hosted-style
|
||||||
|
S3 C(root_domain) from it as C(.s3.<first-entry>). Further entries
|
||||||
|
cover internal C(*.int.*) names.
|
||||||
|
garage_web_domain:
|
||||||
|
type: str
|
||||||
|
default: web.storage.local.test
|
||||||
|
description: Hostname serving the S3-website endpoint.
|
||||||
|
garage_webui_domain:
|
||||||
|
type: str
|
||||||
|
default: console.storage.local.test
|
||||||
|
description: Hostname serving the WebUI console.
|
||||||
|
|
||||||
|
garage_webui_enabled:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
garage_webui_image:
|
||||||
|
type: str
|
||||||
|
default: khairul169/garage-webui:latest
|
||||||
|
garage_webui_port:
|
||||||
|
type: int
|
||||||
|
default: 3909
|
||||||
|
garage_webui_username:
|
||||||
|
type: str
|
||||||
|
default: admin
|
||||||
|
description: htpasswd username. Ignored when C(garage_webui_authentik_forward_auth=true).
|
||||||
|
garage_webui_password:
|
||||||
|
type: str
|
||||||
|
default: admin
|
||||||
|
description:
|
||||||
|
- Plaintext password; hashed with C(htpasswd -nbBC 10) and persisted
|
||||||
|
on disk so re-runs don't churn. Ignored when authentik ForwardAuth
|
||||||
|
is enabled.
|
||||||
|
garage_webui_authentik_forward_auth:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description:
|
||||||
|
- When true the C(AUTH_USER_PASS) env-var is dropped from the WebUI
|
||||||
|
container and traefik attaches a ForwardAuth middleware pointing
|
||||||
|
at the URL below. authentik is then the only gate; htpasswd is
|
||||||
|
disabled.
|
||||||
|
garage_webui_authentik_forward_auth_url:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description:
|
||||||
|
- Required when C(garage_webui_authentik_forward_auth=true).
|
||||||
|
Typically C(https://auth.example.com/outpost.goauthentik.io/auth/traefik).
|
||||||
|
|
||||||
|
garage_s3_api_port:
|
||||||
|
type: int
|
||||||
|
default: 3900
|
||||||
|
garage_s3_web_port:
|
||||||
|
type: int
|
||||||
|
default: 3902
|
||||||
|
garage_admin_port:
|
||||||
|
type: int
|
||||||
|
default: 3903
|
||||||
|
garage_rpc_port:
|
||||||
|
type: int
|
||||||
|
default: 3901
|
||||||
|
|
||||||
|
garage_replication_factor:
|
||||||
|
type: int
|
||||||
|
default: 1
|
||||||
|
garage_compression_level:
|
||||||
|
type: int
|
||||||
|
default: 1
|
||||||
|
garage_db_engine:
|
||||||
|
type: str
|
||||||
|
choices: [lmdb, sqlite, sled]
|
||||||
|
default: lmdb
|
||||||
|
garage_s3_region:
|
||||||
|
type: str
|
||||||
|
default: us-east-1
|
||||||
|
garage_rpc_secret:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
description: Hex secret for node-to-node RPC. Generate with C(openssl rand -hex 32).
|
||||||
|
garage_admin_token:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
garage_metrics_token:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
|
||||||
|
garage_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
garage_internal_network:
|
||||||
|
type: str
|
||||||
|
default: internal
|
||||||
|
garage_use_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
||||||
|
garage_bootstrap_enabled:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description: When true the bootstrap task ensures the node is in the layout.
|
||||||
|
garage_bootstrap_zone:
|
||||||
|
type: str
|
||||||
|
default: dc1
|
||||||
|
description: Zone label assigned during layout bootstrap.
|
||||||
|
garage_bootstrap_capacity:
|
||||||
|
type: str
|
||||||
|
default: 1G
|
||||||
|
description: Capacity string passed to C(garage layout assign -c).
|
||||||
|
|
||||||
|
garage_s3_keys:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
description:
|
||||||
|
- Declarative key + bucket + permission provisioning. The role
|
||||||
|
creates missing keys, missing buckets, and runs C(bucket allow)
|
||||||
|
only when the current RWO flags for a given key don't match.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
buckets:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
description: Buckets this key gets access to.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
permissions:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
choices: [read, write, owner]
|
||||||
|
required: true
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy Garage S3-compatible object storage via Docker Compose, with declarative key/bucket provisioning
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
galaxy_tags:
|
||||||
|
- garage
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- s3
|
||||||
# min_ansible_container_version:
|
- storage
|
||||||
|
- object-storage
|
||||||
galaxy_tags: []
|
- docker
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,27 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage node id -q
|
command: /garage node id -q
|
||||||
register: _garage_node_id
|
register: _garage_node_id
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: Extract short node ID
|
- name: Extract short node ID
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_garage_node_id_short: "{{ _garage_node_id.stdout.split('@')[0] }}"
|
_garage_node_id_short: "{{ _garage_node_id.stdout.split('@')[0] }}"
|
||||||
|
|
||||||
|
- name: Extract truncated node ID (first 16 chars, matches `garage layout show` output)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_garage_node_id_truncated: "{{ _garage_node_id_short[:16] }}"
|
||||||
|
|
||||||
- name: Check if node layout is configured
|
- name: Check if node layout is configured
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage layout show
|
command: /garage layout show
|
||||||
register: _garage_layout_show
|
register: _garage_layout_show
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
- name: Check if node is in layout
|
- name: Check if node is in layout
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_node_in_layout: "{{ _garage_node_id_short in _garage_layout_show.stdout }}"
|
_node_in_layout: "{{ (_garage_node_id_truncated in _garage_layout_show.stdout) or (_garage_node_id_short in _garage_layout_show.stdout) }}"
|
||||||
|
|
||||||
- name: Configure garage node layout
|
- name: Configure garage node layout
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,77 @@
|
||||||
dest: "{{ garage_docker_compose_dir }}/garage.toml"
|
dest: "{{ garage_docker_compose_dir }}/garage.toml"
|
||||||
mode: '0644'
|
mode: '0644'
|
||||||
|
|
||||||
- name: Generate bcrypt hash for webui password using htpasswd
|
- name: Set webui htpasswd activation fact
|
||||||
ansible.builtin.shell: |
|
ansible.builtin.set_fact:
|
||||||
htpasswd -nbBC 10 "{{ garage_webui_username }}" "{{ garage_webui_password }}"
|
# htpasswd only runs when the WebUI is enabled AND authentik ForwardAuth
|
||||||
register: _garage_webui_password_hash
|
# is not handling authentication. When authentik is in front, the
|
||||||
|
# compose template drops AUTH_USER_PASS so no hash is needed.
|
||||||
|
_garage_webui_htpasswd_active: >-
|
||||||
|
{{
|
||||||
|
garage_webui_enabled
|
||||||
|
and not (garage_webui_authentik_forward_auth | default(false))
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Read cached webui htpasswd hash
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
||||||
|
register: _garage_webui_htpasswd_cached
|
||||||
|
failed_when: false
|
||||||
changed_when: false
|
changed_when: false
|
||||||
when: garage_webui_enabled
|
when: _garage_webui_htpasswd_active
|
||||||
|
|
||||||
|
- name: Verify cached webui htpasswd hash still matches password
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- htpasswd
|
||||||
|
- -vbB
|
||||||
|
- "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
||||||
|
- "{{ garage_webui_username }}"
|
||||||
|
- "{{ garage_webui_password }}"
|
||||||
|
register: _garage_webui_htpasswd_verify
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- _garage_webui_htpasswd_active
|
||||||
|
- _garage_webui_htpasswd_cached.content is defined
|
||||||
|
|
||||||
|
- name: Generate bcrypt hash for webui password using htpasswd
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- htpasswd
|
||||||
|
- -nbBC
|
||||||
|
- "10"
|
||||||
|
- "{{ garage_webui_username }}"
|
||||||
|
- "{{ garage_webui_password }}"
|
||||||
|
register: _garage_webui_password_hash_new
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- _garage_webui_htpasswd_active
|
||||||
|
- (_garage_webui_htpasswd_cached.content is not defined)
|
||||||
|
or (_garage_webui_htpasswd_verify.rc | default(1) != 0)
|
||||||
|
|
||||||
|
- name: Persist webui htpasswd hash on disk
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ _garage_webui_password_hash_new.stdout }}\n"
|
||||||
|
dest: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
||||||
|
mode: '0600'
|
||||||
|
when:
|
||||||
|
- _garage_webui_htpasswd_active
|
||||||
|
- _garage_webui_password_hash_new is changed
|
||||||
|
|
||||||
|
- name: Load current webui htpasswd hash
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ garage_docker_compose_dir }}/webui.htpasswd"
|
||||||
|
register: _garage_webui_htpasswd_current
|
||||||
|
changed_when: false
|
||||||
|
when: _garage_webui_htpasswd_active
|
||||||
|
|
||||||
|
- name: Expose current webui htpasswd hash to template
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_garage_webui_password_hash:
|
||||||
|
stdout: "{{ (_garage_webui_htpasswd_current.content | b64decode).strip() }}"
|
||||||
|
when: _garage_webui_htpasswd_active
|
||||||
|
|
||||||
- name: Create docker-compose file for garage
|
- name: Create docker-compose file for garage
|
||||||
template:
|
template:
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,17 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage key list
|
command: /garage key list
|
||||||
register: _existing_keys_output
|
register: _existing_keys_output
|
||||||
|
changed_when: false
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Parse existing key names
|
- name: Parse existing key names
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_existing_keys: "{{ _existing_keys_output.stdout_lines[1:] | select('match', '^GK') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+)\\s+.*$', '\\1') | list }}"
|
# `garage key list` columns: ID Created Name Expiration.
|
||||||
|
# Data rows begin with a GK<hex> key ID; header is "ID Created ..."
|
||||||
|
# and INFO log lines may interleave on stderr (kept separate by
|
||||||
|
# docker_container_exec). Strip ANSI escapes defensively, filter to
|
||||||
|
# GK-prefixed rows, then take the 3rd whitespace-separated field.
|
||||||
|
_existing_keys: "{{ _existing_keys_output.stdout_lines | map('regex_replace', '\\x1b\\[[0-9;]*m', '') | select('match', '^GK[0-9a-fA-F]+') | map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1') | list }}"
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Create S3 keys
|
- name: Create S3 keys
|
||||||
|
|
@ -27,6 +33,7 @@
|
||||||
command: /garage key info {{ item.name }}
|
command: /garage key info {{ item.name }}
|
||||||
loop: "{{ garage_s3_keys }}"
|
loop: "{{ garage_s3_keys }}"
|
||||||
register: _key_info_results
|
register: _key_info_results
|
||||||
|
changed_when: false
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Extract key IDs from info
|
- name: Extract key IDs from info
|
||||||
|
|
@ -42,11 +49,21 @@
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage bucket list
|
command: /garage bucket list
|
||||||
register: _existing_buckets_output
|
register: _existing_buckets_output
|
||||||
|
changed_when: false
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Parse existing bucket names
|
- name: Parse existing bucket names
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
_existing_buckets: "{{ _existing_buckets_output.stdout_lines[2:] | map('split') | map('first') | list }}"
|
# `garage bucket list` columns: ID Created Global aliases Local aliases
|
||||||
|
# Data rows start with a hex bucket ID; filter to those and take the
|
||||||
|
# third whitespace-separated field (the global alias = bucket name).
|
||||||
|
_existing_buckets: >-
|
||||||
|
{{
|
||||||
|
_existing_buckets_output.stdout_lines
|
||||||
|
| select('match', '^[0-9a-f]{16}\\s')
|
||||||
|
| map('regex_replace', '^\\S+\\s+\\S+\\s+(\\S+).*$', '\\1')
|
||||||
|
| list
|
||||||
|
}}
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Get unique bucket names
|
- name: Get unique bucket names
|
||||||
|
|
@ -64,12 +81,37 @@
|
||||||
- item not in _existing_buckets
|
- item not in _existing_buckets
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Get current bucket permissions
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ garage_service_name }}"
|
||||||
|
command: /garage bucket info {{ item.1.name }}
|
||||||
|
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.1.name }}"
|
||||||
|
register: _bucket_info_results
|
||||||
|
changed_when: false
|
||||||
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Set bucket permissions using key IDs
|
- name: Set bucket permissions using key IDs
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ garage_service_name }}"
|
container: "{{ garage_service_name }}"
|
||||||
command: /garage bucket allow {{ item.1.name }} {% for perm in item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.0.name] }}
|
command: /garage bucket allow {{ item.item.1.name }} {% for perm in item.item.1.permissions %}--{{ perm }} {% endfor %}--key {{ _key_id_map[item.item.0.name] }}
|
||||||
loop: "{{ garage_s3_keys | subelements('buckets', skip_missing=True) }}"
|
loop: "{{ _bucket_info_results.results }}"
|
||||||
when: garage_s3_keys | length > 0
|
loop_control:
|
||||||
|
label: "{{ item.item.1.name }} -> {{ item.item.0.name }}"
|
||||||
|
when:
|
||||||
|
- garage_s3_keys | length > 0
|
||||||
|
- >-
|
||||||
|
(item.stdout | regex_search(
|
||||||
|
'(?m)^\s*' ~ _wanted_flags ~ '\s+' ~ _key_id_map[item.item.0.name]
|
||||||
|
)) is none
|
||||||
|
vars:
|
||||||
|
_wanted_flags: >-
|
||||||
|
{{
|
||||||
|
('R' if 'read' in item.item.1.permissions else '-')
|
||||||
|
~ ('W' if 'write' in item.item.1.permissions else '-')
|
||||||
|
~ ('O' if 'owner' in item.item.1.permissions else '-')
|
||||||
|
}}
|
||||||
|
|
||||||
# Export key credentials for use by other roles
|
# Export key credentials for use by other roles
|
||||||
- name: Get detailed key information for all keys
|
- name: Get detailed key information for all keys
|
||||||
|
|
@ -78,6 +120,7 @@
|
||||||
command: /garage key info {{ item.name }} --show-secret
|
command: /garage key info {{ item.name }} --show-secret
|
||||||
loop: "{{ garage_s3_keys }}"
|
loop: "{{ garage_s3_keys }}"
|
||||||
register: _key_details_results
|
register: _key_details_results
|
||||||
|
changed_when: false
|
||||||
when: garage_s3_keys | length > 0
|
when: garage_s3_keys | length > 0
|
||||||
|
|
||||||
- name: Build garage S3 credentials map
|
- name: Build garage S3 credentials map
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,13 @@ services:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ garage_traefik_network }}
|
- traefik.docker.network={{ garage_traefik_network }}
|
||||||
# S3 API endpoint
|
# S3 API endpoint
|
||||||
- traefik.http.routers.{{ garage_service_name }}.rule=Host(`{{ garage_s3_domain }}`)
|
- traefik.http.routers.{{ garage_service_name }}.rule={% for d in garage_s3_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
{% if garage_use_ssl %}
|
{% if garage_use_ssl %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ garage_service_name }}.tls=true
|
- traefik.http.routers.{{ garage_service_name }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ garage_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ garage_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -35,7 +38,9 @@ services:
|
||||||
environment:
|
environment:
|
||||||
API_BASE_URL: "http://{{ garage_service_name }}:{{ garage_admin_port }}"
|
API_BASE_URL: "http://{{ garage_service_name }}:{{ garage_admin_port }}"
|
||||||
S3_ENDPOINT_URL: "http://{{ garage_service_name }}:{{ garage_s3_api_port }}"
|
S3_ENDPOINT_URL: "http://{{ garage_service_name }}:{{ garage_s3_api_port }}"
|
||||||
|
{% if not (garage_webui_authentik_forward_auth | default(false)) %}
|
||||||
AUTH_USER_PASS: '{{ _garage_webui_password_hash.stdout | replace("$", "$$") }}'
|
AUTH_USER_PASS: '{{ _garage_webui_password_hash.stdout | replace("$", "$$") }}'
|
||||||
|
{% endif %}
|
||||||
volumes:
|
volumes:
|
||||||
- {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml:ro
|
- {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml:ro
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -48,12 +53,25 @@ services:
|
||||||
{% if garage_use_ssl %}
|
{% if garage_use_ssl %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
|
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
|
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ garage_service_name }}-console.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web
|
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.service={{ garage_service_name }}-console
|
- traefik.http.routers.{{ garage_service_name }}-console.service={{ garage_service_name }}-console
|
||||||
- traefik.http.routers.{{ garage_service_name }}-console.priority=10
|
- traefik.http.routers.{{ garage_service_name }}-console.priority=10
|
||||||
- traefik.http.services.{{ garage_service_name }}-console.loadbalancer.server.port={{ garage_webui_port }}
|
- traefik.http.services.{{ garage_service_name }}-console.loadbalancer.server.port={{ garage_webui_port }}
|
||||||
|
{% if garage_webui_authentik_forward_auth | default(false) %}
|
||||||
|
# ForwardAuth via the authentik embedded outpost. Unauthenticated
|
||||||
|
# requests are redirected to authentik; authentik then forwards
|
||||||
|
# X-Authentik-* identity headers downstream. htpasswd is disabled
|
||||||
|
# in the env block above so authentik is the only gate.
|
||||||
|
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.address={{ garage_webui_authentik_forward_auth_url }}
|
||||||
|
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.trustForwardHeader=true
|
||||||
|
- traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,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
|
||||||
|
- traefik.http.routers.{{ garage_service_name }}-console.middlewares={{ garage_service_name }}-console-authentik
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ rpc_secret = "{{ garage_rpc_secret }}"
|
||||||
[s3_api]
|
[s3_api]
|
||||||
s3_region = "{{ garage_s3_region }}"
|
s3_region = "{{ garage_s3_region }}"
|
||||||
api_bind_addr = "[::]:{{ garage_s3_api_port }}"
|
api_bind_addr = "[::]:{{ garage_s3_api_port }}"
|
||||||
root_domain = ".s3.{{ garage_s3_domain }}"
|
root_domain = ".s3.{{ garage_s3_domains[0] }}"
|
||||||
|
|
||||||
[s3_web]
|
[s3_web]
|
||||||
bind_addr = "[::]:{{ garage_s3_web_port }}"
|
bind_addr = "[::]:{{ garage_s3_web_port }}"
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,10 @@ secrets to version control.**
|
||||||
| `homarr_admin_password` | strong password | `openssl rand -base64 24` |
|
| `homarr_admin_password` | strong password | `openssl rand -base64 24` |
|
||||||
| `homarr_oidc_client_secret` | from your identity provider | — |
|
| `homarr_oidc_client_secret` | from your identity provider | — |
|
||||||
|
|
||||||
The `assert` task at the top of the role will fail fast if the encryption
|
`homarr_oidc_client_secret` is only required when `oidc` is in
|
||||||
key is missing or malformed.
|
`homarr_auth_providers`; the role asserts it then. The encryption key is
|
||||||
|
always required — the `assert` task at the top of the role fails fast if it
|
||||||
|
is missing or malformed.
|
||||||
|
|
||||||
## Configurable variables
|
## Configurable variables
|
||||||
|
|
||||||
|
|
@ -46,6 +48,8 @@ See `defaults/main.yml` for the full list. Most useful overrides:
|
||||||
| Variable | Default | Purpose |
|
| Variable | Default | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `homarr_domain` | `homarr.local.test` | Traefik Host rule |
|
| `homarr_domain` | `homarr.local.test` | Traefik Host rule |
|
||||||
|
| `homarr_extra_domains` | `[]` | Extra Host-rule hostnames (OR-combined), e.g. internal `*.int.*` FQDN |
|
||||||
|
| `homarr_extra_hosts` | `[]` | Container `/etc/hosts` overrides (`host:ip`) — pin IdP FQDN to LAN IP |
|
||||||
| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL |
|
| `homarr_base_url` | `https://home.local.test` | NEXTAUTH_URL / BASE_URL |
|
||||||
| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both |
|
| `homarr_auth_providers` | `credentials` | `credentials`, `oidc`, or both |
|
||||||
| `homarr_oidc_issuer` | empty | Identity provider issuer URL |
|
| `homarr_oidc_issuer` | empty | Identity provider issuer URL |
|
||||||
|
|
@ -112,7 +116,7 @@ The filter is invoked once from `tasks/main.yml`:
|
||||||
```yaml
|
```yaml
|
||||||
- name: Compute Homarr app layouts
|
- name: Compute Homarr app layouts
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
homarr_layout: "{{ homarr_apps | homarr_compute_layouts }}"
|
homarr_layout: "{{ homarr_apps | digitalboard.core.homarr_compute_layouts }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
This produces a `homarr_layout` fact with two keys, both consumed by
|
This produces a `homarr_layout` fact with two keys, both consumed by
|
||||||
|
|
@ -120,14 +124,14 @@ This produces a `homarr_layout` fact with two keys, both consumed by
|
||||||
|
|
||||||
| Key | Shape | Purpose |
|
| Key | Shape | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `apps` | list, same order as `homarr_apps` | each entry enriched with `desktop`, `tablet`, `mobile` sub-dicts of `{x, y, w, h}` |
|
| `apps` | list, same order as `homarr_apps` | each entry gains `desktop`/`tablet`/`mobile` dicts of `{x, y, w, h}` |
|
||||||
| `section_height` | dict with `desktop`, `tablet`, `mobile` | minimum height of the parent section so all tiles fit |
|
| `section_height` | dict with `desktop`, `tablet`, `mobile` | minimum height of the parent section so all tiles fit |
|
||||||
|
|
||||||
The filter signature accepts custom column counts if Homarr ever
|
The filter signature accepts custom column counts if Homarr ever
|
||||||
changes the breakpoint widths:
|
changes the breakpoint widths:
|
||||||
|
|
||||||
```jinja
|
```jinja
|
||||||
{{ homarr_apps | homarr_compute_layouts(desktop_cols=12, tablet_cols=8, mobile_cols=4) }}
|
{{ homarr_apps | digitalboard.core.homarr_compute_layouts(desktop_cols=12, tablet_cols=8, mobile_cols=4) }}
|
||||||
```
|
```
|
||||||
|
|
||||||
To debug a layout without running the full deploy, run the play with
|
To debug a layout without running the full deploy, run the play with
|
||||||
|
|
@ -240,4 +244,8 @@ and lowercase are accepted.
|
||||||
|
|
||||||
**App tiles overlap.** Check `homarr_apps` for duplicate `id` values.
|
**App tiles overlap.** Check `homarr_apps` for duplicate `id` values.
|
||||||
The role validates this, but if you bypass the check, the seed will
|
The role validates this, but if you bypass the check, the seed will
|
||||||
still run and Homarr will display only one of the duplicates.
|
still run and Homarr will display only one of the duplicates.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,14 @@ homarr_db: "{{ homarr_appdata_dir }}/db/db.sqlite"
|
||||||
|
|
||||||
# Service configuration
|
# Service configuration
|
||||||
homarr_domain: "homarr.local.test"
|
homarr_domain: "homarr.local.test"
|
||||||
|
# Additional hostnames the homarr router answers on (e.g. an internal
|
||||||
|
# *.int.* FQDN so a DMZ reverseproxy can hit a backend hostname covered
|
||||||
|
# by the cert).
|
||||||
|
homarr_extra_domains: []
|
||||||
|
# Extra /etc/hosts entries inside the homarr container (format "host:ip").
|
||||||
|
# Used to pin the IdP's public FQDN to a LAN IP so OIDC discovery stays
|
||||||
|
# in-network while the issuer URL matches what browsers see.
|
||||||
|
homarr_extra_hosts: []
|
||||||
homarr_image: "ghcr.io/homarr-labs/homarr:latest"
|
homarr_image: "ghcr.io/homarr-labs/homarr:latest"
|
||||||
homarr_port: 7575
|
homarr_port: 7575
|
||||||
homarr_use_docker: false
|
homarr_use_docker: false
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart homarr
|
- name: restart homarr
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ homarr_docker_compose_dir }}"
|
project_src: "{{ homarr_docker_compose_dir }}"
|
||||||
state: restarted
|
state: present
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy the Homarr dashboard via Docker Compose behind Traefik, with seeded admin user and OIDC group
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.2
|
galaxy_tags:
|
||||||
|
- homarr
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- dashboard
|
||||||
# min_ansible_container_version:
|
- oidc
|
||||||
|
- docker
|
||||||
galaxy_tags: []
|
- traefik
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
@ -112,19 +112,17 @@
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|
||||||
- name: Generate bcrypt hash for admin password
|
- name: Generate bcrypt hash for admin password
|
||||||
ansible.builtin.shell:
|
|
||||||
cmd: python3 -c "import bcrypt, sys; print(bcrypt.hashpw(sys.stdin.read().encode(), bcrypt.gensalt(rounds=10)).decode())"
|
|
||||||
stdin: "{{ homarr_admin_password }}"
|
|
||||||
stdin_add_newline: false
|
|
||||||
delegate_to: localhost
|
|
||||||
become: false
|
|
||||||
register: bcrypt_result
|
|
||||||
changed_when: false
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Set bcrypt hash fact
|
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
homarr_bcrypt_hash: "{{ bcrypt_result.stdout }}"
|
# Deterministic salt derived from the password's SHA-256 digest so the
|
||||||
|
# hash stays stable across runs (idempotent — no spurious template
|
||||||
|
# changes / container restarts when the password is unchanged). The
|
||||||
|
# bcrypt salt alphabet is [./A-Za-z0-9]; the digest's hex chars are
|
||||||
|
# a strict subset, so we just take the first 22.
|
||||||
|
homarr_bcrypt_hash: >-
|
||||||
|
{{ homarr_admin_password
|
||||||
|
| password_hash('bcrypt', rounds=10,
|
||||||
|
salt=(homarr_admin_password
|
||||||
|
| hash('sha256'))[:22]) }}
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|
@ -161,4 +159,4 @@
|
||||||
register: seed_result
|
register: seed_result
|
||||||
changed_when: seed_result.rc == 0
|
changed_when: seed_result.rc == 0
|
||||||
when: admin_exists.stdout == ""
|
when: admin_exists.stdout == ""
|
||||||
notify: restart homarr
|
notify: restart homarr
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,22 @@ services:
|
||||||
AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}"
|
AUTH_OIDC_AUTO_LOGIN: "{{ homarr_oidc_auto_login | default('false') }}"
|
||||||
networks:
|
networks:
|
||||||
- {{ homarr_traefik_network }}
|
- {{ homarr_traefik_network }}
|
||||||
|
{% if homarr_extra_hosts | default([]) | length > 0 %}
|
||||||
|
extra_hosts:
|
||||||
|
{% for h in homarr_extra_hosts %}
|
||||||
|
- "{{ h }}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ homarr_traefik_network }}
|
- traefik.docker.network={{ homarr_traefik_network }}
|
||||||
- traefik.http.routers.homarr.rule=Host(`{{ homarr_domain }}`)
|
- traefik.http.routers.homarr.rule={% set _all_domains = [homarr_domain] + (homarr_extra_domains | default([])) %}{% for d in _all_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
{% if homarr_use_ssl %}
|
{% if homarr_use_ssl %}
|
||||||
- traefik.http.routers.homarr.entrypoints=websecure
|
- traefik.http.routers.homarr.entrypoints=websecure
|
||||||
- traefik.http.routers.homarr.tls=true
|
- traefik.http.routers.homarr.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.homarr.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.homarr.entrypoints=web
|
- traefik.http.routers.homarr.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,30 @@
|
||||||
Role Name
|
# httpbin
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploys [httpbin](https://httpbin.org/) (`kennethreitz/httpbin`) via
|
||||||
|
Docker Compose behind Traefik. Useful as a throwaway endpoint to verify
|
||||||
|
that the Traefik ingress path, TLS and routing work end to end.
|
||||||
|
|
||||||
Requirements
|
## Role variables
|
||||||
------------
|
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `httpbin_domain` | `httpbin.local.test` | FQDN the Traefik router matches. |
|
||||||
|
| `httpbin_image` | `kennethreitz/httpbin` | Container image. |
|
||||||
|
| `httpbin_port` | `80` | Container port Traefik forwards to. |
|
||||||
|
| `httpbin_traefik_network` | `proxy` | Docker network shared with Traefik. |
|
||||||
|
| `httpbin_use_ssl` | `true` | Route via the `websecure` entrypoint with `tls=true` (otherwise `web`). |
|
||||||
|
|
||||||
Role Variables
|
## Example
|
||||||
--------------
|
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
```yaml
|
||||||
|
- hosts: services
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.httpbin
|
||||||
|
vars:
|
||||||
|
httpbin_domain: "httpbin.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
Dependencies
|
## License
|
||||||
------------
|
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
MIT-0
|
||||||
|
|
||||||
Example Playbook
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
|
||||||
|
|
||||||
- hosts: servers
|
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy httpbin HTTP request/response testing service via Docker Compose behind Traefik
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
galaxy_tags:
|
||||||
|
- httpbin
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- testing
|
||||||
# min_ansible_container_version:
|
- debug
|
||||||
|
- docker
|
||||||
galaxy_tags: []
|
- traefik
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,119 @@
|
||||||
Keycloak
|
# Keycloak
|
||||||
=========
|
|
||||||
|
|
||||||
Ansible role to deploy Keycloak with PostgreSQL database using Docker Compose.
|
Ansible role to deploy Keycloak with a PostgreSQL backend via Docker
|
||||||
|
Compose, published behind Traefik. Optionally provisions realm resources
|
||||||
|
(groups, users, OIDC clients, identity providers, LDAP user federations)
|
||||||
|
through the `community.general` Keycloak modules.
|
||||||
|
|
||||||
Requirements
|
## Requirements
|
||||||
------------
|
|
||||||
|
|
||||||
- Docker and Docker Compose installed on the target host
|
- Docker and Docker Compose on the target host (e.g. via
|
||||||
- Ansible collection: `community.docker`
|
`digitalboard.core.base`)
|
||||||
- Traefik reverse proxy (for external access)
|
- Ansible collections: `community.docker`, and `community.general` when
|
||||||
|
`keycloak_provisioning_enabled` is true
|
||||||
|
- Traefik reverse proxy with the `proxy` network already created (for
|
||||||
|
external access)
|
||||||
|
|
||||||
Role Variables
|
## Role variables
|
||||||
--------------
|
|
||||||
|
|
||||||
Key variables defined in `defaults/main.yml`:
|
Key variables from `defaults/main.yml`:
|
||||||
|
|
||||||
**Base Configuration:**
|
### Base configuration
|
||||||
- `docker_compose_base_dir`: Base directory for Docker Compose files (default: `/etc/docker/compose`)
|
|
||||||
- `docker_volume_base_dir`: Base directory for Docker volumes (default: `/srv/data`)
|
|
||||||
|
|
||||||
**Keycloak Configuration:**
|
| Variable | Default | Description |
|
||||||
- `keycloak_service_name`: Service name (default: `keycloak`)
|
| --- | --- | --- |
|
||||||
- `keycloak_domain`: Domain name for Keycloak (default: `auth.digitalboard.ch`)
|
| `docker_compose_base_dir` | `/etc/docker/compose` | Base dir for Compose projects. |
|
||||||
- `keycloak_image`: Keycloak Docker image (default: `quay.io/keycloak/keycloak:24.0.1`)
|
| `docker_volume_base_dir` | `/srv/data` | Base dir for persistent volumes. |
|
||||||
- `keycloak_port`: Internal Keycloak port (default: `8080`)
|
| `keycloak_service_name` | `keycloak` | Compose/service name; builds the per-service paths. |
|
||||||
- `keycloak_admin_user`: Admin username (default: `admin`)
|
|
||||||
- `keycloak_admin_password`: Admin password (default: `changeme`)
|
|
||||||
- `keycloak_log_level`: Log level (default: `INFO`)
|
|
||||||
- `keycloak_proxy_mode`: Proxy mode (default: `edge`)
|
|
||||||
|
|
||||||
**PostgreSQL Configuration:**
|
### Keycloak
|
||||||
- `keycloak_postgres_image`: PostgreSQL Docker image (default: `postgres:15`)
|
|
||||||
- `keycloak_postgres_db`: Database name (default: `keycloak`)
|
|
||||||
- `keycloak_postgres_user`: Database user (default: `keycloak`)
|
|
||||||
- `keycloak_postgres_password`: Database password (default: `changeme`)
|
|
||||||
|
|
||||||
**Traefik Configuration:**
|
| Variable | Default | Description |
|
||||||
- `keycloak_traefik_network`: Traefik network name (default: `proxy`)
|
| --- | --- | --- |
|
||||||
- `keycloak_backend_network`: Backend network name (default: `backend`)
|
| `keycloak_domain` | `keycloak.local.test` | Host rule and `KC_HOSTNAME`. |
|
||||||
- `keycloak_use_ssl`: Enable SSL (default: `true`)
|
| `keycloak_image` | `quay.io/keycloak/keycloak:24.0.1` | Keycloak image. |
|
||||||
- `keycloak_cert_resolver`: Certificate resolver name (default: `dns`)
|
| `keycloak_port` | `8080` | Internal HTTP port advertised to Traefik. |
|
||||||
|
| `keycloak_admin_user` | `admin` | Bootstrap admin user. |
|
||||||
|
| `keycloak_admin_password` | `changeme` | Admin password — **override this**. |
|
||||||
|
| `keycloak_log_level` | `INFO` | `KC_LOG_LEVEL`. |
|
||||||
|
| `keycloak_proxy_mode` | `edge` | `KC_PROXY` mode. |
|
||||||
|
| `keycloak_gzip_enabled` | `false` | Toggle Keycloak GZIP response encoding. |
|
||||||
|
| `keycloak_truststore_certificates` | `[]` | Host PEM paths mounted into the truststore (`KC_TRUSTSTORE_PATHS`). |
|
||||||
|
| `keycloak_extra_hosts` | `[]` | Extra `host:ip` entries for the container. |
|
||||||
|
|
||||||
Dependencies
|
### PostgreSQL
|
||||||
------------
|
|
||||||
|
|
||||||
This role requires the Traefik reverse proxy to be configured and the `proxy` network to be created.
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `keycloak_postgres_image` | `postgres:15` | PostgreSQL image. |
|
||||||
|
| `keycloak_postgres_db` | `keycloak` | Database name. |
|
||||||
|
| `keycloak_postgres_user` | `keycloak` | Database user. |
|
||||||
|
| `keycloak_postgres_password` | `changeme` | Database password — **override this**. |
|
||||||
|
|
||||||
Example Playbook
|
### Traefik
|
||||||
----------------
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `keycloak_traefik_network` | `proxy` | External Traefik network. |
|
||||||
|
| `keycloak_backend_network` | `backend` | Internal network to PostgreSQL. |
|
||||||
|
| `keycloak_use_ssl` | `true` | Route on `websecure` with `tls=true` instead of `web`. |
|
||||||
|
|
||||||
|
TLS is requested from Traefik via `tls=true`; the role does not set a
|
||||||
|
certificate resolver, so Traefik issues/serves the certificate according
|
||||||
|
to its own configuration.
|
||||||
|
|
||||||
|
### Provisioning (optional)
|
||||||
|
|
||||||
|
Provisioning runs only when `keycloak_provisioning_enabled` is true. The
|
||||||
|
tasks wait for the `/health/ready` endpoint and then call the
|
||||||
|
`community.general.keycloak_*` modules, delegated to `localhost` against
|
||||||
|
`keycloak_auth_url` (derived from `keycloak_use_ssl` + `keycloak_domain`).
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `keycloak_provisioning_enabled` | `false` | Enable realm provisioning. |
|
||||||
|
| `keycloak_realm` | `default` | Target realm; created unless `master`. |
|
||||||
|
| `keycloak_realm_display_name` | `Default Realm` | Realm display name. |
|
||||||
|
| `keycloak_auth_url` | derived | API base URL for provisioning. |
|
||||||
|
| `keycloak_groups` | `[]` | Groups to create. |
|
||||||
|
| `keycloak_local_users` | `[]` | Local users to create. |
|
||||||
|
| `keycloak_oidc_clients` | `[]` | OIDC clients to create. |
|
||||||
|
| `keycloak_identity_providers` | `[]` | Identity providers (e.g. Entra ID). |
|
||||||
|
| `keycloak_user_federations` | `[]` | LDAP user federations. |
|
||||||
|
| `keycloak_removed_users` | `[]` | Usernames to delete. |
|
||||||
|
| `keycloak_removed_groups` | `[]` | Group names to delete. |
|
||||||
|
| `keycloak_removed_clients` | `[]` | Client IDs to delete. |
|
||||||
|
| `keycloak_removed_identity_providers` | `[]` | IdP aliases to delete. |
|
||||||
|
| `keycloak_removed_user_federations` | `[]` | Federation names to delete. |
|
||||||
|
|
||||||
|
See `defaults/main.yml` for the full entry shape of each list.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This role requires the Traefik reverse proxy to be configured and the
|
||||||
|
`proxy` network to be created beforehand (it is referenced as an external
|
||||||
|
network in the Compose file). The `backend` network is created by the
|
||||||
|
Compose project itself.
|
||||||
|
|
||||||
|
## Example playbook
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- hosts: backend_servers
|
- hosts: backend_servers
|
||||||
roles:
|
roles:
|
||||||
- role: keycloak
|
- role: digitalboard.core.keycloak
|
||||||
vars:
|
vars:
|
||||||
keycloak_domain: "auth.example.com"
|
keycloak_domain: "auth.example.com"
|
||||||
keycloak_admin_password: "secure_password"
|
keycloak_admin_password: "{{ vault_keycloak_admin_password }}"
|
||||||
keycloak_postgres_password: "secure_db_password"
|
keycloak_postgres_password: "{{ vault_keycloak_pg_password }}"
|
||||||
|
keycloak_provisioning_enabled: true
|
||||||
|
keycloak_oidc_clients:
|
||||||
|
- client_id: nextcloud
|
||||||
|
name: "Nextcloud"
|
||||||
|
client_secret: "{{ vault_nextcloud_client_secret }}"
|
||||||
|
redirect_uris:
|
||||||
|
- "https://nextcloud.example.com/apps/user_oidc/code"
|
||||||
```
|
```
|
||||||
|
|
||||||
License
|
## License
|
||||||
-------
|
|
||||||
|
|
||||||
MIT-0
|
MIT-0
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,27 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: your name
|
author: digitalboard
|
||||||
description: your role description
|
description: Deploy Keycloak with a PostgreSQL backend via Docker Compose behind Traefik
|
||||||
company: your company (optional)
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
# If the issue tracker for your role is not on github, uncomment the
|
min_ansible_version: "2.14"
|
||||||
# next line and provide a value
|
|
||||||
# issue_tracker_url: http://example.com/issue/tracker
|
|
||||||
|
|
||||||
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
platforms:
|
||||||
# - BSD-3-Clause (default)
|
- name: Debian
|
||||||
# - MIT
|
versions:
|
||||||
# - GPL-2.0-or-later
|
- bookworm
|
||||||
# - GPL-3.0-only
|
- name: Ubuntu
|
||||||
# - Apache-2.0
|
versions:
|
||||||
# - CC-BY-4.0
|
- jammy
|
||||||
license: license (GPL-2.0-or-later, MIT, etc)
|
- noble
|
||||||
|
|
||||||
min_ansible_version: 2.1
|
galaxy_tags:
|
||||||
|
- keycloak
|
||||||
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
- oidc
|
||||||
# min_ansible_container_version:
|
- sso
|
||||||
|
- docker
|
||||||
galaxy_tags: []
|
- traefik
|
||||||
# List tags for your role here, one per line. A tag is a keyword that describes
|
- digitalboard
|
||||||
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
|
||||||
# remove the '[]' above, if you add tags to this list.
|
|
||||||
#
|
|
||||||
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
|
||||||
# Maximum 20 tags per role.
|
|
||||||
|
|
||||||
dependencies: []
|
dependencies: []
|
||||||
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
|
||||||
# if you add dependencies to this list.
|
|
||||||
|
|
|
||||||
124
roles/nextcloud/README.md
Normal file
124
roles/nextcloud/README.md
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Nextcloud
|
||||||
|
|
||||||
|
Ansible role to deploy [Nextcloud](https://nextcloud.com/) (fpm) with
|
||||||
|
Postgres and Redis via Docker Compose, optional Collabora WOPI
|
||||||
|
integration, optional draw.io integration, optional notify_push
|
||||||
|
companion, optional S3 primary storage, plus OIDC and LDAP user
|
||||||
|
backends.
|
||||||
|
|
||||||
|
## What this role does
|
||||||
|
|
||||||
|
- Renders the Compose stack with traefik labels and TLS
|
||||||
|
- Installs and enables a configurable list of Nextcloud apps idempotently
|
||||||
|
- Configures Collabora (richdocuments), draw.io, OIDC providers and
|
||||||
|
LDAP via `occ` — every setting is read first and only written when
|
||||||
|
the stored value differs, so re-runs don't churn
|
||||||
|
- Sets up notify_push (when enabled)
|
||||||
|
- Applies an in-container PHP source workaround for the upstream
|
||||||
|
`UserConfig::getValueBool` TypeError (nextcloud/server#59629, fixed in
|
||||||
|
master via PR #59646 with no stable33 backport before 33.0.4).
|
||||||
|
Idempotent via grep guard; remove the patch task once
|
||||||
|
`nextcloud_image` is >= 33.0.4.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed on the target host
|
||||||
|
- Ansible collection: `community.docker`
|
||||||
|
- Traefik with a shared `nextcloud_traefik_network` (default `proxy`)
|
||||||
|
|
||||||
|
## Role variables
|
||||||
|
|
||||||
|
Full spec with types and defaults: `meta/argument_specs.yml`. The most
|
||||||
|
common overrides:
|
||||||
|
|
||||||
|
### Service
|
||||||
|
|
||||||
|
- `nextcloud_domains`: FQDNs the router accepts. First entry is the
|
||||||
|
canonical hostname (used for `OVERWRITEHOST` and notify_push setup).
|
||||||
|
Further entries cover internal `*.int.*` names so Collabora's WOPI
|
||||||
|
callback hits the instance on a name with a valid cert.
|
||||||
|
- `nextcloud_admin_password`, `nextcloud_postgres_password` (required).
|
||||||
|
- `nextcloud_memory_limit_mb`, `nextcloud_upload_limit_mb`.
|
||||||
|
|
||||||
|
### Collabora
|
||||||
|
|
||||||
|
- `nextcloud_enable_collabora`: toggle integration with a separately
|
||||||
|
deployed Collabora server (see the `collabora` role).
|
||||||
|
- `nextcloud_collabora_domain`: server-to-server hostname.
|
||||||
|
- `nextcloud_collabora_public_domain` (optional): browser-facing
|
||||||
|
hostname when split-horizon uses different names.
|
||||||
|
|
||||||
|
### Draw.io
|
||||||
|
|
||||||
|
- `nextcloud_enable_drawio`: enable the `integration_drawio` app.
|
||||||
|
- `nextcloud_drawio_url`: public draw.io URL.
|
||||||
|
- `nextcloud_drawio_theme`, `nextcloud_drawio_offline`.
|
||||||
|
|
||||||
|
### Notify push
|
||||||
|
|
||||||
|
- `nextcloud_enable_notify_push`: deploy the notify_push companion.
|
||||||
|
- `nextcloud_notify_push_domain` (optional): override the hostname
|
||||||
|
used by `occ notify_push:setup` to avoid hairpinning through the DMZ.
|
||||||
|
|
||||||
|
### S3 primary storage
|
||||||
|
|
||||||
|
Set `nextcloud_use_s3_storage: true` plus the `nextcloud_s3_*` block to
|
||||||
|
point Nextcloud at an external S3-compatible store (e.g. Garage, MinIO).
|
||||||
|
|
||||||
|
### OIDC
|
||||||
|
|
||||||
|
`nextcloud_oidc_providers` is a list of OIDC providers registered with
|
||||||
|
`user_oidc`. Required fields per entry: `identifier`, `display_name`,
|
||||||
|
`client_id`, `client_secret`, `discovery_url`.
|
||||||
|
|
||||||
|
### LDAP
|
||||||
|
|
||||||
|
Set `nextcloud_ldap_enabled: true` and provide `nextcloud_ldap_config`
|
||||||
|
as a dict of `occ ldap:set-config s01 KEY VALUE` pairs. The role reads
|
||||||
|
the current LDAP config via `occ ldap:show-config s01 --output=json`
|
||||||
|
and only calls `ldap:set-config` for keys whose stored value differs.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Traefik network (`nextcloud_traefik_network`, default `proxy`)
|
||||||
|
- Optional: `collabora`, `drawio`, `garage` roles for the corresponding
|
||||||
|
integrations
|
||||||
|
- Optional: an OIDC provider (Keycloak, authentik) reachable from
|
||||||
|
Nextcloud and a 389ds LDAP server when using `user_ldap`
|
||||||
|
|
||||||
|
## Example playbook
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- hosts: app_servers
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.nextcloud
|
||||||
|
vars:
|
||||||
|
nextcloud_domains:
|
||||||
|
- "cloud.example.com"
|
||||||
|
- "cloud.int.example.com"
|
||||||
|
nextcloud_admin_password: "{{ vault_nextcloud_admin_password }}"
|
||||||
|
nextcloud_postgres_password: "{{ vault_nextcloud_pg_password }}"
|
||||||
|
|
||||||
|
nextcloud_enable_collabora: true
|
||||||
|
nextcloud_collabora_domain: "office.int.example.com"
|
||||||
|
nextcloud_collabora_public_domain: "office.example.com"
|
||||||
|
|
||||||
|
nextcloud_enable_notify_push: true
|
||||||
|
nextcloud_notify_push_domain: "cloud.int.example.com"
|
||||||
|
|
||||||
|
nextcloud_oidc_providers:
|
||||||
|
- identifier: authentik
|
||||||
|
display_name: "Login with Authentik"
|
||||||
|
client_id: nextcloud
|
||||||
|
client_secret: "{{ vault_nextcloud_oidc_secret }}"
|
||||||
|
discovery_url: "https://auth.example.com/application/o/nextcloud/.well-known/openid-configuration"
|
||||||
|
mapping:
|
||||||
|
uid: preferred_username
|
||||||
|
display_name: name
|
||||||
|
email: email
|
||||||
|
groups: groups
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT-0
|
||||||
|
|
@ -9,7 +9,12 @@ nextcloud_service_name: nextcloud
|
||||||
nextcloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ nextcloud_service_name }}"
|
nextcloud_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ nextcloud_service_name }}"
|
||||||
nextcloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ nextcloud_service_name }}"
|
nextcloud_docker_volume_dir: "{{ docker_volume_base_dir }}/{{ nextcloud_service_name }}"
|
||||||
|
|
||||||
nextcloud_domain: "nextcloud.local.test"
|
# FQDNs the nextcloud router accepts. The first entry is the canonical
|
||||||
|
# domain (used for OVERWRITEHOST and the notify_push setup); further
|
||||||
|
# entries cover internal *.int.* names so collabora's WOPI callback
|
||||||
|
# hits us on a name with a valid cert.
|
||||||
|
nextcloud_domains:
|
||||||
|
- "nextcloud.local.test"
|
||||||
nextcloud_image: "nextcloud:fpm"
|
nextcloud_image: "nextcloud:fpm"
|
||||||
nextcloud_redis_image: "redis:latest"
|
nextcloud_redis_image: "redis:latest"
|
||||||
nextcloud_port: 80
|
nextcloud_port: 80
|
||||||
|
|
@ -60,6 +65,12 @@ nextcloud_trusted_proxies: "172.16.0.0/12"
|
||||||
# File locking and real-time push notifications
|
# File locking and real-time push notifications
|
||||||
nextcloud_enable_notify_push: false
|
nextcloud_enable_notify_push: false
|
||||||
nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1"
|
nextcloud_notify_push_image: "icewind1991/notify_push:1.3.1"
|
||||||
|
# Domain used when calling `occ notify_push:setup`. Defaults to the
|
||||||
|
# first nextcloud_domains entry (the canonical public name). Override
|
||||||
|
# with an internal FQDN to avoid hairpinning the setup check through
|
||||||
|
# the DMZ; the FQDN must also be in nextcloud_domains so the push
|
||||||
|
# router matches it.
|
||||||
|
# nextcloud_notify_push_domain: "cloud.int.example.com"
|
||||||
|
|
||||||
# Nextcloud Talk: register external HPB signaling + TURN + STUN
|
# Nextcloud Talk: register external HPB signaling + TURN + STUN
|
||||||
# Set to true to run tasks/talk.yml after Nextcloud is up.
|
# Set to true to run tasks/talk.yml after Nextcloud is up.
|
||||||
|
|
|
||||||
253
roles/nextcloud/meta/argument_specs.yml
Normal file
253
roles/nextcloud/meta/argument_specs.yml
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
---
|
||||||
|
argument_specs:
|
||||||
|
main:
|
||||||
|
short_description: Deploy Nextcloud (fpm) + Redis + Postgres via Docker Compose.
|
||||||
|
description:
|
||||||
|
- Renders a Compose stack for Nextcloud with traefik labels, optional
|
||||||
|
Collabora WOPI integration, optional draw.io integration, optional
|
||||||
|
notify_push companion, optional S3 primary storage, OIDC providers
|
||||||
|
and LDAP user backend.
|
||||||
|
- "All C(occ)-driven configuration tasks are idempotent: each setting
|
||||||
|
is read with C(config:app:get) (or C(ldap:show-config)) first and
|
||||||
|
only written when the stored value differs."
|
||||||
|
options:
|
||||||
|
docker_compose_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /etc/docker/compose
|
||||||
|
docker_volume_base_dir:
|
||||||
|
type: path
|
||||||
|
default: /srv/data
|
||||||
|
nextcloud_service_name:
|
||||||
|
type: str
|
||||||
|
default: nextcloud
|
||||||
|
nextcloud_docker_compose_dir:
|
||||||
|
type: path
|
||||||
|
nextcloud_docker_volume_dir:
|
||||||
|
type: path
|
||||||
|
|
||||||
|
nextcloud_domains:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: ['nextcloud.local.test']
|
||||||
|
description:
|
||||||
|
- FQDNs the nextcloud router accepts. The first entry is the
|
||||||
|
canonical domain (used for C(OVERWRITEHOST) and the
|
||||||
|
C(notify_push) setup). Further entries cover internal C(*.int.*)
|
||||||
|
names so Collabora's WOPI callback hits the instance on a name
|
||||||
|
with a valid certificate.
|
||||||
|
nextcloud_image:
|
||||||
|
type: str
|
||||||
|
default: nextcloud:fpm
|
||||||
|
nextcloud_redis_image:
|
||||||
|
type: str
|
||||||
|
default: redis:latest
|
||||||
|
nextcloud_port:
|
||||||
|
type: int
|
||||||
|
default: 80
|
||||||
|
nextcloud_extra_hosts:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
nextcloud_extra_networks:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
nextcloud_allow_local_remote_servers:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description: Allow requests to local network from Nextcloud (dev only).
|
||||||
|
|
||||||
|
nextcloud_postgres_image:
|
||||||
|
type: str
|
||||||
|
default: postgres:15
|
||||||
|
nextcloud_postgres_db:
|
||||||
|
type: str
|
||||||
|
default: nextcloud
|
||||||
|
nextcloud_postgres_user:
|
||||||
|
type: str
|
||||||
|
default: nextcloud
|
||||||
|
nextcloud_postgres_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
|
||||||
|
nextcloud_backend_network:
|
||||||
|
type: str
|
||||||
|
default: nextcloud-internal
|
||||||
|
nextcloud_traefik_network:
|
||||||
|
type: str
|
||||||
|
default: proxy
|
||||||
|
nextcloud_use_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
|
||||||
|
nextcloud_enable_collabora:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
nextcloud_collabora_domain:
|
||||||
|
type: str
|
||||||
|
default: office.local.test
|
||||||
|
description: Hostname Nextcloud uses to talk to Collabora server-to-server.
|
||||||
|
nextcloud_collabora_public_domain:
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
- Optional browser-facing hostname for Collabora; defaults to
|
||||||
|
C(nextcloud_collabora_domain) when unset. Set when split-horizon
|
||||||
|
uses different names for browser and server traffic.
|
||||||
|
nextcloud_collabora_disable_cert_verification:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
|
||||||
|
nextcloud_enable_drawio:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description: Enable the integration_drawio Nextcloud app and configure the URL/theme.
|
||||||
|
nextcloud_drawio_url:
|
||||||
|
type: str
|
||||||
|
default: ''
|
||||||
|
description: Public draw.io URL used by the integration_drawio app.
|
||||||
|
nextcloud_drawio_theme:
|
||||||
|
type: str
|
||||||
|
choices: [kennedy, atlas, dark, sketch, min]
|
||||||
|
default: kennedy
|
||||||
|
nextcloud_drawio_offline:
|
||||||
|
type: str
|
||||||
|
choices: ['yes', 'no']
|
||||||
|
default: 'yes'
|
||||||
|
|
||||||
|
nextcloud_use_s3_storage:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
description: Use S3 primary object storage instead of the local data dir.
|
||||||
|
nextcloud_s3_key:
|
||||||
|
type: str
|
||||||
|
default: changeme
|
||||||
|
nextcloud_s3_secret:
|
||||||
|
type: str
|
||||||
|
default: changeme
|
||||||
|
nextcloud_s3_region:
|
||||||
|
type: str
|
||||||
|
default: us-east-1
|
||||||
|
nextcloud_s3_bucket:
|
||||||
|
type: str
|
||||||
|
default: nextcloud
|
||||||
|
nextcloud_s3_host:
|
||||||
|
type: str
|
||||||
|
default: s3.example.com
|
||||||
|
nextcloud_s3_port:
|
||||||
|
type: int
|
||||||
|
default: 443
|
||||||
|
nextcloud_s3_ssl:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
nextcloud_s3_usepath_style:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
nextcloud_s3_autocreate:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
|
||||||
|
nextcloud_admin_user:
|
||||||
|
type: str
|
||||||
|
default: admin
|
||||||
|
nextcloud_admin_password:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
nextcloud_memory_limit_mb:
|
||||||
|
type: int
|
||||||
|
default: 1024
|
||||||
|
nextcloud_upload_limit_mb:
|
||||||
|
type: int
|
||||||
|
default: 2048
|
||||||
|
nextcloud_scale_factor:
|
||||||
|
type: int
|
||||||
|
default: 2
|
||||||
|
|
||||||
|
nextcloud_trusted_proxies:
|
||||||
|
type: str
|
||||||
|
default: '172.16.0.0/12'
|
||||||
|
description: Trusted proxy CIDR(s) — by default the Docker internal range.
|
||||||
|
|
||||||
|
nextcloud_enable_notify_push:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
nextcloud_notify_push_image:
|
||||||
|
type: str
|
||||||
|
default: icewind1991/notify_push:1.3.1
|
||||||
|
nextcloud_notify_push_domain:
|
||||||
|
type: str
|
||||||
|
description:
|
||||||
|
- Hostname used when calling C(occ notify_push:setup). Defaults to
|
||||||
|
the first C(nextcloud_domains) entry. Override with an internal
|
||||||
|
FQDN to avoid hairpinning the setup check through the DMZ; the
|
||||||
|
FQDN must also be in C(nextcloud_domains).
|
||||||
|
|
||||||
|
nextcloud_apps_to_install:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default:
|
||||||
|
- groupfolders
|
||||||
|
- richdocuments
|
||||||
|
- spreed
|
||||||
|
- user_ldap
|
||||||
|
- user_oidc
|
||||||
|
- whiteboard
|
||||||
|
- files_lock
|
||||||
|
- notify_push
|
||||||
|
description:
|
||||||
|
- Non-default Nextcloud apps to install + enable.
|
||||||
|
Install/enable detection is idempotent — re-runs report C(ok)
|
||||||
|
when the app is already present and enabled.
|
||||||
|
|
||||||
|
nextcloud_oidc_allow_selfsigned:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
nextcloud_oidc_providers:
|
||||||
|
type: list
|
||||||
|
elements: dict
|
||||||
|
default: []
|
||||||
|
description: OIDC providers registered with the user_oidc app.
|
||||||
|
options:
|
||||||
|
identifier:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
display_name:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
client_id:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
client_secret:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
discovery_url:
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
scope:
|
||||||
|
type: str
|
||||||
|
default: openid email profile
|
||||||
|
unique_uid:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
check_bearer:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
send_id_token_hint:
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
mapping:
|
||||||
|
type: dict
|
||||||
|
nextcloud_oidc_providers_removed:
|
||||||
|
type: list
|
||||||
|
elements: str
|
||||||
|
default: []
|
||||||
|
|
||||||
|
nextcloud_ldap_enabled:
|
||||||
|
type: bool
|
||||||
|
default: false
|
||||||
|
nextcloud_ldap_config:
|
||||||
|
type: dict
|
||||||
|
default: {}
|
||||||
|
description:
|
||||||
|
- Key/value pairs passed to C(occ ldap:set-config s01 KEY VALUE).
|
||||||
|
The role reads the current config first and only invokes
|
||||||
|
C(set-config) when a stored value differs.
|
||||||
28
roles/nextcloud/meta/main.yml
Normal file
28
roles/nextcloud/meta/main.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#SPDX-License-Identifier: MIT-0
|
||||||
|
galaxy_info:
|
||||||
|
author: digitalboard
|
||||||
|
description: Deploy Nextcloud (fpm) + Redis + Postgres via Docker Compose behind Traefik
|
||||||
|
company: Digitalboard
|
||||||
|
license: MIT-0
|
||||||
|
|
||||||
|
min_ansible_version: "2.14"
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: Debian
|
||||||
|
versions:
|
||||||
|
- bookworm
|
||||||
|
- name: Ubuntu
|
||||||
|
versions:
|
||||||
|
- jammy
|
||||||
|
- noble
|
||||||
|
|
||||||
|
galaxy_tags:
|
||||||
|
- nextcloud
|
||||||
|
- files
|
||||||
|
- collabora
|
||||||
|
- oidc
|
||||||
|
- docker
|
||||||
|
- traefik
|
||||||
|
- digitalboard
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
|
|
@ -1,22 +1,55 @@
|
||||||
#SPDX-License-Identifier: MIT-0
|
#SPDX-License-Identifier: MIT-0
|
||||||
---
|
---
|
||||||
# tasks file for configuring Collabora in Nextcloud
|
# tasks file for configuring Collabora in Nextcloud
|
||||||
- name: Configure Collabora WOPI URL
|
- name: Read current richdocuments config values
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
|
command: php /var/www/html/occ config:app:get richdocuments {{ item }}
|
||||||
|
loop:
|
||||||
|
- wopi_url
|
||||||
|
- public_wopi_url
|
||||||
|
- disable_certificate_verification
|
||||||
|
- wopi_allowlist
|
||||||
|
register: _richdocuments_current
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Build map of current richdocuments config
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_richdocuments_cfg: "{{ _richdocuments_cfg | default({}) | combine({item.item: (item.stdout | default('')).strip()}) }}"
|
||||||
|
loop: "{{ _richdocuments_current.results }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item }}"
|
||||||
|
|
||||||
|
- name: Configure Collabora WOPI URL (server-to-server)
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments wopi_url --value=https://{{ nextcloud_collabora_domain }}
|
command: php /var/www/html/occ config:app:set richdocuments wopi_url --value=https://{{ nextcloud_collabora_domain }}
|
||||||
|
when: _richdocuments_cfg.wopi_url != ('https://' ~ nextcloud_collabora_domain)
|
||||||
|
|
||||||
|
- name: Configure Collabora public WOPI URL (browser-facing)
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
|
command: php /var/www/html/occ config:app:set richdocuments public_wopi_url --value=https://{{ nextcloud_collabora_public_domain }}
|
||||||
|
when:
|
||||||
|
- nextcloud_collabora_public_domain is defined
|
||||||
|
- nextcloud_collabora_public_domain != nextcloud_collabora_domain
|
||||||
|
- _richdocuments_cfg.public_wopi_url != ('https://' ~ nextcloud_collabora_public_domain)
|
||||||
|
|
||||||
- name: Configure certificate verification for Collabora
|
- name: Configure certificate verification for Collabora
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments disable_certificate_verification --value={{ nextcloud_collabora_disable_cert_verification | ternary('yes', 'no') }}
|
command: php /var/www/html/occ config:app:set richdocuments disable_certificate_verification --value={{ nextcloud_collabora_disable_cert_verification | ternary('yes', 'no') }}
|
||||||
|
when: _richdocuments_cfg.disable_certificate_verification != (nextcloud_collabora_disable_cert_verification | ternary('yes', 'no'))
|
||||||
|
|
||||||
- name: Set Collabora WOPI allowlist
|
- name: Set Collabora WOPI allowlist
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value=''
|
command: php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value=''
|
||||||
|
when: _richdocuments_cfg.wopi_allowlist | default('') != ''
|
||||||
|
|
||||||
- name: Activate richdocuments configuration (fetch discovery from Collabora)
|
- name: Activate richdocuments configuration (fetch discovery from Collabora)
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ richdocuments:activate-config
|
command: php /var/www/html/occ richdocuments:activate-config
|
||||||
|
changed_when: false
|
||||||
|
|
@ -2,18 +2,41 @@
|
||||||
---
|
---
|
||||||
# tasks file for configuring draw.io in Nextcloud
|
# tasks file for configuring draw.io in Nextcloud
|
||||||
|
|
||||||
|
- name: Read current drawio config values
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
|
command: php /var/www/html/occ config:app:get drawio {{ item }}
|
||||||
|
loop:
|
||||||
|
- DrawioUrl
|
||||||
|
- DrawioTheme
|
||||||
|
- DrawioOffline
|
||||||
|
register: _drawio_current
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Build map of current drawio config
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_drawio_cfg: "{{ _drawio_cfg | default({}) | combine({item.item: (item.stdout | default('')).strip()}) }}"
|
||||||
|
loop: "{{ _drawio_current.results }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item }}"
|
||||||
|
|
||||||
- name: Configure draw.io URL
|
- name: Configure draw.io URL
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioUrl --value={{ nextcloud_drawio_url }}
|
command: php /var/www/html/occ config:app:set drawio DrawioUrl --value={{ nextcloud_drawio_url }}
|
||||||
when: nextcloud_drawio_url | length > 0
|
when:
|
||||||
|
- nextcloud_drawio_url | length > 0
|
||||||
|
- _drawio_cfg.DrawioUrl != nextcloud_drawio_url
|
||||||
|
|
||||||
- name: Configure draw.io theme
|
- name: Configure draw.io theme
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioTheme --value={{ nextcloud_drawio_theme }}
|
command: php /var/www/html/occ config:app:set drawio DrawioTheme --value={{ nextcloud_drawio_theme }}
|
||||||
|
when: _drawio_cfg.DrawioTheme != (nextcloud_drawio_theme | string)
|
||||||
|
|
||||||
- name: Configure draw.io offline mode
|
- name: Configure draw.io offline mode
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ config:app:set drawio DrawioOffline --value={{ nextcloud_drawio_offline }}
|
command: php /var/www/html/occ config:app:set drawio DrawioOffline --value={{ nextcloud_drawio_offline }}
|
||||||
|
when: _drawio_cfg.DrawioOffline != (nextcloud_drawio_offline | string)
|
||||||
|
|
@ -15,6 +15,24 @@
|
||||||
command: php /var/www/html/occ ldap:create-empty-config
|
command: php /var/www/html/occ ldap:create-empty-config
|
||||||
when: "'s01' not in ldap_show_config.stdout"
|
when: "'s01' not in ldap_show_config.stdout"
|
||||||
|
|
||||||
|
- name: Read current LDAP config for s01
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
||||||
|
command: php /var/www/html/occ ldap:show-config s01 --output=json
|
||||||
|
register: _ldap_show_s01
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Parse current LDAP config
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
_ldap_current: >-
|
||||||
|
{{
|
||||||
|
(_ldap_show_s01.stdout | from_json) if (
|
||||||
|
(_ldap_show_s01.stdout | default('') | trim) is match('^[\\[{]')
|
||||||
|
) else {}
|
||||||
|
}}
|
||||||
|
when: _ldap_show_s01.rc | default(1) == 0
|
||||||
|
|
||||||
- name: Configure LDAP settings
|
- name: Configure LDAP settings
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
container: "{{ nextcloud_service_name }}-nextcloud-1"
|
||||||
|
|
@ -29,6 +47,7 @@
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.key }}"
|
label: "{{ item.key }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
when: ((_ldap_current | default({})).get(item.key) | default(none) | string) != (item.value | string)
|
||||||
|
|
||||||
- name: Test LDAP configuration
|
- name: Test LDAP configuration
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,61 @@
|
||||||
project_src: "{{ nextcloud_docker_compose_dir }}"
|
project_src: "{{ nextcloud_docker_compose_dir }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
# nextcloud/server#59629: UserConfig::getValueBool() passes a non-string from
|
||||||
|
# getTypedValue() into strtolower() under PHP 8.x + OPcache, throwing a
|
||||||
|
# TypeError on every authenticated request once user_ldap is involved. Fix
|
||||||
|
# is in master (PR #59646) but no stable33 backport landed before 33.0.4.
|
||||||
|
# Apply the (string) cast in-container; idempotent via grep guard. Remove
|
||||||
|
# this block once nextcloud_image >= 33.0.4.
|
||||||
|
- name: Discover nextcloud php containers needing the UserConfig patch
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: >-
|
||||||
|
docker ps --filter "label=com.docker.compose.project={{ nextcloud_docker_compose_dir | basename }}"
|
||||||
|
--filter "label=com.docker.compose.service=nextcloud"
|
||||||
|
--format '{% raw %}{{.Names}}{% endraw %}'
|
||||||
|
register: _nextcloud_php_containers
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Check UserConfig.php patch status per container
|
||||||
|
ansible.builtin.shell:
|
||||||
|
# rc 0 -> already patched; rc 1 -> still the unpatched original; rc 2 ->
|
||||||
|
# neither marker present (upstream drift -> the guard task below fails loud).
|
||||||
|
cmd: >-
|
||||||
|
docker exec {{ item }} sh -c '
|
||||||
|
grep -q "strtolower((string)\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 0;
|
||||||
|
grep -q "strtolower(\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 1;
|
||||||
|
exit 2'
|
||||||
|
loop: "{{ _nextcloud_php_containers.stdout_lines }}"
|
||||||
|
register: _nextcloud_userconfig_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Fail if the UserConfig.php source drifted from the expected upstream line
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: >-
|
||||||
|
Neither the patched nor the expected original strtolower($this->getTypedValue(...))
|
||||||
|
line was found in {{ item.item }}:/var/www/html/lib/private/Config/UserConfig.php.
|
||||||
|
The nextcloud/server#59629 workaround can no longer locate its target — the upstream
|
||||||
|
source likely changed. Re-verify whether the fix shipped (then drop this block) or
|
||||||
|
update the sed expression. Silently skipping would let the TypeError regress.
|
||||||
|
loop: "{{ _nextcloud_userconfig_check.results }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item }}"
|
||||||
|
when:
|
||||||
|
- item.rc | default(2) == 2
|
||||||
|
|
||||||
|
- name: Apply UserConfig::getValueBool string-cast workaround
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: >-
|
||||||
|
docker exec {{ item.item }}
|
||||||
|
sed -i 's|$b = strtolower($this->getTypedValue|$b = strtolower((string)$this->getTypedValue|'
|
||||||
|
/var/www/html/lib/private/Config/UserConfig.php
|
||||||
|
loop: "{{ _nextcloud_userconfig_check.results }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item }}"
|
||||||
|
when:
|
||||||
|
- item.rc | default(2) == 1
|
||||||
|
|
||||||
- name: Wait for Nextcloud to be ready
|
- name: Wait for Nextcloud to be ready
|
||||||
ansible.builtin.shell:
|
ansible.builtin.shell:
|
||||||
cmd: docker compose exec -T nextcloud php /var/www/html/occ status --output=json
|
cmd: docker compose exec -T nextcloud php /var/www/html/occ status --output=json
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,16 @@
|
||||||
---
|
---
|
||||||
# tasks file for configuring notify_push in Nextcloud
|
# tasks file for configuring notify_push in Nextcloud
|
||||||
|
|
||||||
|
- name: Read current notify_push base endpoint
|
||||||
|
community.docker.docker_container_exec:
|
||||||
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
|
command: php /var/www/html/occ config:app:get notify_push base_endpoint
|
||||||
|
register: _notify_push_current
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
- name: Configure notify_push base endpoint
|
- name: Configure notify_push base endpoint
|
||||||
community.docker.docker_container_exec:
|
community.docker.docker_container_exec:
|
||||||
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
container: "{{ nextcloud_docker_compose_dir | basename }}-nextcloud-1"
|
||||||
command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_domain }}/push
|
command: php /var/www/html/occ notify_push:setup https://{{ nextcloud_notify_push_domain | default(nextcloud_domains[0]) }}/push
|
||||||
|
when: (_notify_push_current.stdout | default('') | trim) != ('https://' ~ (nextcloud_notify_push_domain | default(nextcloud_domains[0])) ~ '/push')
|
||||||
|
|
@ -8,7 +8,9 @@
|
||||||
chdir: "{{ nextcloud_docker_compose_dir }}"
|
chdir: "{{ nextcloud_docker_compose_dir }}"
|
||||||
loop: "{{ nextcloud_apps_to_install }}"
|
loop: "{{ nextcloud_apps_to_install }}"
|
||||||
register: app_install_result
|
register: app_install_result
|
||||||
changed_when: "'installed' in app_install_result.stdout"
|
changed_when:
|
||||||
|
- "'already installed' not in app_install_result.stdout"
|
||||||
|
- "'installed' in app_install_result.stdout"
|
||||||
failed_when:
|
failed_when:
|
||||||
- app_install_result.rc != 0
|
- app_install_result.rc != 0
|
||||||
- "'already installed' not in app_install_result.stdout"
|
- "'already installed' not in app_install_result.stdout"
|
||||||
|
|
@ -19,7 +21,9 @@
|
||||||
chdir: "{{ nextcloud_docker_compose_dir }}"
|
chdir: "{{ nextcloud_docker_compose_dir }}"
|
||||||
loop: "{{ nextcloud_apps_to_install }}"
|
loop: "{{ nextcloud_apps_to_install }}"
|
||||||
register: app_enable_result
|
register: app_enable_result
|
||||||
changed_when: "'enabled' in app_enable_result.stdout"
|
changed_when:
|
||||||
|
- "'already enabled' not in app_enable_result.stdout"
|
||||||
|
- "'enabled' in app_enable_result.stdout"
|
||||||
failed_when:
|
failed_when:
|
||||||
- app_enable_result.rc != 0
|
- app_enable_result.rc != 0
|
||||||
- "'already enabled' not in app_enable_result.stdout"
|
- "'already enabled' not in app_enable_result.stdout"
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,13 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ nextcloud_traefik_network }}
|
- traefik.docker.network={{ nextcloud_traefik_network }}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}.rule=Host(`{{ nextcloud_domain }}`)
|
- traefik.http.routers.{{ nextcloud_service_name }}.rule={% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
|
||||||
{% if nextcloud_use_ssl %}
|
{% if nextcloud_use_ssl %}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=websecure
|
- traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}.tls=true
|
- traefik.http.routers.{{ nextcloud_service_name }}.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ nextcloud_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=web
|
- traefik.http.routers.{{ nextcloud_service_name }}.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -60,7 +63,7 @@ services:
|
||||||
PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M
|
PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M
|
||||||
PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M
|
PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M
|
||||||
OVERWRITEPROTOCOL: https
|
OVERWRITEPROTOCOL: https
|
||||||
OVERWRITEHOST: {{ nextcloud_domain }}
|
OVERWRITEHOST: {{ nextcloud_domains[0] }}
|
||||||
TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}"
|
TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}"
|
||||||
volumes:
|
volumes:
|
||||||
- {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html
|
- {{ nextcloud_docker_volume_dir }}/nextcloud/:/var/www/html
|
||||||
|
|
@ -69,6 +72,12 @@ services:
|
||||||
{% for net in nextcloud_extra_networks %}
|
{% for net in nextcloud_extra_networks %}
|
||||||
- {{ net }}
|
- {{ net }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if nextcloud_extra_hosts is defined and nextcloud_extra_hosts | length > 0 %}
|
||||||
|
extra_hosts:
|
||||||
|
{% for host in nextcloud_extra_hosts %}
|
||||||
|
- "{{ host }}"
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: {{ nextcloud_image }}
|
image: {{ nextcloud_image }}
|
||||||
|
|
@ -88,7 +97,7 @@ services:
|
||||||
PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M
|
PHP_MEMORY_LIMIT: {{ nextcloud_memory_limit_mb }}M
|
||||||
PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M
|
PHP_UPLOAD_LIMIT: {{ nextcloud_upload_limit_mb }}M
|
||||||
OVERWRITEPROTOCOL: https
|
OVERWRITEPROTOCOL: https
|
||||||
OVERWRITEHOST: {{ nextcloud_domain }}
|
OVERWRITEHOST: {{ nextcloud_domains[0] }}
|
||||||
TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}"
|
TRUSTED_PROXIES: "{{ nextcloud_trusted_proxies }}"
|
||||||
{% if nextcloud_use_s3_storage %}
|
{% if nextcloud_use_s3_storage %}
|
||||||
OBJECTSTORE_S3_KEY: {{ nextcloud_s3_key }}
|
OBJECTSTORE_S3_KEY: {{ nextcloud_s3_key }}
|
||||||
|
|
@ -127,7 +136,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
PORT: "7867"
|
PORT: "7867"
|
||||||
REDIS_URL: "redis://redis:6379"
|
REDIS_URL: "redis://redis:6379"
|
||||||
DATABASE_URL: "postgres://{{ nextcloud_postgres_user }}:{{ nextcloud_postgres_password }}@db:5432/{{ nextcloud_postgres_db }}"
|
DATABASE_URL: "postgres://{{ nextcloud_postgres_user | urlencode | replace('/', '%2F') }}:{{ nextcloud_postgres_password | urlencode | replace('/', '%2F') }}@db:5432/{{ nextcloud_postgres_db }}"
|
||||||
DATABASE_PREFIX: "oc_"
|
DATABASE_PREFIX: "oc_"
|
||||||
NEXTCLOUD_URL: "http://nginx"
|
NEXTCLOUD_URL: "http://nginx"
|
||||||
networks:
|
networks:
|
||||||
|
|
@ -136,11 +145,14 @@ services:
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=true
|
- traefik.enable=true
|
||||||
- traefik.docker.network={{ nextcloud_traefik_network }}
|
- traefik.docker.network={{ nextcloud_traefik_network }}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}-push.rule=Host(`{{ nextcloud_domain }}`) && PathPrefix(`/push`)
|
- traefik.http.routers.{{ nextcloud_service_name }}-push.rule=({% for d in nextcloud_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor %}) && PathPrefix(`/push`)
|
||||||
- traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867
|
- traefik.http.services.{{ nextcloud_service_name }}-push.loadbalancer.server.port=7867
|
||||||
{% if nextcloud_use_ssl %}
|
{% if nextcloud_use_ssl %}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure
|
- traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=websecure
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}-push.tls=true
|
- traefik.http.routers.{{ nextcloud_service_name }}-push.tls=true
|
||||||
|
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
|
||||||
|
- traefik.http.routers.{{ nextcloud_service_name }}-push.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=web
|
- traefik.http.routers.{{ nextcloud_service_name }}-push.entrypoints=web
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,43 @@
|
||||||
Role Name
|
# opencloud
|
||||||
=========
|
|
||||||
|
|
||||||
A brief description of the role goes here.
|
Deploys [OpenCloud](https://opencloud.eu/) (`opencloudeu/opencloud`) as a
|
||||||
|
self-contained file platform via Docker Compose behind Traefik. Supports
|
||||||
|
the built-in IdP or external OIDC, optional S3 storage, external LDAP,
|
||||||
|
Collabora and draw.io integration, and OIDC-claim-based role assignment.
|
||||||
|
|
||||||
Requirements
|
## Role variables
|
||||||
------------
|
|
||||||
|
|
||||||
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
A selection of the most relevant variables — see
|
||||||
|
[defaults/main.yml](defaults/main.yml) for the full set.
|
||||||
|
|
||||||
Role Variables
|
| Variable | Default | Description |
|
||||||
--------------
|
| --- | --- | --- |
|
||||||
|
| `opencloud_domain` | `opencloud.local.test` | FQDN the Traefik router matches. |
|
||||||
|
| `opencloud_image` | `opencloudeu/opencloud:latest` | Container image. |
|
||||||
|
| `opencloud_port` | `9200` | Container port Traefik forwards to. |
|
||||||
|
| `opencloud_admin_password` | `admin` | Initial admin password — **override this**. |
|
||||||
|
| `opencloud_traefik_network` | `proxy` | Docker network shared with Traefik. |
|
||||||
|
| `opencloud_use_ssl` | `true` | Enable the TLS resolver on the router. |
|
||||||
|
| `opencloud_oidc_issuer` | `""` | External OIDC issuer; empty uses the built-in IdP. |
|
||||||
|
| `opencloud_use_s3_storage` | `false` | Use S3 storage instead of local disk. |
|
||||||
|
| `opencloud_ldap_uri` | `""` | External LDAP URI; empty uses the built-in directory. |
|
||||||
|
| `opencloud_collabora_domain` | `""` | Collabora server domain; set with `opencloud_wopi_domain` to enable editing. |
|
||||||
|
| `opencloud_wopi_domain` | `""` | WOPI server FQDN; required alongside `opencloud_collabora_domain`. |
|
||||||
|
| `opencloud_drawio_url` | `""` | draw.io URL; set to enable diagram editing. |
|
||||||
|
| `opencloud_role_assignment_driver` | `default` | Set to `oidc` to map OIDC claims to roles. |
|
||||||
|
|
||||||
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
## Example
|
||||||
|
|
||||||
Dependencies
|
```yaml
|
||||||
------------
|
- hosts: services
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: digitalboard.core.opencloud
|
||||||
|
vars:
|
||||||
|
opencloud_domain: "opencloud.example.com"
|
||||||
|
opencloud_admin_password: "{{ vault_opencloud_admin_password }}"
|
||||||
|
```
|
||||||
|
|
||||||
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
## License
|
||||||
|
|
||||||
Example Playbook
|
MIT-0
|
||||||
----------------
|
|
||||||
|
|
||||||
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
|
||||||
|
|
||||||
- hosts: servers
|
|
||||||
roles:
|
|
||||||
- { role: username.rolename, x: 42 }
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
BSD
|
|
||||||
|
|
||||||
Author Information
|
|
||||||
------------------
|
|
||||||
|
|
||||||
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@
|
||||||
- name: restart opencloud
|
- name: restart opencloud
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
project_src: "{{ opencloud_docker_compose_dir }}"
|
project_src: "{{ opencloud_docker_compose_dir }}"
|
||||||
state: restarted
|
state: present
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue