Bundle of cross-role changes for the gymb services deployment: - Traefik routers: OR-combine opnform/homarr/bookstack Host rules with new *_extra_domains (internal *.int.* FQDNs for a DMZ reverseproxy), and emit tls.certresolver only when traefik_cert_mode == acme (drawio, homarr, opnform, send). - Split-horizon: bookstack_extra_hosts / opnform_extra_hosts add container /etc/hosts overrides so containers reach the IdP public FQDN over the LAN. - bookstack: assert the OIDC issuer resolves concretely (reject "//v2.0"), allowing non-Entra IdPs that override bookstack_oidc_issuer. - homarr: derive the bcrypt salt from the password digest so the admin hash is idempotent — no spurious template changes / container restarts. - opnform: PATCH an existing OIDC connection instead of skipping (applies corrected inventory on re-run); add OIDC_FORCE_LOGIN (enabled only after bootstrap) and an optional direct-SSO ingress entrypoint. Docs: READMEs and meta/argument_specs.yml updated for all new variables.
154 lines
6.4 KiB
Markdown
154 lines
6.4 KiB
Markdown
# Ansible Role: bookstack
|
|
|
|
Deploys [BookStack](https://www.bookstackapp.com/) as a self-contained Docker
|
|
Compose stack behind Traefik, with its own MariaDB container, OIDC SSO
|
|
(Entra ID by default) and a daily systemd-timer driven backup of database
|
|
and uploads.
|
|
|
|
## Requirements
|
|
|
|
- Docker Engine + Compose plugin on the target host
|
|
- Traefik already running, with the external network referenced by
|
|
`bookstack_traefik_network` (default: `proxy`)
|
|
- `community.docker` collection on the controller
|
|
- DNS for `bookstack_domain` pointing at the Traefik host
|
|
|
|
## Required variables
|
|
|
|
The role asserts these are set; the play fails fast if any is empty:
|
|
|
|
| Variable | Description |
|
|
|---|---|
|
|
| `bookstack_db_root_password` | MariaDB root password |
|
|
| `bookstack_db_password` | MariaDB user password |
|
|
| `bookstack_admin_password` | Initial local admin password |
|
|
| `bookstack_oidc_client_id` | OIDC client ID (if OIDC on) |
|
|
| `bookstack_oidc_client_secret` | OIDC client secret (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
|
|
real secrets.
|
|
|
|
## Optional variables
|
|
|
|
See `defaults/main.yml`. Frequently overridden:
|
|
|
|
- `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_oidc_enabled` (set `false` to disable OIDC entirely)
|
|
- `bookstack_oidc_auto_initiate` (`true` redirects straight to IdP)
|
|
- `bookstack_oidc_user_to_groups` (`true` syncs roles from Entra groups)
|
|
- `bookstack_backup_enabled`, `bookstack_backup_schedule`,
|
|
`bookstack_backup_retention_days`
|
|
|
|
## Entra ID app registration
|
|
|
|
1. Azure Portal → Entra ID → App registrations → New registration
|
|
2. Redirect URI (Web): `https://<bookstack_domain>/oidc/callback`
|
|
3. Front-channel logout URL: `https://<bookstack_domain>/logout`
|
|
4. Certificates & secrets → New client secret →
|
|
`bookstack_oidc_client_secret`
|
|
5. For group sync (`bookstack_oidc_user_to_groups: true`):
|
|
- Token configuration → Add groups claim → Security groups
|
|
- In BookStack, create roles whose **External Auth ID** equals the
|
|
Entra group Object ID, so the mapping resolves on first login.
|
|
|
|
## What the role does
|
|
|
|
| Phase | Action |
|
|
|---|---|
|
|
| Validate | `assert` all required secrets are set |
|
|
| Prepare | install packages, create volume dirs, generate persistent `APP_KEY`, verify Traefik network |
|
|
| Deploy | render `docker-compose.yml`, pull images, bring stack up |
|
|
| Configure | wait for the app, create the initial local admin via `php artisan bookstack:create-admin` (idempotent) |
|
|
| Backup | render `/usr/local/bin/bookstack-backup.sh` + systemd timer (daily 03:00, 14-day retention) |
|
|
|
|
## Example playbook
|
|
|
|
```yaml
|
|
- name: Deploy BookStack service
|
|
hosts: bookstack_servers
|
|
become: true
|
|
roles:
|
|
- digitalboard.core.bookstack
|
|
```
|
|
|
|
With inventory variables:
|
|
|
|
```yaml
|
|
# group_vars/bookstack_servers.yml
|
|
bookstack_domain: wiki.digitalboard.ch
|
|
bookstack_base_url: "https://wiki.digitalboard.ch"
|
|
bookstack_entra_tenant_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.tenant_id }}"
|
|
bookstack_oidc_client_id: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.client_id }}"
|
|
bookstack_oidc_client_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.client_secret }}"
|
|
bookstack_db_root_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.db_root_password }}"
|
|
bookstack_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.db_password }}"
|
|
bookstack_admin_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/bookstack',
|
|
mount_point='kv').data.data.admin_password }}"
|
|
```
|
|
|
|
## Backup / restore
|
|
|
|
Backups land in `{{ bookstack_backup_dir }}` (default
|
|
`/srv/data/bookstack/backup`) with three files per run:
|
|
|
|
- `bookstack-db-<stamp>.sql.gz` — mariadb-dump
|
|
- `bookstack-files-<stamp>.tar.gz` — uploads, attachments
|
|
- `bookstack-appkey-<stamp>.txt` — APP_KEY (required for restore!)
|
|
|
|
Manual trigger: `systemctl start bookstack-backup.service`
|
|
Timer status: `systemctl list-timers bookstack-backup.timer`
|
|
|
|
Restore procedure:
|
|
|
|
1. Stop the stack: `docker compose down` in `bookstack_docker_compose_dir`
|
|
2. Restore the APP_KEY: copy the `.txt` content to
|
|
`{{ bookstack_docker_volume_dir }}/.app_key` (the key MUST match or
|
|
encrypted DB values become unreadable)
|
|
3. Start only the DB container, then load the dump:
|
|
```bash
|
|
gunzip -c bookstack-db-<stamp>.sql.gz \
|
|
| docker exec -i bookstack-db \
|
|
mariadb -u root -p"<root-pw>" bookstack
|
|
```
|
|
4. Extract the files: `tar -xzf bookstack-files-<stamp>.tar.gz -C
|
|
{{ bookstack_appdata_dir }}/www/`
|
|
5. Bring the stack back up: `docker compose up -d`
|
|
|
|
## Notes
|
|
|
|
- `bookstack_oidc_auto_initiate: false` (default) shows a login page
|
|
with an SSO button alongside the local login form. With `true`, users
|
|
go straight to the IdP — the local admin then has to use
|
|
`https://<domain>/login?email_login=1`.
|
|
- `bookstack_oidc_user_to_groups: true` only makes sense once BookStack
|
|
roles with the correct **External Auth IDs** (= Entra group Object
|
|
IDs) exist; otherwise users lose their role assignment on every login.
|
|
- Image tags default to pinned versions; bump them deliberately rather
|
|
than chasing `latest`.
|
|
- BookStack officially supports MySQL/MariaDB only — no PostgreSQL.
|
|
|
|
## License
|
|
|
|
MIT
|