From f797c3bad0b54cc38ac7029d0c44ad1acbe57059 Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Mon, 18 May 2026 13:44:16 +0200 Subject: [PATCH 1/3] fix(roles/keycloak): run kc.sh build as keycloak user --- CHANGELOG.md | 1 + roles/keycloak/tasks/main.yml | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6a1eaa..250f5e1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **role:keycloak**: Fix ownership under `/opt/keycloak/data/`. Previously the post-install build step ran as `root` and left `/opt/keycloak/data/` and `/opt/keycloak/data/tmp/` owned by `root:root`, which the `keycloak` service user could not write into (no `data/cache/` was ever created). The build now runs as the `keycloak` service user, and existing installations get the ownership corrected on the next role run. * **role:nextcloud**: The `nextcloud-update` script now owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand. Previously, callers enabled maintenance mode before invoking the script (to protect the DB dump), which disables the LDAP user provider and causes the `before-update` export (`occ user:list`, `config:list`, `app:list`) to silently omit LDAP users. The script now assumes maintenance mode is **off** at start, runs the `before-update` export with apps loaded, lets `updater.phar` manage maintenance mode itself, and explicitly disables it again before `occ upgrade` and `occ app:update` (since `occ upgrade` does not turn it off on its own) — so all post-upgrade commands (`app:update`, `db:add-missing-*`, `db:convert-filecache-bigint`, the `after-update` export) also run with apps loaded. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow; the DB dump should rely on `--single-transaction` instead. * **roles**: Set `become: false` on tasks delegated to localhost across the collection. Previously these tasks inherited `become: true` from the playbook level and tried to call `sudo` on the Ansible controller, which fails on controllers without a passwordless sudo setup with `sudo: a password is required`. Affected are all `repo_*` roles, the `*_vm` cloud roles (`exoscale_vm`, `hetzner_vm`, `infomaniak_vm`), all `icingaweb2_module_*` roles that download artefacts, `monitoring_plugins`, `shared`, plus several others. Existing playbooks that were working without playbook-level `become: true` are unaffected ([#242](https://github.com/Linuxfabrik/lfops/issues/242)). diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 13de4453..84b5ff2b 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -118,10 +118,14 @@ - block: - - name: 'Change the working directory to /opt/keycloak and bin/kc.sh build --db {{ keycloak__db_vendor }}' + # kc.sh build always rebuilds and gives no reliable idempotency signal in its stdout, hence changed_when: true. + - name: 'cd /opt/keycloak && bin/kc.sh build --db {{ keycloak__db_vendor }}' ansible.builtin.command: 'bin/kc.sh build --db {{ keycloak__db_vendor }}' args: chdir: '/opt/keycloak' + become: true + become_user: 'keycloak' + changed_when: true when: 'keycloak__mode == "production"' tags: From 9268dc8d47d70b67070cdbf197511f79ee5ded35 Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Mon, 18 May 2026 13:45:09 +0200 Subject: [PATCH 2/3] feat(roles/keycloak): auto-remove bootstrap admin credentials after first run --- CHANGELOG.md | 1 + roles/keycloak/README.md | 10 ++-- roles/keycloak/tasks/main.yml | 52 ++++++++++++++++++- .../etc/sysconfig/keycloak-sysconfig.j2 | 12 ++++- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 250f5e1c..2caf04d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/opt/keycloak/.bootstrap_admin_done` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. * **CONTRIBUTING**: `meta/argument_specs.yml` must declare the `__dependent_var` slot for any variable that `setup_*` playbooks inject into the role via `vars:`. Dict variables fed by external lookups like `linuxfabrik.lfops.bitwarden_item` should use `type: 'dict'` without strict sub-options, since the lookup returns the full item with additional keys. * **role:example**: Demonstrate the `delegate_to: 'localhost'` + `become: false` pattern (download on the controller, copy to the target) so role authors can copy it consistently. diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index d65446c9..0527d346 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -54,13 +54,17 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__admin_login` -* The *temporary* Keycloak Admin login credentials. To harden security, create a permanent admin account after logging in as a temporary admin user, and delete the temporary one. +* The *temporary* Keycloak bootstrap admin login credentials. Keycloak only honors `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` on the very first start, when no admin user exists in the `master` realm yet. Subsequent restarts ignore these variables. +* Mandatory only on the first role run (and during disaster recovery, see below). The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/opt/keycloak/.bootstrap_admin_done`. The cleartext password no longer lingers on disk after the role finishes. +* On subsequent runs the role detects the marker file and renders the sysconfig file without credentials right away. `keycloak__admin_login` can be removed from the inventory at that point. +* Use a username that visibly marks the account as throwaway (suffix `-temp`), so it is obvious in the Keycloak UI which account must be deleted once a permanent admin has been created. +* Disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/opt/keycloak/.bootstrap_admin_done`, re-add `keycloak__admin_login` to the inventory, and re-run the role. * Type: Dictionary. * Subkeys: * `username`: - * Mandatory. Username. + * Mandatory. Username. By convention, end with `-temp` (e.g. `keycloak-admin-temp`) to flag the account as the bootstrap user that must be deleted after the permanent admin is in place. * Type: String. * `password`: @@ -99,7 +103,7 @@ Example: # mandatory keycloak__admin_login: password: 'password' - username: 'keycloak-admin' + username: 'keycloak-admin-temp' keycloak__db_login: password: 'password' username: 'keycloak' diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 84b5ff2b..481bf562 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,9 +1,14 @@ - block: - - name: 'Debug Variables:' + - name: 'stat /opt/keycloak/.bootstrap_admin_done' + ansible.builtin.stat: + path: '/opt/keycloak/.bootstrap_admin_done' + register: '__keycloak__bootstrap_marker' + + - name: 'Debug Variables' ansible.builtin.debug: msg: | - keycloak__admin_login.username: {{ keycloak__admin_login.username }} + keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(bootstrap already done)") }} keycloak__db_login.username: {{ keycloak__db_login.username }} keycloak__db_url_database: {{ keycloak__db_url_database }} keycloak__db_url_host: {{ keycloak__db_url_host }} @@ -95,6 +100,8 @@ owner: 'root' group: 'root' mode: 0o640 + vars: + __keycloak__render_bootstrap_creds: '{{ not __keycloak__bootstrap_marker["stat"]["exists"] }}' notify: 'keycloak: systemctl restart keycloak' - name: 'Create keycloak.service' @@ -148,3 +155,44 @@ - 'keycloak' - 'keycloak:configure' - 'keycloak:state' + + +- block: + + - name: 'Flush handlers so Keycloak restarts with the bootstrap credentials loaded' + ansible.builtin.meta: 'flush_handlers' + + - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }} (bootstrap admin gets provisioned during startup)' + ansible.builtin.wait_for: + port: '{{ __keycloak__listen_port }}' + host: '127.0.0.1' + timeout: 180 + vars: + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | d("") | length > 0) | ternary(8443, 8080) }}' + + - name: 'Deploy /etc/sysconfig/keycloak (without bootstrap credentials)' + ansible.builtin.template: + backup: true + src: 'etc/sysconfig/keycloak-sysconfig.j2' + dest: '/etc/sysconfig/keycloak' + owner: 'root' + group: 'root' + mode: 0o640 + vars: + __keycloak__render_bootstrap_creds: false + + - name: 'touch /opt/keycloak/.bootstrap_admin_done' + ansible.builtin.file: + path: '/opt/keycloak/.bootstrap_admin_done' + state: 'touch' + owner: 'keycloak' + group: 'keycloak' + mode: 0o644 + + when: + - 'not __keycloak__bootstrap_marker["stat"]["exists"]' + - 'keycloak__state == "started"' + + tags: + - 'keycloak' + - 'keycloak:configure' diff --git a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 index 1e7db746..d1f70e04 100644 --- a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 +++ b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 @@ -1,7 +1,17 @@ # {{ ansible_managed }} -# 2025022701 +# 2026051801 +{% if __keycloak__render_bootstrap_creds | d(false) | bool %} # WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead # WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login.username }} KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login.password }} +{% else %} +# Bootstrap admin credentials have been removed by the role after they served +# their purpose on the first Keycloak start. Re-bootstrap (e.g. after a DB +# loss) by removing the state marker /opt/keycloak/.bootstrap_admin_done and +# re-running the role with `keycloak__admin_login` defined. +# +# This file is intentionally kept on disk (empty of credentials) because +# keycloak.service references it as a mandatory `EnvironmentFile=`. +{% endif %} From 62c153c107f69c73e37193882d4f9774fb5847c8 Mon Sep 17 00:00:00 2001 From: Navid Sassan Date: Tue, 19 May 2026 17:14:12 +0200 Subject: [PATCH 3/3] style(roles/keycloak): improve state file handling and variable naming and some other minor improvements --- CHANGELOG.md | 2 +- roles/keycloak/README.md | 9 +- roles/keycloak/tasks/main.yml | 130 +++++++++++++----- .../etc/sysconfig/keycloak-sysconfig.j2 | 14 +- 4 files changed, 105 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2caf04d6..779b27ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/opt/keycloak/.bootstrap_admin_done` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. +* **role:keycloak**: The role no longer leaves the bootstrap admin credentials lying around in `/etc/sysconfig/keycloak` after the first run. It now writes the credentials, waits for Keycloak to consume them on startup (provisioning the bootstrap admin in the `master` realm), re-renders the sysconfig file with the credentials removed, and stores a state marker at `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state` so subsequent runs skip the credential render entirely. After the first run, `keycloak__admin_login` can be removed from the inventory. Disaster recovery: delete the marker file, re-add the variable, re-run. Also recommend a `-temp` suffix for the initial admin username (example: `keycloak-admin-temp`) so it is visually obvious in the Keycloak UI which account must be deleted once a permanent admin exists. * **role:monitoring_plugins**: `install_method: 'source'` now reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` (`py39` ... `py314`) from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python. The previous root-level `requirements.txt` no longer exists upstream. No variable changes; rsync sources updated. * **CONTRIBUTING**: `meta/argument_specs.yml` must declare the `__dependent_var` slot for any variable that `setup_*` playbooks inject into the role via `vars:`. Dict variables fed by external lookups like `linuxfabrik.lfops.bitwarden_item` should use `type: 'dict'` without strict sub-options, since the lookup returns the full item with additional keys. * **role:example**: Demonstrate the `delegate_to: 'localhost'` + `become: false` pattern (download on the controller, copy to the target) so role authors can copy it consistently. diff --git a/roles/keycloak/README.md b/roles/keycloak/README.md index 0527d346..1a6e8f15 100644 --- a/roles/keycloak/README.md +++ b/roles/keycloak/README.md @@ -55,10 +55,10 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__admin_login` * The *temporary* Keycloak bootstrap admin login credentials. Keycloak only honors `KC_BOOTSTRAP_ADMIN_USERNAME` / `KC_BOOTSTRAP_ADMIN_PASSWORD` on the very first start, when no admin user exists in the `master` realm yet. Subsequent restarts ignore these variables. -* Mandatory only on the first role run (and during disaster recovery, see below). The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/opt/keycloak/.bootstrap_admin_done`. The cleartext password no longer lingers on disk after the role finishes. +* Mandatory only on the first role run. The role writes the credentials to `/etc/sysconfig/keycloak`, restarts Keycloak so it consumes them and provisions the bootstrap admin in the `master` realm, then immediately re-renders the sysconfig file with the credentials removed and marks the bootstrap as done via `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state`. The cleartext password no longer lingers on disk after the role finishes. * On subsequent runs the role detects the marker file and renders the sysconfig file without credentials right away. `keycloak__admin_login` can be removed from the inventory at that point. * Use a username that visibly marks the account as throwaway (suffix `-temp`), so it is obvious in the Keycloak UI which account must be deleted once a permanent admin has been created. -* Disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/opt/keycloak/.bootstrap_admin_done`, re-add `keycloak__admin_login` to the inventory, and re-run the role. +* For disaster recovery (e.g. lost database, need to re-bootstrap an admin): remove `/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state`, re-add `keycloak__admin_login` to the inventory, and re-run the role. * Type: Dictionary. * Subkeys: @@ -96,17 +96,18 @@ All Keycloak config settings are described here: https://www.keycloak.org/server `keycloak__version` * The version of Keycloak that should be installed. +* Possible options: . * Type: String. Example: ```yaml # mandatory keycloak__admin_login: - password: 'password' username: 'keycloak-admin-temp' + password: 'linuxfabrik' keycloak__db_login: - password: 'password' username: 'keycloak' + password: 'linuxfabrik' keycloak__hostname: 'keycloak.local' keycloak__version: '26.1.2' ``` diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml index 481bf562..2cb1cfc1 100644 --- a/roles/keycloak/tasks/main.yml +++ b/roles/keycloak/tasks/main.yml @@ -1,33 +1,42 @@ - block: - - name: 'stat /opt/keycloak/.bootstrap_admin_done' + - name: 'stat /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' ansible.builtin.stat: - path: '/opt/keycloak/.bootstrap_admin_done' - register: '__keycloak__bootstrap_marker' + path: '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + register: '__keycloak__admin_login_bootstrapped_stat' + + # Read-only fact used in the debug output and as the default value of the per-render + # `__keycloak__include_bootstrap_creds` flag below. Do NOT reuse this fact directly in the + # sysconfig template: set_fact has higher Ansible variable precedence than task-level `vars:`, + # so a task trying to override this fact would be silently ignored. + - name: 'Set __keycloak__admin_login_bootstrapped' + ansible.builtin.set_fact: + __keycloak__admin_login_bootstrapped: '{{ __keycloak__admin_login_bootstrapped_stat["stat"]["exists"] }}' - name: 'Debug Variables' ansible.builtin.debug: - msg: | - keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(bootstrap already done)") }} - keycloak__db_login.username: {{ keycloak__db_login.username }} - keycloak__db_url_database: {{ keycloak__db_url_database }} - keycloak__db_url_host: {{ keycloak__db_url_host }} - keycloak__db_vendor: {{ keycloak__db_vendor }} - keycloak__expose_healthcheck_endpoints: {{ keycloak__expose_healthcheck_endpoints }} - keycloak__expose_metrics_endpoints: {{ keycloak__expose_metrics_endpoints }} - keycloak__hostname: {{ keycloak__hostname }} - keycloak__hostname_backchannel_dynamic: {{ keycloak__hostname_backchannel_dynamic }} - keycloak__https_certificate_file: {{ keycloak__https_certificate_file }} - keycloak__https_certificate_key_file: {{ keycloak__https_certificate_key_file }} - keycloak__https_protocols: {{ keycloak__https_protocols }} - keycloak__log: {{ keycloak__log }} - keycloak__log_file: {{ keycloak__log_file }} - keycloak__mode: {{ keycloak__mode }} - keycloak__proxy_headers: {{ keycloak__proxy_headers }} - keycloak__service_enabled: {{ keycloak__service_enabled }} - keycloak__spi_sticky_session_encoder_infinispan_should_attach_route: {{ keycloak__spi_sticky_session_encoder_infinispan_should_attach_route }} - keycloak__state: {{ keycloak__state }} - keycloak__version: {{ keycloak__version }} + msg: + - 'keycloak__admin_login.username: {{ (keycloak__admin_login | d({}))["username"] | d("(unset)") }}' + - '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state exists: {{ __keycloak__admin_login_bootstrapped }}' + - 'keycloak__db_login.username: {{ keycloak__db_login["username"] }}' + - 'keycloak__db_url_database: {{ keycloak__db_url_database }}' + - 'keycloak__db_url_host: {{ keycloak__db_url_host }}' + - 'keycloak__db_vendor: {{ keycloak__db_vendor }}' + - 'keycloak__expose_healthcheck_endpoints: {{ keycloak__expose_healthcheck_endpoints }}' + - 'keycloak__expose_metrics_endpoints: {{ keycloak__expose_metrics_endpoints }}' + - 'keycloak__hostname: {{ keycloak__hostname }}' + - 'keycloak__hostname_backchannel_dynamic: {{ keycloak__hostname_backchannel_dynamic }}' + - 'keycloak__https_certificate_file: {{ keycloak__https_certificate_file }}' + - 'keycloak__https_certificate_key_file: {{ keycloak__https_certificate_key_file }}' + - 'keycloak__https_protocols: {{ keycloak__https_protocols }}' + - 'keycloak__log: {{ keycloak__log }}' + - 'keycloak__log_file: {{ keycloak__log_file }}' + - 'keycloak__mode: {{ keycloak__mode }}' + - 'keycloak__proxy_headers: {{ keycloak__proxy_headers }}' + - 'keycloak__service_enabled: {{ keycloak__service_enabled }}' + - 'keycloak__spi_sticky_session_encoder_infinispan_should_attach_route: {{ keycloak__spi_sticky_session_encoder_infinispan_should_attach_route }}' + - 'keycloak__state: {{ keycloak__state }}' + - 'keycloak__version: {{ keycloak__version }}' tags: - 'always' @@ -101,7 +110,7 @@ group: 'root' mode: 0o640 vars: - __keycloak__render_bootstrap_creds: '{{ not __keycloak__bootstrap_marker["stat"]["exists"] }}' + __keycloak__include_bootstrap_creds: '{{ not __keycloak__admin_login_bootstrapped }}' notify: 'keycloak: systemctl restart keycloak' - name: 'Create keycloak.service' @@ -162,37 +171,82 @@ - name: 'Flush handlers so Keycloak restarts with the bootstrap credentials loaded' ansible.builtin.meta: 'flush_handlers' - - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }} (bootstrap admin gets provisioned during startup)' + # Port-up only proves Quarkus is listening, not that the bootstrap admin was provisioned. + # The admin-cli token request below is the actual readiness check; this wait_for just + # avoids spamming "connection refused" before Keycloak has bound the socket. + - name: 'Wait for Keycloak listener on port {{ __keycloak__listen_port }}' ansible.builtin.wait_for: port: '{{ __keycloak__listen_port }}' host: '127.0.0.1' - timeout: 180 + delay: 5 + sleep: 2 + timeout: 300 + vars: + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | length > 0) | ternary(8443, 8080) }}' + + # Verify the bootstrap admin actually exists with the configured password before touching + # the marker. If this fails, the marker stays absent and a re-run with corrected variables + # will re-render the credentials and retry. + - name: 'curl --data grant_type=password --data client_id=admin-cli {{ __keycloak__base_url }}/realms/master/protocol/openid-connect/token' + ansible.builtin.uri: + url: '{{ __keycloak__base_url }}/realms/master/protocol/openid-connect/token' + method: 'POST' + body_format: 'form-urlencoded' + body: + client_id: 'admin-cli' + grant_type: 'password' + password: '{{ keycloak__admin_login["password"] }}' + username: '{{ keycloak__admin_login["username"] }}' + status_code: 200 + validate_certs: false + register: '__keycloak__admin_token_result' + until: '__keycloak__admin_token_result["status"] | d(0) == 200' + retries: 30 + delay: 5 + no_log: true + changed_when: false + check_mode: false vars: - __keycloak__listen_port: '{{ (keycloak__https_certificate_file | d("") | length > 0) | ternary(8443, 8080) }}' + __keycloak__listen_port: '{{ (keycloak__https_certificate_file | length > 0) | ternary(8443, 8080) }}' + __keycloak__scheme: '{{ (keycloak__https_certificate_file | length > 0) | ternary("https", "http") }}' + __keycloak__base_url: '{{ __keycloak__scheme }}://127.0.0.1:{{ __keycloak__listen_port }}' + # `backup: false` is on purpose. The previous file contained the cleartext bootstrap password; + # writing a `.~` backup would leave those credentials on disk and defeat the + # whole point of this cleanup pass. - name: 'Deploy /etc/sysconfig/keycloak (without bootstrap credentials)' ansible.builtin.template: - backup: true + backup: false src: 'etc/sysconfig/keycloak-sysconfig.j2' dest: '/etc/sysconfig/keycloak' owner: 'root' group: 'root' mode: 0o640 vars: - __keycloak__render_bootstrap_creds: false + __keycloak__include_bootstrap_creds: false - - name: 'touch /opt/keycloak/.bootstrap_admin_done' + - name: 'mkdir -p /etc/ansible/facts.d' ansible.builtin.file: - path: '/opt/keycloak/.bootstrap_admin_done' - state: 'touch' - owner: 'keycloak' - group: 'keycloak' - mode: 0o644 + path: '/etc/ansible/facts.d' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 + + # `copy` with `content: ''` instead of `file` with `state: touch` so the task does not + # report changed on every run after the bootstrap is complete. + - name: 'Create state marker /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + ansible.builtin.copy: + content: '' + dest: '/etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state' + owner: 'root' + group: 'root' + mode: 0o600 + # block when: - - 'not __keycloak__bootstrap_marker["stat"]["exists"]' + - 'not (__keycloak__admin_login_bootstrapped | bool)' - 'keycloak__state == "started"' - tags: - 'keycloak' - 'keycloak:configure' diff --git a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 index d1f70e04..de3ab542 100644 --- a/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 +++ b/roles/keycloak/templates/etc/sysconfig/keycloak-sysconfig.j2 @@ -1,15 +1,15 @@ # {{ ansible_managed }} -# 2026051801 +# 2026051808 -{% if __keycloak__render_bootstrap_creds | d(false) | bool %} -# WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead -# WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead -KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login.username }} -KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login.password }} +{% if __keycloak__include_bootstrap_creds | d(false) | bool %} +{# WARN: Environment variable 'KEYCLOAK_ADMIN' is deprecated, use 'KC_BOOTSTRAP_ADMIN_USERNAME' instead #} +{# WARN: Environment variable 'KEYCLOAK_ADMIN_PASSWORD' is deprecated, use 'KC_BOOTSTRAP_ADMIN_PASSWORD' instead #} +KC_BOOTSTRAP_ADMIN_USERNAME={{ keycloak__admin_login["username"] }} +KC_BOOTSTRAP_ADMIN_PASSWORD={{ keycloak__admin_login["password"] }} {% else %} # Bootstrap admin credentials have been removed by the role after they served # their purpose on the first Keycloak start. Re-bootstrap (e.g. after a DB -# loss) by removing the state marker /opt/keycloak/.bootstrap_admin_done and +# loss) by removing /etc/ansible/facts.d/keycloak__admin_login_bootstrapped.state and # re-running the role with `keycloak__admin_login` defined. # # This file is intentionally kept on disk (empty of credentials) because