From 70f78ef0ad6d177e0a653e8a3e2b3b247ca8c0ee Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Mon, 23 Mar 2026 14:58:50 +0100 Subject: [PATCH 1/7] feat(roles/grafana): Add JWT support --- CHANGELOG.md | 2 ++ roles/grafana/README.md | 4 +++ roles/grafana/defaults/main.yml | 3 ++ .../templates/etc/grafana/grafana.ini.j2 | 31 +++++++++++++++- roles/icingaweb2_module_grafana/README.md | 6 ++-- .../defaults/main.yml | 1 + .../icingaweb2_module_grafana/tasks/main.yml | 35 +++++++++++++++++++ .../icingaweb2/modules/grafana/config.ini.j2 | 11 +++++- 8 files changed, 89 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58718e6e6..52413d49e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:icingaweb2_module_grafana**: Add JWT support +* **role:grafana**: Add JWT support * **role:mariadb_server**: Add `mariadb_server__cnf_wsrep_log_conflicts` and `mariadb_server__cnf_wsrep_retry_autocommit` variables * **role:mariadb_server**: Add `mariadb_server__cnf_wsrep_gtid_mode` variable to configure `wsrep_gtid_mode` for Galera * **role:openvpn_server**: Add `openvpn_server:crl` tag to allow deploying the certificate revocation list independently diff --git a/roles/grafana/README.md b/roles/grafana/README.md index 7dfbf1d3f..0e1a601f9 100644 --- a/roles/grafana/README.md +++ b/roles/grafana/README.md @@ -47,6 +47,8 @@ grafana__root_url: 'https://monitoring.example.com/grafana' | `grafana__auth_anonymous_enabled` | Whether to allow anonymous (passwordless) access or not. Possible options: `true` or `false` | `false` | | `grafana__auth_anonymous_org_name` | The organization name that should be used for unauthenticated users. | `'Main Org.'` | | `grafana__auth_anonymous_org_role` | The role for unauthenticated users. | `'Viewer'` | +| `grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests. | `false` | +| `grafana__auth_jwt_key_file` | Path to the public key file used to verify JWT signatures for Grafana authentication. | `/etc/grafana/icinga.pem` | | `grafana__bitwarden_collection_id` | Will be used to store the token of the created service accounts to this Bitwarden Collection. Can be obtained from the URL in Bitwarden WebGUI. | `'{{ lfops__bitwarden_collection_id | default() }}'` | | `grafana__bitwarden_organization_id` | Will be used to store the token of the created service accounts to this Bitwarden Organization. Can be obtained from the URL in Bitwarden WebGUI. | `'{{ lfops__bitwarden_organization_id | default() }}'` | | `grafana__cookie_samesite` | The [SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). Possible options:
* disabled
* lax
* none
* strict | `'lax'` | @@ -71,6 +73,8 @@ grafana__api_url: 'https://grafana01.example.com/grafana' grafana__auth_anonymous_enabled: false grafana__auth_anonymous_org_name: 'Main Org.' grafana__auth_anonymous_org_role: 'Viewer' +grafana__auth_jwt: false +grafana__auth_jwt_key_file: '/etc/grafana/icinga.pem' grafana__cookie_samesite: 'lax' grafana__https_config: cert_file: '/etc/ssl/ssl-certificate.crt' diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml index 65b29ff89..1c4c8c122 100644 --- a/roles/grafana/defaults/main.yml +++ b/roles/grafana/defaults/main.yml @@ -6,6 +6,8 @@ grafana__auth_anonymous_org_role: 'Viewer' grafana__bitwarden_collection_id: '{{ lfops__bitwarden_collection_id | default() }}' grafana__bitwarden_organization_id: '{{ lfops__bitwarden_organization_id | default() }}' grafana__cookie_samesite: 'lax' +grafana__auth_jwt: false +grafana__auth_jwt_key_file: '/etc/grafana/icinga.pem' grafana__plugins__dependent_var: [] grafana__plugins__group_var: [] grafana__plugins__host_var: [] @@ -58,3 +60,4 @@ grafana__serve_from_sub_path: false grafana__service_enabled: true grafana__skip_token_to_bitwarden: false grafana__validate_certs: true + diff --git a/roles/grafana/templates/etc/grafana/grafana.ini.j2 b/roles/grafana/templates/etc/grafana/grafana.ini.j2 index 63d7e99ea..7c5faa4b8 100644 --- a/roles/grafana/templates/etc/grafana/grafana.ini.j2 +++ b/roles/grafana/templates/etc/grafana/grafana.ini.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2024031901 +# 2026032301 ##################### Grafana Configuration Example ##################### # @@ -1217,3 +1217,32 @@ interval_year = YYYY # Enable or disable loading other base map layers ;enable_custom_baselayers = true + +{% if grafana__auth_jwt %} +[auth.jwt] +# By default, auth.jwt is disabled. +enabled = true + +# HTTP header to look into to get a JWT token. +header_name = X-JWT-Assertion + +# Specify a claim to use as a username to sign in. +username_claim = sub + +# Specify a claim to use as an email to sign in. +email_claim = sub + +# enable JWT authentication in the URL +url_login = true + +# PEM-encoded key file in PKIX, PKCS #1, PKCS #8 or SEC 1 format. +key_file = {{ grafana__auth_jwt_key_file }} + +# This can be seen as a required "subset" of a JWT Claims Set. +# expect_claims = {"iss": "https://icinga.yourdomain"} + +# role_attribute_path = contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' + +# To skip the assignment of roles and permissions upon login via JWT and handle them via other mechanisms like the user interface, we can skip the organization role synchronization with the following configuration. +skip_org_role_sync = true +{% endif %} diff --git a/roles/icingaweb2_module_grafana/README.md b/roles/icingaweb2_module_grafana/README.md index 3d732f8a6..c5b80562b 100644 --- a/roles/icingaweb2_module_grafana/README.md +++ b/roles/icingaweb2_module_grafana/README.md @@ -5,7 +5,7 @@ Additionally, it deploys the the graph configuration for the [Linuxfabrik Monito This role is tested with the following IcingaWeb2 Grafana Module versions: -* 3.0.1 +* 3.1.3 ## Mandatory Requirements @@ -36,7 +36,7 @@ Example: ```yaml # mandatory icingaweb2_module_grafana__monitoring_plugins_version: '1.2.0.11' -icingaweb2_module_grafana__version: 'v3.1.1' +icingaweb2_module_grafana__version: 'v3.1.3' ``` @@ -44,6 +44,7 @@ icingaweb2_module_grafana__version: 'v3.1.1' | Variable | Description | Default Value | | -------- | ----------- | ------------- | +| `icingaweb2_module_grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests | `false` | | `icingaweb2_module_grafana__custom_graphs_config` | Multiline string. Custom configuration for the Grafana Graphs, will be deployed to `/etc/icingweb2/modules/grafana/graphs.ini` along with the configuration for the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) | `''` | | `icingaweb2_module_grafana__default_dashboard` | Name of the default Grafana dashboard | `'Default'` | | `icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config` | Skip the deployment of the graph configuration for [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins). | `false` | @@ -53,6 +54,7 @@ icingaweb2_module_grafana__version: 'v3.1.1' Example: ```yaml # optional +icingaweb2_module_grafana__auth_jwt: false icingaweb2_module_grafana__custom_graphs_config: |- [icingacli-x509] dashboard = "Default" diff --git a/roles/icingaweb2_module_grafana/defaults/main.yml b/roles/icingaweb2_module_grafana/defaults/main.yml index 5fa9b480b..77919f567 100644 --- a/roles/icingaweb2_module_grafana/defaults/main.yml +++ b/roles/icingaweb2_module_grafana/defaults/main.yml @@ -4,6 +4,7 @@ icingaweb2_module_grafana__monitoring_plugins_version: '{{ lfops__monitoring_plu icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config: false icingaweb2_module_grafana__theme: 'light' icingaweb2_module_grafana__url: '{{ grafana__root_url }}' +icingaweb2_module_grafana__auth_jwt: '{{ grafana__auth_jwt }}' # ----------------------------------------------------------------------------- diff --git a/roles/icingaweb2_module_grafana/tasks/main.yml b/roles/icingaweb2_module_grafana/tasks/main.yml index a08996ee5..0a1485415 100644 --- a/roles/icingaweb2_module_grafana/tasks/main.yml +++ b/roles/icingaweb2_module_grafana/tasks/main.yml @@ -87,6 +87,41 @@ group: 'icingaweb2' mode: 0o660 + - name: 'generate JWT RSA private key' + community.crypto.openssl_privatekey: + path: '/etc/icingaweb2/modules/grafana/jwt.key.priv' + size: 2048 + type: 'RSA' + owner: 'apache' + group: 'icingaweb2' + mode: '0640' + when: icingaweb2_module_grafana__auth_jwt + + - name: 'generate JWT RSA public key' + community.crypto.openssl_publickey: + path: '/etc/icingaweb2/modules/grafana/jwt.key.pub' + privatekey_path: '/etc/icingaweb2/modules/grafana/jwt.key.priv' + owner: 'apache' + group: 'icingaweb2' + mode: '0644' + when: icingaweb2_module_grafana__auth_jwt + + - name: 'copy /etc/icingaweb2/modules/grafana/jwt.key.pub to /etc/grafana/icinga.pem' + ansible.builtin.copy: + src: '/etc/icingaweb2/modules/grafana/jwt.key.pub' + dest: '/etc/grafana/icinga.pem' + remote_src: true + owner: 'root' + group: 'root' + mode: '0644' + when: icingaweb2_module_grafana__auth_jwt + + - name: 'restart grafana' + ansible.builtin.systemd: + name: 'grafana-server' + state: 'restarted' + when: icingaweb2_module_grafana__auth_jwt + tags: - 'icingaweb2_module_grafana' - 'icingaweb2_module_grafana:configure' diff --git a/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 b/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 index 0b87a5816..e1b263308 100644 --- a/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 +++ b/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 @@ -1,5 +1,5 @@ ; {{ ansible_managed }} -; 2023050802 +; 2026032301 [grafana] accessmode = "iframe" @@ -15,3 +15,12 @@ shadows = "0" theme = "{{ icingaweb2_module_grafana__theme }}" timerange = "2d" timerangeAll = "1w/w" +ssl_verifypeer = "0" +ssl_verifyhost = "0" +dashboardlink = "0" +{% if icingaweb2_module_grafana__auth_jwt %} +jwtEnable = "1" +jwtUser = "grafana-admin" +jwtIssuer = "https://{{ (icingaweb2_module_grafana__url | split('://'))[1] }}" +jwtExpires = "30" +{% endif %} From 92dad08630a45cd7119b332c990409e6230bb6bc Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 27 Mar 2026 17:50:58 +0100 Subject: [PATCH 2/7] refactor(roles/icingaweb2_module_grafana): clean up and delegate to grafana role --- roles/icingaweb2_module_grafana/README.md | 2 ++ .../defaults/main.yml | 1 + .../icingaweb2_module_grafana/tasks/main.yml | 35 +++---------------- .../icingaweb2/modules/grafana/config.ini.j2 | 8 ++--- 4 files changed, 12 insertions(+), 34 deletions(-) diff --git a/roles/icingaweb2_module_grafana/README.md b/roles/icingaweb2_module_grafana/README.md index c5b80562b..7792070a3 100644 --- a/roles/icingaweb2_module_grafana/README.md +++ b/roles/icingaweb2_module_grafana/README.md @@ -45,6 +45,7 @@ icingaweb2_module_grafana__version: 'v3.1.3' | Variable | Description | Default Value | | -------- | ----------- | ------------- | | `icingaweb2_module_grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests | `false` | +| `icingaweb2_module_grafana__auth_jwt__priv_key_file` | Path to the private key file used for JWT-based Grafana authentication | `'{{ grafana__auth_jwt__priv_key_file }}'` | | `icingaweb2_module_grafana__custom_graphs_config` | Multiline string. Custom configuration for the Grafana Graphs, will be deployed to `/etc/icingweb2/modules/grafana/graphs.ini` along with the configuration for the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) | `''` | | `icingaweb2_module_grafana__default_dashboard` | Name of the default Grafana dashboard | `'Default'` | | `icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config` | Skip the deployment of the graph configuration for [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins). | `false` | @@ -55,6 +56,7 @@ Example: ```yaml # optional icingaweb2_module_grafana__auth_jwt: false +icingaweb2_module_grafana__auth_jwt__priv_key_file: '/etc/grafana/jwt.key.priv' icingaweb2_module_grafana__custom_graphs_config: |- [icingacli-x509] dashboard = "Default" diff --git a/roles/icingaweb2_module_grafana/defaults/main.yml b/roles/icingaweb2_module_grafana/defaults/main.yml index 77919f567..f5ffeb7da 100644 --- a/roles/icingaweb2_module_grafana/defaults/main.yml +++ b/roles/icingaweb2_module_grafana/defaults/main.yml @@ -5,6 +5,7 @@ icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config: false icingaweb2_module_grafana__theme: 'light' icingaweb2_module_grafana__url: '{{ grafana__root_url }}' icingaweb2_module_grafana__auth_jwt: '{{ grafana__auth_jwt }}' +icingaweb2_module_grafana__auth_jwt__priv_key_file: '{{ grafana__auth_jwt__priv_key_file }}' # ----------------------------------------------------------------------------- diff --git a/roles/icingaweb2_module_grafana/tasks/main.yml b/roles/icingaweb2_module_grafana/tasks/main.yml index 0a1485415..633156487 100644 --- a/roles/icingaweb2_module_grafana/tasks/main.yml +++ b/roles/icingaweb2_module_grafana/tasks/main.yml @@ -87,40 +87,15 @@ group: 'icingaweb2' mode: 0o660 - - name: 'generate JWT RSA private key' - community.crypto.openssl_privatekey: - path: '/etc/icingaweb2/modules/grafana/jwt.key.priv' - size: 2048 - type: 'RSA' - owner: 'apache' - group: 'icingaweb2' - mode: '0640' - when: icingaweb2_module_grafana__auth_jwt - - - name: 'generate JWT RSA public key' - community.crypto.openssl_publickey: - path: '/etc/icingaweb2/modules/grafana/jwt.key.pub' - privatekey_path: '/etc/icingaweb2/modules/grafana/jwt.key.priv' - owner: 'apache' - group: 'icingaweb2' - mode: '0644' - when: icingaweb2_module_grafana__auth_jwt - - - name: 'copy /etc/icingaweb2/modules/grafana/jwt.key.pub to /etc/grafana/icinga.pem' + - name: 'copy {{ icingaweb2_module_grafana__auth_jwt__priv_key_file }} to /etc/icingaweb2/modules/grafana/jwt.key.priv' ansible.builtin.copy: - src: '/etc/icingaweb2/modules/grafana/jwt.key.pub' - dest: '/etc/grafana/icinga.pem' + src: '{{ icingaweb2_module_grafana__auth_jwt__priv_key_file }}' + dest: '/etc/icingaweb2/modules/grafana/jwt.key.priv' remote_src: true owner: 'root' group: 'root' - mode: '0644' - when: icingaweb2_module_grafana__auth_jwt - - - name: 'restart grafana' - ansible.builtin.systemd: - name: 'grafana-server' - state: 'restarted' - when: icingaweb2_module_grafana__auth_jwt + mode: 0o644 + when: 'icingaweb2_module_grafana__auth_jwt' tags: - 'icingaweb2_module_grafana' diff --git a/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 b/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 index e1b263308..f45a44f16 100644 --- a/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 +++ b/roles/icingaweb2_module_grafana/templates/etc/icingaweb2/modules/grafana/config.ini.j2 @@ -1,5 +1,5 @@ ; {{ ansible_managed }} -; 2026032301 +; 2026032601 [grafana] accessmode = "iframe" @@ -9,8 +9,8 @@ defaultdashboard = "{{ icingaweb2_module_grafana__default_dashboard }}" defaultdashboardpanelid = "1" defaultdashboarduid = "default" defaultorgid = "1" -host = "{{ (icingaweb2_module_grafana__url | split('://'))[1] }}" -protocol = "{{ (icingaweb2_module_grafana__url | split('://'))[0] }}" +host = "{{ icingaweb2_module_grafana__url | ansible.builtin.urlsplit('hostname') }}{{ icingaweb2_module_grafana__url | ansible.builtin.urlsplit('path') }}" +protocol = "{{ icingaweb2_module_grafana__url | ansible.builtin.urlsplit('scheme') }}" shadows = "0" theme = "{{ icingaweb2_module_grafana__theme }}" timerange = "2d" @@ -21,6 +21,6 @@ dashboardlink = "0" {% if icingaweb2_module_grafana__auth_jwt %} jwtEnable = "1" jwtUser = "grafana-admin" -jwtIssuer = "https://{{ (icingaweb2_module_grafana__url | split('://'))[1] }}" +jwtIssuer = "{{ icingaweb2_module_grafana__url }}" jwtExpires = "30" {% endif %} From dd3f4eec5bc32cc1df2568d5c96ef730897f40c4 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 27 Mar 2026 17:54:15 +0100 Subject: [PATCH 3/7] feat(roles/grafana): add variable for JWT TLS key and issue cert via role --- roles/grafana/README.md | 6 ++- roles/grafana/defaults/main.yml | 5 ++- roles/grafana/tasks/main.yml | 17 +++++++ .../templates/etc/grafana/grafana.ini.j2 | 45 ++++--------------- 4 files changed, 33 insertions(+), 40 deletions(-) diff --git a/roles/grafana/README.md b/roles/grafana/README.md index 0e1a601f9..0e4f0e3cb 100644 --- a/roles/grafana/README.md +++ b/roles/grafana/README.md @@ -48,7 +48,8 @@ grafana__root_url: 'https://monitoring.example.com/grafana' | `grafana__auth_anonymous_org_name` | The organization name that should be used for unauthenticated users. | `'Main Org.'` | | `grafana__auth_anonymous_org_role` | The role for unauthenticated users. | `'Viewer'` | | `grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests. | `false` | -| `grafana__auth_jwt_key_file` | Path to the public key file used to verify JWT signatures for Grafana authentication. | `/etc/grafana/icinga.pem` | +| `grafana__auth_jwt__priv_key_file` | Path to the private key file used to verify JWT signatures for Grafana authentication. | `'/etc/grafana/jwt.key.priv'` | +| `grafana__auth_jwt__pub_key_file` | Path to the public key file used to verify JWT signatures for Grafana authentication. | `'/etc/grafana/jwt.key.pub'` | | `grafana__bitwarden_collection_id` | Will be used to store the token of the created service accounts to this Bitwarden Collection. Can be obtained from the URL in Bitwarden WebGUI. | `'{{ lfops__bitwarden_collection_id | default() }}'` | | `grafana__bitwarden_organization_id` | Will be used to store the token of the created service accounts to this Bitwarden Organization. Can be obtained from the URL in Bitwarden WebGUI. | `'{{ lfops__bitwarden_organization_id | default() }}'` | | `grafana__cookie_samesite` | The [SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite). Possible options:
* disabled
* lax
* none
* strict | `'lax'` | @@ -74,7 +75,8 @@ grafana__auth_anonymous_enabled: false grafana__auth_anonymous_org_name: 'Main Org.' grafana__auth_anonymous_org_role: 'Viewer' grafana__auth_jwt: false -grafana__auth_jwt_key_file: '/etc/grafana/icinga.pem' +grafana__auth_jwt__priv_key_file: '/etc/grafana/jwt.key.priv' +grafana__auth_jwt__pub_key_file: '/etc/grafana/jwt.key.pub' grafana__cookie_samesite: 'lax' grafana__https_config: cert_file: '/etc/ssl/ssl-certificate.crt' diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml index 1c4c8c122..20986c7e3 100644 --- a/roles/grafana/defaults/main.yml +++ b/roles/grafana/defaults/main.yml @@ -1,13 +1,14 @@ grafana__allow_embedding: true grafana__api_url: '{{ grafana__root_url }}' +grafana__auth_jwt: false +grafana__auth_jwt__priv_key_file: '/etc/grafana/jwt.key.priv' +grafana__auth_jwt__pub_key_file: '/etc/grafana/jwt.key.pub' grafana__auth_anonymous_enabled: false grafana__auth_anonymous_org_name: 'Main Org.' grafana__auth_anonymous_org_role: 'Viewer' grafana__bitwarden_collection_id: '{{ lfops__bitwarden_collection_id | default() }}' grafana__bitwarden_organization_id: '{{ lfops__bitwarden_organization_id | default() }}' grafana__cookie_samesite: 'lax' -grafana__auth_jwt: false -grafana__auth_jwt_key_file: '/etc/grafana/icinga.pem' grafana__plugins__dependent_var: [] grafana__plugins__group_var: [] grafana__plugins__host_var: [] diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml index 39122ee80..6cd75aaa2 100644 --- a/roles/grafana/tasks/main.yml +++ b/roles/grafana/tasks/main.yml @@ -26,6 +26,23 @@ when: - 'grafana__https_config is defined and grafana__https_config | length > 0' + - name: 'generate JWT RSA private key' + community.crypto.openssl_privatekey: + path: '{{ grafana__auth_jwt__priv_key_file }}' + size: 2048 + type: 'RSA' + owner: 'apache' + group: 'icingaweb2' + mode: 0o644 + + - name: 'generate JWT RSA public key' + community.crypto.openssl_publickey: + path: '{{ grafana__auth_jwt__pub_key_file }}' + privatekey_path: '{{ grafana__auth_jwt__priv_key_file }}' + owner: 'apache' + group: 'icingaweb2' + mode: 0o644 + - name: 'deploy /etc/grafana/grafana.ini' ansible.builtin.template: src: 'etc/grafana/grafana.ini.j2' diff --git a/roles/grafana/templates/etc/grafana/grafana.ini.j2 b/roles/grafana/templates/etc/grafana/grafana.ini.j2 index 7c5faa4b8..2d186872e 100644 --- a/roles/grafana/templates/etc/grafana/grafana.ini.j2 +++ b/roles/grafana/templates/etc/grafana/grafana.ini.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2026032301 +# 2026032701 ##################### Grafana Configuration Example ##################### # @@ -606,16 +606,18 @@ hide_version = true #################################### Auth JWT ########################## [auth.jwt] -;enabled = true -;header_name = X-JWT-Assertion -;email_claim = sub -;username_claim = sub +enabled = {{ grafana__auth_jwt | lower }} +header_name = X-JWT-Assertion +email_claim = sub +username_claim = sub ;jwk_set_url = https://foo.bar/.well-known/jwks.json ;jwk_set_file = /path/to/jwks.json ;cache_ttl = 60m ;expected_claims = {"aud": ["foo", "bar"]} -;key_file = /path/to/key/file -;auto_sign_up = false +key_file = {{ grafana__auth_jwt__pub_key_file }} +auto_sign_up = false +url_login = true +skip_org_role_sync = true #################################### Auth LDAP ########################## [auth.ldap] @@ -1217,32 +1219,3 @@ interval_year = YYYY # Enable or disable loading other base map layers ;enable_custom_baselayers = true - -{% if grafana__auth_jwt %} -[auth.jwt] -# By default, auth.jwt is disabled. -enabled = true - -# HTTP header to look into to get a JWT token. -header_name = X-JWT-Assertion - -# Specify a claim to use as a username to sign in. -username_claim = sub - -# Specify a claim to use as an email to sign in. -email_claim = sub - -# enable JWT authentication in the URL -url_login = true - -# PEM-encoded key file in PKIX, PKCS #1, PKCS #8 or SEC 1 format. -key_file = {{ grafana__auth_jwt_key_file }} - -# This can be seen as a required "subset" of a JWT Claims Set. -# expect_claims = {"iss": "https://icinga.yourdomain"} - -# role_attribute_path = contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' - -# To skip the assignment of roles and permissions upon login via JWT and handle them via other mechanisms like the user interface, we can skip the organization role synchronization with the following configuration. -skip_org_role_sync = true -{% endif %} From 453c1276e9fb44fe094a4c7767cd8f56622b2f04 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 27 Mar 2026 17:58:31 +0100 Subject: [PATCH 4/7] refactor(roles/icingaweb2_module_grafana): fix sort order --- roles/icingaweb2_module_grafana/defaults/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/icingaweb2_module_grafana/defaults/main.yml b/roles/icingaweb2_module_grafana/defaults/main.yml index f5ffeb7da..ee7716b28 100644 --- a/roles/icingaweb2_module_grafana/defaults/main.yml +++ b/roles/icingaweb2_module_grafana/defaults/main.yml @@ -1,11 +1,11 @@ +icingaweb2_module_grafana__auth_jwt: '{{ grafana__auth_jwt }}' +icingaweb2_module_grafana__auth_jwt__priv_key_file: '{{ grafana__auth_jwt__priv_key_file }}' icingaweb2_module_grafana__custom_graphs_config: '' icingaweb2_module_grafana__default_dashboard: 'Default' icingaweb2_module_grafana__monitoring_plugins_version: '{{ lfops__monitoring_plugins_version | default() }}' icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config: false icingaweb2_module_grafana__theme: 'light' icingaweb2_module_grafana__url: '{{ grafana__root_url }}' -icingaweb2_module_grafana__auth_jwt: '{{ grafana__auth_jwt }}' -icingaweb2_module_grafana__auth_jwt__priv_key_file: '{{ grafana__auth_jwt__priv_key_file }}' # ----------------------------------------------------------------------------- From b1f63f1def57e42992d3d0311fb611ab18a131a4 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Fri, 27 Mar 2026 18:03:54 +0100 Subject: [PATCH 5/7] fix(roles/icingaweb2_module_grafana): fix private key file ownership --- roles/icingaweb2_module_grafana/tasks/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/icingaweb2_module_grafana/tasks/main.yml b/roles/icingaweb2_module_grafana/tasks/main.yml index 633156487..f04befd0c 100644 --- a/roles/icingaweb2_module_grafana/tasks/main.yml +++ b/roles/icingaweb2_module_grafana/tasks/main.yml @@ -92,8 +92,8 @@ src: '{{ icingaweb2_module_grafana__auth_jwt__priv_key_file }}' dest: '/etc/icingaweb2/modules/grafana/jwt.key.priv' remote_src: true - owner: 'root' - group: 'root' + owner: 'apache' + group: 'icingaweb2' mode: 0o644 when: 'icingaweb2_module_grafana__auth_jwt' From c67b6f3498ec00d020e5f47df181359de0f1a860 Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Thu, 2 Apr 2026 15:10:18 +0200 Subject: [PATCH 6/7] fix(roles/grafana): correct jwt key file permissions and task conditions --- roles/grafana/defaults/main.yml | 6 +++--- roles/grafana/tasks/main.yml | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/roles/grafana/defaults/main.yml b/roles/grafana/defaults/main.yml index 20986c7e3..2afeaf56e 100644 --- a/roles/grafana/defaults/main.yml +++ b/roles/grafana/defaults/main.yml @@ -1,11 +1,11 @@ grafana__allow_embedding: true grafana__api_url: '{{ grafana__root_url }}' -grafana__auth_jwt: false -grafana__auth_jwt__priv_key_file: '/etc/grafana/jwt.key.priv' -grafana__auth_jwt__pub_key_file: '/etc/grafana/jwt.key.pub' grafana__auth_anonymous_enabled: false grafana__auth_anonymous_org_name: 'Main Org.' grafana__auth_anonymous_org_role: 'Viewer' +grafana__auth_jwt: false +grafana__auth_jwt__priv_key_file: '/etc/grafana/jwt.key.priv' +grafana__auth_jwt__pub_key_file: '/etc/grafana/jwt.key.pub' grafana__bitwarden_collection_id: '{{ lfops__bitwarden_collection_id | default() }}' grafana__bitwarden_organization_id: '{{ lfops__bitwarden_organization_id | default() }}' grafana__cookie_samesite: 'lax' diff --git a/roles/grafana/tasks/main.yml b/roles/grafana/tasks/main.yml index 6cd75aaa2..0aa19cb2e 100644 --- a/roles/grafana/tasks/main.yml +++ b/roles/grafana/tasks/main.yml @@ -31,17 +31,19 @@ path: '{{ grafana__auth_jwt__priv_key_file }}' size: 2048 type: 'RSA' - owner: 'apache' - group: 'icingaweb2' + owner: 'root' + group: 'grafana' mode: 0o644 + when: 'grafana__auth_jwt | bool' - name: 'generate JWT RSA public key' community.crypto.openssl_publickey: path: '{{ grafana__auth_jwt__pub_key_file }}' privatekey_path: '{{ grafana__auth_jwt__priv_key_file }}' - owner: 'apache' - group: 'icingaweb2' + owner: 'root' + group: 'grafana' mode: 0o644 + when: 'grafana__auth_jwt | bool' - name: 'deploy /etc/grafana/grafana.ini' ansible.builtin.template: From 5179dac7c21821d795fcbbf2d45229066880607f Mon Sep 17 00:00:00 2001 From: Danyal Berchtold Date: Thu, 2 Apr 2026 15:12:31 +0200 Subject: [PATCH 7/7] docs(roles/icingaweb2_module_grafana): update default value for auth_jwt --- roles/icingaweb2_module_grafana/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/icingaweb2_module_grafana/README.md b/roles/icingaweb2_module_grafana/README.md index 7792070a3..d0cbe9e95 100644 --- a/roles/icingaweb2_module_grafana/README.md +++ b/roles/icingaweb2_module_grafana/README.md @@ -44,7 +44,7 @@ icingaweb2_module_grafana__version: 'v3.1.3' | Variable | Description | Default Value | | -------- | ----------- | ------------- | -| `icingaweb2_module_grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests | `false` | +| `icingaweb2_module_grafana__auth_jwt` | Enable JWT-based authentication for Grafana requests | `'{{ grafana__auth_jwt }}'` | | `icingaweb2_module_grafana__auth_jwt__priv_key_file` | Path to the private key file used for JWT-based Grafana authentication | `'{{ grafana__auth_jwt__priv_key_file }}'` | | `icingaweb2_module_grafana__custom_graphs_config` | Multiline string. Custom configuration for the Grafana Graphs, will be deployed to `/etc/icingweb2/modules/grafana/graphs.ini` along with the configuration for the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) | `''` | | `icingaweb2_module_grafana__default_dashboard` | Name of the default Grafana dashboard | `'Default'` |