ObservabilityLXXIX · Securing GrafanaSecureGrafana
Plugin Security
What you'll learn
- Distinguish signed from unsigned Grafana plugins and explain the trust boundary each one creates
- Configure allow_loading_unsigned_plugins to allow exactly the unsigned plugin IDs required and nothing else
- Recognise the role of the admin-approved category for plugins that require per-org approval before they are usable
- Diagnose a plugin that fails to load and identify whether the cause is signature, version, or admin approval
- Audit installed plugins and revoke any that are unused, unsigned, or no longer maintained
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A Grafana 11.x install has the official plugins installed. An operator needs a community data source for an internal metrics backend and enables allow_loading_unsigned_plugins in grafana.ini to load it. The community plugin works. The next Grafana restart silently re-enables the same flag because the operator added it once and forgot. Two months later a new Grafana version is upgraded; the plugin is incompatible; the install falls back to a previous version of the plugin; that version has a known vulnerability that exfiltrates data source credentials through out-of-band HTTP. The audit finds the unsigned plugin was enabled eighteen months ago and the operator who enabled it has left the company. The unsigned flag is the only thing that distinguished the install from a Grafana running arbitrary code.
This lesson is about closing that gap. Grafana plugins are code loaded into the Grafana process. The signing model and the admin-approval gate are the two boundaries the operator has to defend.
What it is
A Grafana plugin is a bundle of front-end and back-end code that extends Grafana with a new panel, a new data source, or a new app. The bundle is signed by Grafana Labs with a private key; the public key is shipped with Grafana and used to verify the signature at load time. A plugin whose signature does not verify is refused to load, unless the operator has explicitly opted in through allow_loading_unsigned_plugins.
Operator installs plugin
|
v
grafana-cli plugins install <id>
|
v
Grafana restart
|
v
Plugin loader checks signature
|
+--------+--------+
| |
verify ok verify fail
| |
v v
load plugin check [plugins] allow_loading_unsigned_plugins
|
+-- id listed: load
+-- id absent: refuse, log error
Plugins are categorised in two ways.
By signature:
- Signed plugins — bundled by Grafana Labs, installed via the official plugin repository, signature verified at load time. Default trust.
- Unsigned plugins — community or in-house plugins that have not been signed by Grafana Labs. Require explicit opt-in.
By admin approval:
- Auto-enabled plugins — loaded and usable by every user the moment the plugin is installed.
- Admin-approved plugins — a category introduced in Grafana 10.x; the plugin is loaded by Grafana but is not usable by users until a Server Admin explicitly enables it for the org. Used for plugins that touch data the org should not expose by default.
The plugin type matters too. A panel plugin runs only in the browser; a data source plugin runs in the Grafana process on the server and has access to data source credentials; an app plugin can do both.
Why a sysadmin cares
A plugin is code. The same code that draws a panel on a dashboard also has access to the JavaScript context of the Grafana front-end; a data source plugin additionally runs in the Grafana back-end process with the same network access as Grafana itself. The signature model is the only thing that distinguishes a community plugin from a Grafana Labs plugin.
The four production failure shapes:
- Unsigned plugin enabled by accident. An operator enables allow_loading_unsigned_plugins to install one plugin and the flag stays on. Every subsequent plugin install is unsigned by default. The blast radius is every code path the plugin can reach.
- Plugin admin approval not used for sensitive plugins. A new data source plugin that touches production data is auto-enabled the moment it is installed. Users see it before the security team has reviewed it.
- Plugin upgrade pulls a malicious version. The plugin repository is compromised; an attacker publishes a new version of a legitimate plugin with embedded exfiltration. The signature verifies because Grafana Labs signed the bundle; the bundle contains attacker code. This is the supply-chain attack shape.
- Plugin abandoned but still installed. A community plugin is no longer maintained. A CVE is published; the install runs the vulnerable version because the operator never audited the plugin list.
How it works
The plugin loader runs at Grafana startup and on every plugin install. For each plugin it finds in the plugin directory, it:
- Reads the manifest (plugin.json) for the id, type, version, signature, and dependencies.
- Verifies the signature against the public key shipped with Grafana. A signed plugin has a MANIFEST.txt with a cryptographic signature; an unsigned plugin has none or an invalid one.
- Checks the [plugins] allow_loading_unsigned_plugins list. If the plugin id is listed, it loads regardless of signature.
- Loads the plugin. Panel plugins are loaded into the browser; data source plugins are loaded into the Grafana back-end process; app plugins are loaded into both.
The admin-approval gate is enforced at the org level. With [plugins] plugin_admin_enabled = true under Grafana 11.x, every plugin that has the adminApproval flag in its manifest is loaded but not usable until a Server Admin enables it for the org through the API:
POST /api/plugins/<pluginId>/settings
{ "enabled": true }
Plugin installed
|
v
Signature verified (or allowlisted)
|
v
Plugin loaded
|
+-- adminApproval flag false: usable by everyone
|
+-- adminApproval flag true: usable after Server Admin enables for org
How to configure it
Default off for unsigned plugins
# /etc/grafana/grafana.ini
[plugins]
allow_loading_unsigned_plugins =
plugin_admin_enabled = true
The allow_loading_unsigned_plugins list is empty by default; the flag exists in the file as documentation. Production does not enable any unsigned plugin unless there is a documented business need.
One specific unsigned plugin
[plugins]
allow_loading_unsigned_plugins = company-internal-datasource
plugin_admin_enabled = true
The list is comma-separated and matches plugin ids exactly. A typo in the id means the plugin fails to load silently; the Grafana log records the missing id at startup.
Install from the official repository
# CONFIGURATION: install a signed data source plugin.
grafana-cli plugins install grafana-iot-timeline
# Installed grafana-iot-timeline v1.0.0
# Restart Grafana after installation.
systemctl restart grafana-server
Install a community plugin
# CONFIGURATION: install an unsigned community plugin.
grafana-cli --pluginUrl https://example.com/community-plugin.zip plugins install community-plugin
# Installed community-plugin v0.1.0
# Edit grafana.ini to allow this specific id.
vi /etc/grafana/grafana.ini
systemctl restart grafana-server
Enable an admin-approved plugin for the org
# CONFIGURATION: Server Admin enables a plugin for the org.
curl -fsS -X POST -H "Authorization: Bearer ${GF_SA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"enabled": true}' \
https://grafana.example.com/api/plugins/grafana-iot-timeline/settings
How to validate it
# READ-ONLY: list installed plugins.
grafana-cli plugins ls
# id: grafana-iot-timeline version: 1.0.0
# id: grafana-clock-panel version: 1.0.0
# id: company-internal-datasource version: 0.1.0 # unsigned
# READ-ONLY: confirm an unsigned plugin is allowlisted.
grep -A 1 'allow_loading_unsigned_plugins' /etc/grafana/grafana.ini
# allow_loading_unsigned_plugins = company-internal-datasource
# READ-ONLY: confirm a plugin is admin-approved for the org.
curl -fsS -H "Authorization: Bearer ${GF_SA_TOKEN}" \
https://grafana.example.com/api/plugins/grafana-iot-timeline/settings | jq
# {"id":"grafana-iot-timeline","enabled":true,"pinned":false}
# READ-ONLY: enumerate installed plugin signatures.
for d in /var/lib/grafana/plugins/*/; do
id=$(basename "$d")
sig="${d}MANIFEST.txt"
if [ -f "$sig" ]; then
echo "$id: signed"
else
echo "$id: unsigned"
fi
done
# grafana-iot-timeline: signed
# grafana-clock-panel: signed
# company-internal-datasource: unsigned
# READ-ONLY: check the Grafana log for plugin load errors.
grep -E 'plugin|signature' /var/log/grafana/grafana.log | tail -20
# level=info msg="Plugin Loaded" pluginId=grafana-iot-timeline
# level=error msg="Failed to load plugin" pluginId=community-plugin
# error="plugin 'community-plugin' is unsigned and not in allow_loading_unsigned_plugins"
# READ-ONLY: confirm the Grafana version for CVE matching.
grafana-cli --version
# Grafana CLI version 11.2.0
How it can fail
The high-frequency plugin failure shapes from real Grafana installs.
- allow_loading_unsigned_plugins left populated with a wildcard or empty value. A typo from a deleted plugin is replaced with a wildcard during a refactor. Every unsigned plugin loads. The blast radius is every code path the unsigned plugin reaches.
- Plugin upgrade pulls a malicious version. The plugin repository is compromised. The signature verifies because the attacker signed the bundle with a stolen key (rare) or because the bundle was built from the legitimate source with embedded malicious code (supply-chain). The fix is plugin pinning to a known-good version and a Grafana-managed proxy in front of the plugin repository.
- Plugin abandoned but still installed. A community plugin is no longer maintained. A CVE is published; the install runs the vulnerable version. The fix is a quarterly plugin audit with a CVE feed cross-referenced.
- Admin-approved plugin auto-enabled for all orgs. A plugin with the adminApproval flag is loaded but not usable; an operator enables it for org 1 in the Grafana UI; the same flag is not honoured per-org and the plugin is usable for every org. Verify the behaviour on a staging copy before enabling.
- Plugin incompatible with the Grafana version. The plugin.json specifies a Grafana version range; the running Grafana is outside the range; the plugin refuses to load. The symptom is a panel that worked yesterday is now blank.
- Plugin requires a feature flag the Grafana version does not have. The plugin manifest references an API that was removed in the current Grafana. The symptom is a startup error of plugin requires feature X which is not available.
How to troubleshoot it
The diagnostic order matters: a missing panel, a missing data source, or a 500 from a query can all be plugin failures.
- Reproduce as the affected user. Open the dashboard as the user who reports the failure. Confirm the panel is missing or the data source returns an error.
- Check the Grafana log at debug. log.level = debug in [log] produces a line for every plugin load attempt including the signature verification result and the allow_loading_unsigned_plugins decision.
- Verify the plugin id and version. grafana-cli plugins ls. Confirm the installed plugin is the version the dashboard expects.
- Inspect the plugin manifest. Run
cat /var/lib/grafana/plugins/<id>/plugin.json | jqfor the installed plugin id. Confirm the Grafana version range and dependencies. - Test the boundary. Disable the plugin via the API. If the panel disappears cleanly, the plugin is loaded but failing. If the panel still shows, the panel is not from this plugin.
- For admin-approved plugins: GET the plugin settings endpoint for the plugin id. If enabled is false, the plugin is loaded but not usable for the org.
Security implications
- A plugin is code. Data source plugins run in the Grafana process with the same network access as the Grafana server. The signature is the only trust boundary.
- Signed is not safe; signed is verified. A signed plugin verified against the Grafana Labs key is the default. A signed plugin verified against a stolen key is a supply-chain compromise; pin plugin versions and audit upgrades.
- allow_loading_unsigned_plugins is an allowlist. A wildcard defeats the purpose. List specific ids; review the list on every Grafana upgrade.
- Admin approval is per-org by default. A plugin with adminApproval = true in the manifest is loaded but not usable until a Server Admin enables it for the specific org.
- The plugin directory is a code directory. Treat /var/lib/grafana/plugins as you would treat /usr/local/bin. Read access is fine; write access is Server Admin only.
Performance implications
- Plugins load at startup. A Grafana with hundreds of plugins pays the load cost on every restart. Disable plugins that are not in use; the load cost is paid even for plugins that no dashboard references.
- Data source plugins proxy through Grafana. A data source plugin runs in the Grafana process and makes the backend request on the browser behalf. The performance profile is the same as the built-in data sources; the proxy adds one hop.
- Plugin upgrades restart the plugin loader. A plugin upgrade does not require a full Grafana restart in every case; the plugin loader is the gate. Verify on a staging copy.
Production guidance
- allow_loading_unsigned_plugins empty by default; populated only with a comma-separated allowlist of specific ids.
- plugin_admin_enabled = true; review every admin-approved plugin before enabling for an org.
- Pin plugin versions; align with the Grafana version matrix.
- Quarterly plugin audit; cross-reference installed plugins with the CVE feed.
- Treat the plugin directory as code; restrict write access to Server Admin.
- Test every plugin upgrade on a staging copy before production.
Verification
You should now be able to answer:
- What is the difference between a signed and an unsigned Grafana plugin, and where is the trust boundary?
- What is the admin-approved plugin category, and how does it differ from auto-enabled plugins?
- Why is allow_loading_unsigned_plugins = * a failure shape, and what is the correct alternative?
- How does the Grafana plugin loader verify a signature at startup?
- What is the recovery path if a plugin is discovered to be loaded but unsigned on the main Grafana?
Quiz
Knowledge check · 8 questions
Q1. A Grafana data source plugin runs in which process boundary?
Q2. A signed Grafana plugin is always safe because the signature verifies against the Grafana Labs public key.
Q3. Which of these are required to load an unsigned Grafana plugin in production?
Q4. An admin-approved plugin (adminApproval = true in plugin.json) is:
Q5. Name the file inside a Grafana plugin directory that Grafana uses to verify the plugin signature.
Q6. A panel that worked yesterday is blank today after a Grafana upgrade. The data source is fine. The most likely cause is:
Q7. A wildcard pattern in allow_loading_unsigned_plugins is acceptable for a staging environment but should never reach production.
Q8. Which of these are true about Grafana plugin security in 11.x?
Passing score: 75%. Answers are checked in this browser.