diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e5c2748f..94d7abfce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,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
* Add `playbooks/README.md` documenting all playbooks with their roles in execution order and available skip variables
* **role:apache_httpd**: Add platform-specific behavior section, wsgi example, and document localhost endpoints in README
* **role:apache_httpd**: Add skip variables section to README linking to relevant playbooks
diff --git a/roles/grafana/README.md b/roles/grafana/README.md
index 7dfbf1d3f..0e4f0e3cb 100644
--- a/roles/grafana/README.md
+++ b/roles/grafana/README.md
@@ -47,6 +47,9 @@ 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__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'` |
@@ -71,6 +74,9 @@ 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__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 65b29ff89..2afeaf56e 100644
--- a/roles/grafana/defaults/main.yml
+++ b/roles/grafana/defaults/main.yml
@@ -3,6 +3,9 @@ grafana__api_url: '{{ grafana__root_url }}'
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'
@@ -58,3 +61,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/tasks/main.yml b/roles/grafana/tasks/main.yml
index 504266adf..89182c727 100644
--- a/roles/grafana/tasks/main.yml
+++ b/roles/grafana/tasks/main.yml
@@ -26,6 +26,25 @@
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: '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: 'root'
+ group: 'grafana'
+ mode: 0o644
+ when: 'grafana__auth_jwt | bool'
+
- name: 'deploy /etc/grafana/grafana.ini'
ansible.builtin.template:
backup: true
diff --git a/roles/grafana/templates/etc/grafana/grafana.ini.j2 b/roles/grafana/templates/etc/grafana/grafana.ini.j2
index 63d7e99ea..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 }}
-# 2024031901
+# 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]
diff --git a/roles/icingaweb2_module_grafana/README.md b/roles/icingaweb2_module_grafana/README.md
index 3d732f8a6..d0cbe9e95 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,8 @@ icingaweb2_module_grafana__version: 'v3.1.1'
| Variable | Description | Default Value |
| -------- | ----------- | ------------- |
+| `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'` |
| `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 +55,8 @@ icingaweb2_module_grafana__version: 'v3.1.1'
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 5fa9b480b..ee7716b28 100644
--- a/roles/icingaweb2_module_grafana/defaults/main.yml
+++ b/roles/icingaweb2_module_grafana/defaults/main.yml
@@ -1,3 +1,5 @@
+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() }}'
diff --git a/roles/icingaweb2_module_grafana/tasks/main.yml b/roles/icingaweb2_module_grafana/tasks/main.yml
index 7b2658088..49c0a0773 100644
--- a/roles/icingaweb2_module_grafana/tasks/main.yml
+++ b/roles/icingaweb2_module_grafana/tasks/main.yml
@@ -88,6 +88,16 @@
group: 'icingaweb2'
mode: 0o660
+ - name: 'copy {{ icingaweb2_module_grafana__auth_jwt__priv_key_file }} to /etc/icingaweb2/modules/grafana/jwt.key.priv'
+ ansible.builtin.copy:
+ src: '{{ icingaweb2_module_grafana__auth_jwt__priv_key_file }}'
+ dest: '/etc/icingaweb2/modules/grafana/jwt.key.priv'
+ remote_src: true
+ owner: 'apache'
+ group: 'icingaweb2'
+ mode: 0o644
+ 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..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 }}
-; 2023050802
+; 2026032601
[grafana]
accessmode = "iframe"
@@ -9,9 +9,18 @@ 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"
timerangeAll = "1w/w"
+ssl_verifypeer = "0"
+ssl_verifyhost = "0"
+dashboardlink = "0"
+{% if icingaweb2_module_grafana__auth_jwt %}
+jwtEnable = "1"
+jwtUser = "grafana-admin"
+jwtIssuer = "{{ icingaweb2_module_grafana__url }}"
+jwtExpires = "30"
+{% endif %}