Replace ansible-galaxy init placeholders across the collection and correct documentation that drifted from the code, after a multi-agent review of every role README against its defaults, tasks and templates. Collection level: - README: role table for all 16 roles, requirements and role-ordering - galaxy.yml: declare community.docker and community.general deps, real description/tags/urls; normalize license to MIT-0 - meta/runtime.yml: requires_ansible '>=2.15.0' - plugins/README: document the homarr_layout filter and garage_credentials lookup instead of scaffold boilerplate Per-role meta/main.yml and README for the placeholder roles (389ds, authentik, authentik_outpost_ldap, base, collabora, drawio, garage, homarr, httpbin, keycloak, nextcloud, opencloud, traefik). Correctness fixes found during review: - keycloak: wrong domain default, drop invented keycloak_cert_resolver, document the provisioning feature - garage: root_domain is .s3.<first-entry>, not the bare domain - opnform: jwt/front_api secrets use `openssl rand -hex 32`; align the validation fail_msg in tasks/main.yml accordingly - send: S3 example references garage_s3_domains[0] (was singular) - opencloud: document required opencloud_wopi_domain License normalized to MIT-0 across galaxy.yml, role meta and READMEs to match the SPDX headers.
219 lines
7.9 KiB
Markdown
219 lines
7.9 KiB
Markdown
# opnform
|
|
|
|
Deploy [OpnForm](https://github.com/OpnForm/OpnForm) as a self-contained
|
|
Docker Compose stack behind Traefik.
|
|
|
|
## What this role does
|
|
|
|
- Deploys the full official OpnForm stack: `api`, `api-worker`, `api-scheduler`,
|
|
`ui`, `db` (Postgres), `redis`, and `ingress` (nginx)
|
|
- Configures all environment variables for self-hosted production use
|
|
- Integrates the ingress container with an existing Traefik proxy network
|
|
- Waits for the API container to become healthy before returning
|
|
|
|
## What this role does NOT do
|
|
|
|
- Does not migrate existing OpnForm databases — only bootstraps fresh
|
|
installs (admin registration + OIDC connection are idempotent)
|
|
|
|
## Architecture note: why two reverse proxies?
|
|
|
|
```
|
|
Browser → Traefik (TLS, host routing) → ingress-nginx → api (PHP-FPM) / ui (Nuxt)
|
|
```
|
|
|
|
The `ingress` container looks like a redundant proxy next to Traefik but
|
|
does a different job. OpnForm's `api` image is **PHP-FPM only** — it
|
|
speaks the FastCGI protocol on port 9000, not HTTP. Traefik cannot
|
|
translate FastCGI, so the ingress nginx is required to:
|
|
|
|
- Translate HTTP `/api/*` requests into FastCGI calls to `api:9000`
|
|
- Rewrite request URIs via the `$api_uri` map
|
|
- Set Laravel-specific FastCGI params (`SCRIPT_FILENAME`, `REQUEST_URI`)
|
|
- Reverse-proxy `/` to the Nuxt UI container on port 3000
|
|
|
|
Both containers run on the same Docker network on the same host, so the
|
|
performance overhead of the extra hop is negligible (in-kernel memory
|
|
copy, not a real network round-trip). Removing the ingress would require
|
|
a custom OpnForm image with a built-in HTTP server, which is out of
|
|
scope for this role.
|
|
|
|
## Required variables
|
|
|
|
Provide via OpenBao, Ansible Vault, or extra-vars. **Never commit real
|
|
secrets to version control.**
|
|
|
|
| Variable | Format | Generate with |
|
|
|---|---|---|
|
|
| `opnform_app_key` | `base64:<32 bytes base64>` | `echo "base64:$(openssl rand -base64 32)"` |
|
|
| `opnform_jwt_secret` | 32-byte hex string | `openssl rand -hex 32` |
|
|
| `opnform_front_api_secret` | 32-byte hex string | `openssl rand -hex 32` |
|
|
| `opnform_db_password` | strong password | `openssl rand -base64 24` |
|
|
|
|
`opnform_app_key` MUST keep the `base64:` prefix — the validation task
|
|
asserts it. `opnform_jwt_secret` and `opnform_front_api_secret` have no
|
|
enforced format; any sufficiently random value works.
|
|
|
|
When `opnform_oidc_enabled` is `true`:
|
|
|
|
| Variable | Source |
|
|
|---|---|
|
|
| `opnform_oidc_client_secret` | from your Keycloak/Authentik client |
|
|
|
|
The `assert` task at the top of the role will fail fast if any secret is
|
|
missing or malformed.
|
|
|
|
## First login
|
|
|
|
OpnForm in self-hosted mode does **not** ship a pre-seeded admin user.
|
|
The first user to register becomes the owner of the default workspace,
|
|
and further public registration is disabled afterwards (additional
|
|
users must be invited via the Admin UI).
|
|
|
|
This role supports two ways to create that first user:
|
|
|
|
### Option A — automated bootstrap (recommended)
|
|
|
|
Set `opnform_admin_email` and `opnform_admin_password` (ideally from
|
|
Vault / OpenBao). The role then POSTs to `/api/register` after the
|
|
API container is healthy, skipping the setup page entirely. The task
|
|
is idempotent: it does a login check first and only registers if the
|
|
user does not already exist.
|
|
|
|
```yaml
|
|
opnform_admin_name: "Administrator" # default
|
|
opnform_admin_email: "admin@example.com"
|
|
opnform_admin_password: "{{ vault_opnform_admin_password }}"
|
|
```
|
|
|
|
Password rules enforced by OpnForm: minimum 8 characters, at least one
|
|
letter, one digit, and one of `@$!%*#?&-_+=.,:;<>^()[]{}|~`.
|
|
|
|
### Option B — manual setup page
|
|
|
|
Leave `opnform_admin_email` / `opnform_admin_password` empty. Visit
|
|
`opnform_base_url` and complete the setup page in the browser.
|
|
|
|
## OIDC setup
|
|
|
|
Set `opnform_oidc_enabled: true` and the role provisions an
|
|
IdentityConnection on the admin's default workspace via
|
|
`POST /api/open/workspaces/{id}/oidc-connections`. OpnForm enforces a
|
|
single OIDC connection per workspace, so the task is idempotent: it GETs
|
|
existing connections first, then either POSTs a new one or PATCHes the
|
|
existing one to the desired state. PATCHing (rather than skipping when
|
|
one exists) keeps inventory changes — e.g. a corrected issuer — applied
|
|
on re-runs instead of leaving stale values in the DB.
|
|
|
|
**Prerequisite**: the admin bootstrap must be configured
|
|
(`opnform_admin_email` + `opnform_admin_password`). The OIDC API
|
|
requires an authenticated admin token; the role logs in with those
|
|
credentials to make the call. The validation block fails fast if OIDC
|
|
is enabled without admin credentials.
|
|
|
|
### Required when `opnform_oidc_enabled: true`
|
|
|
|
| Variable | Notes |
|
|
|---|---|
|
|
| `opnform_oidc_client_secret` | from your IdP, never commit |
|
|
| `opnform_oidc_domain` | email domain that triggers OIDC (e.g. `example.com`) |
|
|
|
|
### Tunables (defaults shown)
|
|
|
|
```yaml
|
|
opnform_oidc_issuer: "https://auth.digitalboard.ch/realms/Digitalboard"
|
|
opnform_oidc_client_id: "opnform-digitalboard"
|
|
opnform_oidc_client_name: "Digitalboard" # display name in UI
|
|
opnform_oidc_slug: "oidc" # used in /auth/{slug}/callback
|
|
opnform_oidc_scopes: [openid, profile, email, groups]
|
|
```
|
|
|
|
### Group → role mapping
|
|
|
|
Two ways, the list takes precedence:
|
|
|
|
```yaml
|
|
# Option 1: full list (any number of mappings)
|
|
opnform_oidc_group_role_mappings:
|
|
- idp_group: "opnform-admins"
|
|
role: admin
|
|
- idp_group: "opnform-editors"
|
|
role: editor
|
|
|
|
# Option 2: convenience — single admin group
|
|
opnform_oidc_admin_group: "opnform-admins" # mapped to role=admin
|
|
```
|
|
|
|
Valid roles: `owner`, `admin`, `editor`, `member`.
|
|
|
|
### Force OIDC-only login
|
|
|
|
```yaml
|
|
opnform_oidc_force_login: true # default false
|
|
```
|
|
|
|
Sets `OIDC_FORCE_LOGIN=true` on the API: password login is disabled and
|
|
every user must authenticate via OIDC. The role keeps force-login **off**
|
|
during the first deploy (the admin/OIDC bootstrap is password-based) and
|
|
switches it on only after the OIDC connection is provisioned, recreating
|
|
the API containers. Ensure all real users have addresses under
|
|
`opnform_oidc_domain` before enabling — there is no password fallback.
|
|
|
|
### Direct-SSO entrypoint
|
|
|
|
OpnForm has no native way to skip the email login form and jump straight
|
|
to the IdP. When enabled, the ingress serves a tiny redirect page that
|
|
calls `/api/auth/{slug}/redirect` (no domain check) and forwards the
|
|
browser to the IdP authorize URL.
|
|
|
|
```yaml
|
|
opnform_oidc_sso_entrypoint: true # default false
|
|
opnform_oidc_sso_path: "/sso" # link users to https://<domain>/sso
|
|
```
|
|
|
|
## Networking / split-horizon
|
|
|
|
```yaml
|
|
opnform_extra_domains: [] # extra Host-rule hostnames (OR-combined)
|
|
opnform_extra_hosts: [] # API container /etc/hosts overrides ("host:ip")
|
|
```
|
|
|
|
`opnform_extra_domains` adds internal `*.int.*` FQDNs so a DMZ
|
|
reverseproxy can reach a backend hostname covered by the cert.
|
|
`opnform_extra_hosts` lets the API containers reach the IdP's public FQDN
|
|
(used in the OIDC `iss` claim) over the LAN when the DMZ has no NAT
|
|
loopback.
|
|
|
|
## Example playbook
|
|
|
|
```yaml
|
|
- name: Deploy OpnForm service
|
|
hosts: opnform_servers
|
|
become: true
|
|
roles:
|
|
- digitalboard.core.opnform
|
|
```
|
|
|
|
With inventory variables:
|
|
|
|
```yaml
|
|
# group_vars/opnform_servers.yml
|
|
opnform_domain: forms.digitalboard.ch
|
|
opnform_base_url: "https://forms.digitalboard.ch"
|
|
opnform_app_key: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/opnform',
|
|
mount_point='kv').data.data.app_key }}"
|
|
opnform_jwt_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/opnform',
|
|
mount_point='kv').data.data.jwt_secret }}"
|
|
opnform_front_api_secret: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/opnform',
|
|
mount_point='kv').data.data.front_api_secret }}"
|
|
opnform_db_password: "{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
'digitalboard/opnform',
|
|
mount_point='kv').data.data.db_password }}"
|
|
```
|
|
|
|
## License
|
|
|
|
MIT-0
|