digitalboard.core/roles/opnform
Simon Bärlocher 3236ca332f
docs(collection): document all roles and fix metadata drift
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.
2026-05-27 23:12:24 +02:00
..
defaults feat(services): multi-domain routing, split-horizon and OIDC hardening 2026-05-27 23:12:24 +02:00
handlers feat(opnform)!: add admin and OIDC bootstrap, rename role to lowercase 2026-05-26 14:54:35 +02:00
meta feat(services): multi-domain routing, split-horizon and OIDC hardening 2026-05-27 23:12:24 +02:00
tasks docs(collection): document all roles and fix metadata drift 2026-05-27 23:12:24 +02:00
templates feat(services): multi-domain routing, split-horizon and OIDC hardening 2026-05-27 23:12:24 +02:00
tests feat(opnform)!: add admin and OIDC bootstrap, rename role to lowercase 2026-05-26 14:54:35 +02:00
vars fix(opnform): address review feedback on vars header and meta boilerplate 2026-05-26 14:58:10 +02:00
README.md docs(collection): document all roles and fix metadata drift 2026-05-27 23:12:24 +02:00

opnform

Deploy 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:

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.

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)

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:

# 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

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.

opnform_oidc_sso_entrypoint: true  # default false
opnform_oidc_sso_path: "/sso"      # link users to https://<domain>/sso

Networking / split-horizon

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

- name: Deploy OpnForm service
  hosts: opnform_servers
  become: true
  roles:
    - digitalboard.core.opnform

With inventory variables:

# 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