Back to the blog

Release v0.55.0

· 20 min readRelease notes

Welcome to the v0.55.0 release!

This release first comes with a large number of security fixes, several of them critical, so we strongly recommend upgrading as soon as possible.

On the dashboard side, you can now repeat a single panel, define annotations per panel, drag and drop panels between groups and lock the plugin versions used by a dashboard. Prometheus exemplars are supported, the search has been rewritten on the backend, and two new plugins are available: Canvas and JSON.

As usual, this blog post doesn't cover every change. For the complete list, check the changelog.

Security#

This release fixes a significant number of security vulnerabilities, several of them critical. Most of them can be exploited without authentication when Perses runs with its default configuration (security.enable_auth: false). We strongly recommend upgrading as soon as possible.

Some of these fixes are breaking changes, check the upgrade guide before upgrading and or the section Breaking changes below for more details.

We also published a security considerations page (#4512) describing how to deploy Perses safely.

Authentication bypass via forged JWTs (GHSA-mpfg-6jh9-3jmg)#

When authentication was enabled without setting security.encryption_key (or security.encryption_key_file), Perses fell back to a key hardcoded in its public source code. This key is also used to sign the JWTs, so anyone could forge a valid token for any user, including an admin.

Perses now refuses to start when authentication is enabled and no encryption key is provided.

Server-Side Request Forgery in the datasource proxy (GHSA-58wc-w2cq-7vf3)#

The datasource proxy (HTTP and SQL) connected to whatever destination the datasource defined. Combined with the "unsaved" proxy endpoints, Perses could be used to read any service reachable from the server: loopback, cloud metadata endpoints, the Kubernetes API, etc.

The destination of the proxy is now verified when a datasource is saved and before each request, including on every resolved IP address. Loopback, link-local, the cloud metadata endpoints and the Kubernetes API are denied by default. Private networks remain allowed. The restrictions can be adjusted in the new datasource.proxy configuration section:

datasource:
  proxy:
    allowed_networks: ["127.0.0.0/8", "::1/128"]

Arbitrary file read through secrets (GHSA-2qpg-wfx2-frv2)#

Secrets can reference files (passwordFile, credentialsFile, clientSecretFile, caFile, certFile, keyFile). These files could be anywhere on the server, so a user able to create a secret could exfiltrate any file readable by Perses through the datasource proxy.

File references are now rejected by default. They are only allowed in the directories listed in the new security.secret_file_allowed_directories configuration.

Caller credentials forwarded to the datasource (GHSA-phr8-237x-w7mj)#

The HTTP datasource proxy forwarded the Cookie and Authorization headers of the caller to the datasource. Anyone controlling a datasource could steal the Perses session of its users (including the refresh token) and take over their account.

The Cookie header of the caller is no longer forwarded, and neither is the Authorization header when the native authorization is enabled. To authenticate against a datasource, use a secret or the oauthPassthrough option.

Stored XSS through the datasource proxy (GHSA-8hm2-fqjh-pgj2)#

The responses of the datasource proxy are served from the Perses origin, almost unchanged. A malicious datasource could serve HTML and JavaScript running with the session of any user opening a link to it, set cookies on the Perses domain, or redirect users anywhere.

The proxy responses are now sandboxed with a strict Content-Security-Policy, and the headers that apply to the Perses origin (Set-Cookie, CORS headers, absolute redirections, etc.) are removed. As a consequence, the UI of a datasource (e.g. the Prometheus UI) can no longer be browsed through the proxy.

SQL proxy vulnerabilities#

Several vulnerabilities have been fixed in the SQL proxy:

  • Read-only restriction bypass (GHSA-r457-39g6-wpc6): queries modifying the data or the schema could get through the keyword check. Queries must now start with a read keyword (SELECT, WITH, SHOW, etc.) and are executed in a read-only transaction. MySQL multiStatements, as well as SELECT ... INTO OUTFILE / DUMPFILE, are rejected.
  • Arbitrary file read with MySQL/MariaDB (GHSA-92jg-r5jf-x5fh): the allowAllFiles driver option could be enabled from the datasource params, letting the database server read any file of the Perses server. This option, as well as allowCleartextPasswords and allowOldPasswords, is now always disabled.
  • MySQL/MariaDB TLS config mix-up (GHSA-jg7f-c7hw-wxgw): two datasources with colliding names could use each other's TLS configuration (CA, client certificate, verification settings). The TLS configuration is now set per connection.

Missing secret authorization in dashboard datasources (GHSA-mc34-vgvh-gpr7)#

A user allowed to edit a dashboard, but not to read secrets, could embed a datasource referencing any secret of the project and make the proxy send it to a server they control. Perses now checks that the user can read the secret used by a datasource embedded in a dashboard.

Wiping the database with a single request (GHSA-j8hv-r356-hfw6)#

With the file system database, deleting the project .. (DELETE /api/v1/projects/..) deleted the entire database. The name of the resource in the URL was never validated. Every parameter of the URL is now validated before being used.

Path traversal in the file system database (GHSA-63qw-3pm6-j4f3, GHSA-2wxp-jhwc-h2ff)#

The fix released in v0.54.0 for CVE-2026-63445 could be bypassed with a project named .. (e.g. GET /api/v1/variables?project=..), returning the whole database. The anonymous endpoint POST /api/validate/dashboards was also using the metadata.project of the dashboard without any validation.

The project name is now validated for every HTTP method and in the body of the requests, and the file system database itself rejects any key that would escape its directory.

Note that these issues (as well as the previous one) don't affect the SQL database.

Breaking changes#

Removal of the package @perses-dev/core#

As announced in v0.54.0, @perses-dev/core is no longer published (#4325). Its code has been dispatched in @perses-dev/spec, @perses-dev/client, @perses-dev/plugin-system and @perses-dev/components. It is also no longer shared with the plugins at runtime (perses/shared#246), so plugins still importing it must be migrated.

Dependency upgrades: React 17 dropped, Node 24, TypeScript 7 and end of CommonJS#

The frontend toolchain has been modernized across all the repositories:

If you embed Perses in your own application, you now have to register the Perses packages shared with the plugins before rendering your app (perses/shared#268):

import * as PersesClient from '@perses-dev/client';
import * as PersesComponents from '@perses-dev/components';
import * as PersesDashboards from '@perses-dev/dashboards';
import * as PersesExplore from '@perses-dev/explore';
import * as PersesPluginSystem from '@perses-dev/plugin-system';
import { registerHostSharedModules } from '@perses-dev/plugin-system';
import * as PersesSpec from '@perses-dev/spec';

registerHostSharedModules({
  '@perses-dev/spec': PersesSpec,
  '@perses-dev/client': PersesClient,
  '@perses-dev/components': PersesComponents,
  '@perses-dev/plugin-system': PersesPluginSystem,
  '@perses-dev/explore': PersesExplore,
  '@perses-dev/dashboards': PersesDashboards,
});

Removal of the old dashboard and datasource specification#

Since v0.54.0, the dashboard and datasource specifications live in perses/spec. The deprecated copies remaining in Perses have been removed (#4326): the Go packages pkg/model/api/v1/{common,dashboard,datasource,plugin,variable} and their CUE equivalents. If you still import them, switch to the ones provided by github.com/perses/spec (e.g. github.com/perses/spec/go/dashboard).

Go SDK: datasource.Selector is now a union type#

A datasource selector can now reference a variable, which is serialized as a plain string (e.g. "$myDatasource") (#4545). As a result, datasource.Selector no longer has the Kind and Name fields, but a Static or a Variable selector. Use the new constructors instead of a struct literal (#4557):

// Before
sel := &datasource.Selector{Kind: "PrometheusDatasource", Name: "myPrometheus"}

// After
static := datasource.NewStaticSelector("PrometheusDatasource", "myPrometheus")
variable, err := datasource.NewVariableSelector("myDatasource")
// Or let the SDK detect a variable reference ("$foo" or "${foo}")
auto, err := datasource.NewSelector("PrometheusDatasource", name)

The official plugins have been updated (perses/plugins#857): their Selector helper now detects variable references, and a new VariableSelector helper is available. If you maintain your own datasource plugin with a Selector helper, update it the same way.

Likewise, the deprecated CUE package cue/common/proxy has been removed from perses/shared (perses/shared#327). Use github.com/perses/spec/cue/datasource/proxy/http and github.com/perses/spec/cue/datasource/proxy/sql instead.

API changes in the TypeScript packages#

If you develop plugins or embed Perses, these changes in the @perses-dev/* packages may require an update:

  • useDeepMemo and useMemoized are removed, use React useMemo instead. The spec of a Resource and the cell values of TableColumnConfig are now unknown instead of any (perses/shared#310).
  • useId is no longer exported from @perses-dev/components, use React useId() instead (perses/shared#348).
  • Zod schemas are typed with z.ZodType<T, T> instead of z.ZodType<T> or the deprecated z.ZodSchema<T> (perses/shared#282, perses/spec#95).
  • The PanelEditorValues type and its schema are removed from @perses-dev/spec, use the ones from @perses-dev/plugin-system (perses/spec#59).
  • Annotations are now fetched on demand, only by the panels displaying them (perses/shared#330). The annotationState store field and the setAnnotationState action are removed, and usePanelAnnotationsWithData no longer returns hidden annotations.

Security changes#

Some of the security fixes require a configuration change on your side:

  • Encryption key: if authentication is enabled, security.encryption_key (or security.encryption_key_file) must be set, otherwise Perses won't start. If you were relying on the default key, the secrets previously stored can no longer be decrypted and must be re-created, and users will have to log in again.
  • Datasource proxy destinations: datasources pointing to loopback, link-local addresses, cloud metadata endpoints or the Kubernetes API are now rejected. If you rely on such datasources (e.g. a Prometheus running next to Perses on localhost), allow them through datasource.proxy.allowed_networks or datasource.proxy.allowed_hosts. URLs containing credentials (https://user:password@host) are also refused: use a secret instead.
  • Files referenced in secrets: they are rejected by default. List the directories containing them in security.secret_file_allowed_directories.
  • Resource names: a name can no longer contain .., nor start or end with . (perses/spec#102). Rename the resources using such a name.
  • Proxy headers: the Authorization header of the caller is no longer forwarded to the datasource when the native authorization is enabled, and the UI of a datasource can no longer be browsed through the proxy.

See the upgrade guide for more details.

Performance improvements#

This release comes with several performance improvements on the backend. The numbers below come from the benchmarks shared in the corresponding pull requests.

Datasource proxy reuses its connections#

Until now, the proxy created a new HTTP transport for every proxied request, so no TCP or TLS connection was ever reused. Perses now keeps one transport per saved datasource (#4521), which is particularly visible with HTTPS datasources:

CaseBeforeAfterGain
HTTP, sequential153 µs/op70 µs/op~2.2x
HTTP, parallel (10 CPUs)60 µs/op22 µs/op~2.8x
HTTPS, sequential1.69 ms/op72 µs/op~23x
HTTPS, parallel (10 CPUs)295 µs/op23 µs/op~13x

It also uses ~4x less memory per request. On a real network, each request additionally saves the round trips of the TCP and TLS handshakes. The connection limits can be tuned with the new datasource.proxy.http configuration:

datasource:
  proxy:
    http:
      max_conns_per_host: 0 # 0 = no limit
      max_idle_conns: 100
      max_idle_conns_per_host: 10

Faster native authorization#

The permission cache of the native authorization is rebuilt at startup, periodically, on every RBAC change and on the first login of every OAuth/OIDC user. Its build now runs in linear time (#4540). With 10,000 users and 1,000 projects:

BenchmarkBeforeAfterDelta
Cache refresh2.81 s21 ms−99.25%
25 simultaneous first OAuth logins12.68 s149 ms−98.82%

The cache is now also an immutable snapshot read without any lock (#4537). It fixes a deadlock that could hang every authorization check until a restart, and makes permission checks under concurrent load ~7x faster (from ~145 ns to ~20 ns per check).

Faster plugin extraction at startup#

Plugin archives are now extracted in parallel (#4507), and the ones already extracted are skipped (#4534). With the default plugins (32 archives, ~9,000 files), the extraction step goes from ~1–2 s to ~1 ms when the archives are already extracted, and the whole plugin initialization from ~1.35 s to ~0.3 s.

Skipping the archives benefits binary installations and deployments where the plugin folder is kept between restarts (e.g. mounted on a volume). With the official container image and its default setup, every new container still extracts all archives, but in parallel.

Better browser caching of static assets#

Content-hashed JS and CSS bundles (#4475), as well as fonts and images from the app and the plugins (#4533), are now served as immutable assets. Browsers no longer download or revalidate them on every page load.

Dashboard lock mode#

Dashboards don't pin the version of the plugins they use, so a plugin upgrade can change how a panel looks. The new lock mode (perses/shared#243) pins every plugin of a dashboard to the latest installed version. When a newer version is available, a button lets you update the plugins, with a preview of the change based on the first panel using it. If a pinned version is no longer installed, an error is displayed.

The panel type selector also shows the plugin version when several versions are installed.

The feature is disabled by default (#4370):

frontend:
  enable_lock_mode: true

Drag and drop panels between panel groups#

The grid layout library has been replaced by Snapgrid (perses/shared#296), so panels can now be dragged from one panel group to another. Drag and drop in lists (e.g. item actions) is also only triggered from the drag handle, so you can select text again (perses/shared#335).

Panel repeat#

Until now, only a panel group could be repeated over the values of a variable. A single panel can now be repeated as well (perses/spec#32, perses/shared#149). In the Layout tab of the panel editor, choose the variable, the alignment (horizontal or vertical) and the maximum number of panels per row or column:

layouts:
  - kind: Grid
    spec:
      items:
        - x: 0
          y: 0
          width: 12
          height: 6
          content:
            $ref: '#/spec/panels/cpu'
          repeatVariable:
            value: instance
            alignment: horizontal
            maxPer: 4

Only the selected values are used, and a group repeat takes precedence over a panel repeat. Repeated panels are also supported by the Grafana migration (#4328).

Panel-level annotations#

v0.54.0 introduced annotations at the dashboard level. They can now be defined on a panel as well (perses/spec#61, perses/shared#206, #4356). The panel displays the dashboard annotations plus its own ones.

A new Annotations tab is available in the panel editor for the plugins supporting it. For now, only the TimeSeriesChart does (perses/plugins#754). Panel annotations are validated by the backend like the dashboard ones (#4299), and the Go SDK can now build both of them (#4448).

Exemplars support#

The Prometheus datasource can now fetch exemplars (perses/plugins#799). It is disabled by default and must be enabled per datasource:

kind: GlobalDatasource
metadata:
  name: prometheus
spec:
  plugin:
    kind: PrometheusDatasource
    spec:
      exemplars:
        enable: true
      proxy:
        kind: HTTPProxy
        spec:
          url: https://prometheus.example.com

Exemplars are then fetched with range queries and displayed by the TimeSeriesChart as diamonds on top of their series (perses/plugins#800). Hovering one shows its labels (e.g. the trace ID), value and timestamp, and clicking it pins the tooltip (perses/shared#278).

Improved search experience#

The search is now done by the backend, through the new endpoint /api/v1/search/dashboards (#4355). Dashboards are indexed in memory and matched with a fuzzy search. With a SQL database, every Perses instance periodically checks for changes to keep its index in sync. By default, the name and the display name are indexed, but you can index any attribute, like the panel names:

search:
  index_keys:
    dashboard:
      - metadata.name
      - spec.display.name
      - spec.panels.@dig:display.name

The project view also uses this endpoint to list the dashboards, without loading their full definition (#4366), and its search bar now matches tags too (#4446).

Finally, the important dashboards of the home page can be organized in groups, and an entire project can be marked as important (#4506). The previous flat list is still supported but deprecated.

frontend:
  important_dashboards:
    - title: Perses demos
      dashboards:
        - project: perses
        - project: testing
    - title: Quick links
      description: Highlighted dashboards
      dashboards:
        - project: perses
          dashboard: nodeexporter

Authentication & authorization#

  • Roles from OAuth/OIDC claims (#4172): roles can be assigned from the claims of the identity provider token, without creating any RoleBinding. Without project, the role is a GlobalRole.

    security:
      authentication:
        providers:
          oidc:
            - slug_id: keycloak
              # ...
              claims:
                - claim_name: roles
                  mappings:
                    - claim_value: admin
                      role_name: admin
                    - claim_value: ops
                      role_name: editor
                      project: platform
  • OAuth pass-through (#4404): with oauthPassThrough: true in its HTTPProxy, a datasource is queried with the OAuth/OIDC token of the current user. The token is refreshed when needed.

  • Kubernetes impersonation (#4339): with the Kubernetes authorization, the Impersonate-User header is honored if the caller is allowed to impersonate this user.

  • Pre-hashed passwords (#4423): users can be provisioned with a bcrypt hash (spec.nativeProvider.passwordHash) instead of a cleartext password.

  • OIDC login property (#4468): like the OAuth provider, the OIDC provider supports custom_login_property to choose the user info field used as login (e.g. preferred_username).

HTTP proxy improvements#

The HTTP proxy used by the datasources received a lot of attention in this release: it is safer, faster and more configurable.

Header filtering policies#

Some datasources don't play well with certain headers sent by the browser or added by Perses (e.g. X-Forwarded-For). The HTTPProxy spec now supports two optional header policies (#4428):

  • allowHeaders: only the listed headers are forwarded to the datasource.
  • dropHeaders: the listed headers are removed, and the others are forwarded.
kind: Datasource
metadata:
  name: prometheus
  project: my-project
spec:
  default: true
  plugin:
    kind: PrometheusDatasource
    spec:
      proxy:
        kind: HTTPProxy
        spec:
          url: https://prometheus.example.com
          dropHeaders:
            - X-Forwarded-For
            - User-Agent

Header names are case-insensitive and only one of the two lists can be set. The filtering is applied after the headers defined in the datasource, but before the secret authentication, so there is no need to allow Authorization when the datasource uses a secret.

Configurable timeout#

The HTTPProxy spec also accepts a new optional timeout field, available in the datasource editor as well (#4525). To avoid letting users hold goroutines and sockets against unreachable hosts for too long, the timeout of a datasource is bounded by the server configuration:

datasource:
  proxy:
    http:
      default_timeout: 30s # used when the datasource doesn't define a timeout
      max_timeout: 1m # highest timeout a datasource can define

A datasource defining a timeout greater than max_timeout is rejected when it is saved, and its timeout is clamped when it is used.

Security hardening#

As described in the Security section, the proxy has been hardened in many ways:

  • its destination is verified against a list of allowed and denied networks/hosts (loopback, link-local, cloud metadata endpoints and Kubernetes API denied by default), configurable through datasource.proxy. The allowed schemes can be restricted with datasource.proxy.http.allowed_schemes;
  • the Cookie header of the caller is never forwarded, and neither is the Authorization header when the native authorization is enabled. An Authorization header defined in the headers of a datasource is ignored: use a secret instead;
  • its responses are sandboxed with a strict Content-Security-Policy, and the headers applying to the Perses origin are removed.

Some of these changes may require a configuration update, check the Security changes section and the proxy documentation for more details.

Connection reuse#

Finally, the proxy now keeps one HTTP transport per datasource and reuses its connections, making HTTPS requests up to ~23x faster. See Datasource proxy reuses its connections for the numbers and the new connection limits.

Refresh interval configuration#

The auto-refresh options of the dashboards can now be configured (#4432), or the auto-refresh can be disabled entirely (#4338). In that case, the interval picker is hidden and the refreshInterval of the dashboards, as well as the ?refresh= URL parameter, are ignored.

Similarly, administrators can define the default timezone, theme and number of rows per page, used when a user hasn't chosen their own (#4268).

frontend:
  auto_refresh:
    disable: false
    options: ["0s", "30s", "1m", "5m"]
  default_user_preferences:
    timezone: UTC
    theme: dark
    rows_per_page: 50

Provisioning improvements#

  • Perses can now watch the provisioning folders and apply the changes right away, instead of waiting for the next interval (#4136). Resources are also only updated when they actually changed, which avoids unnecessary writes and ever-increasing versions.
  • A provisioning file can contain several resources separated by --- (#4332).
provisioning:
  enable_watch: true
  folders:
    - /etc/perses/provisioning

CLI improvements#

  • The new command percli dac watch rebuilds your Dashboard-as-Code files on every change, including the dashboards depending on a modified library file (#3903). Combined with the provisioning watch mode, it gives you a live reload of your dashboards.
  • percli dac preview supports the --create-project flag, like percli apply (#4413).

Dashboard and plugin schemas API#

Perses now exposes the CUE schemas of the dashboards and of the installed plugins (#4505). It is particularly useful for tools generating dashboards, like the Perses MCP server:

  • GET /api/v1/schemas/dashboards: the complete dashboard schema, including the plugin schemas.
  • GET /api/v1/schemas/plugins: the schemas of all plugins.
  • GET /api/v1/schemas/plugins/<plugin_name>: the schema of a single plugin, with the optional version and registry query parameters.

Datasource improvements#

The datasource editor has a new Test Connection button to check the datasource is reachable before saving it (#4396, perses/shared#99). It is available for Prometheus (perses/plugins#620), OpenSearch (perses/plugins#797) and AlertManager (perses/plugins#798).

The Kubernetes datasource discovery can also mark a discovered datasource as the default one, based on its labels or annotations (#4368):

datasource:
  global:
    discovery:
      - name: prometheus
        kubernetes_sd:
          # ...
          default:
            enable: true
            labels:
              app.kubernetes.io/name: prometheus

New plugins#

Canvas#

The Canvas panel (perses/plugins#729) is a free-form diagram editor to build weathermap-like visualizations. Place nodes, connect them with edges and add background shapes or images. Nodes and edges can be bound to a query: their color then follows thresholds and the edge thickness can scale with the value.

Json#

The new JSON datasource and query (perses/plugins#770) can query any HTTP endpoint returning JSON, with GET or POST, query parameters and variables. The response can be transformed with a JSONata expression, and the result displayed in a Table panel.

Plugin improvements#

  • TimeSeriesChart:
    • the color palette can be selected again (perses/plugins#845). Auto keeps the current color by series name, the categorical palettes give more distinct colors.
    • rendering is faster, especially with many series (perses/plugins#832).
  • StatChart: multiple series are laid out in a grid adapting to the panel size, instead of a single row (perses/plugins#812, perses/plugins#814). Their values and legends also share the same font size, which can be customized (perses/plugins#778).
  • Table: sorting can be enabled for all columns at once (perses/plugins#759), and the new PivotByLabel transform turns series into a time × label matrix (perses/shared#288).
  • BarChart: colors can be overridden with regexes on the series or bar names (perses/plugins#774).
  • PieChart: the inner and outer radius can be configured, e.g. to display a donut (perses/plugins#823).
  • Markdown: external links open in a new tab (perses/plugins#815).
  • Prometheus: a query can be executed as an instant query, regardless of the panel (perses/plugins#752).
  • Pyroscope: profiles are queried through the Connect API instead of the legacy /render endpoint (perses/plugins#763). The minimum step can be configured on the datasource (perses/plugins#810), and the profile types and labels proposed are restricted to the selected service (perses/plugins#820, perses/plugins#835).
TimeSeriesChart color paletteStatChart grid layout

Other improvements#

  • Duplicate a dashboard into another project (#4536), including from a project where you only have read access.

  • Open in a new tab: the project and admin tabs, as well as the ephemeral dashboards, are now real links (#4252).

  • Page title: the browser tab title now reflects the current page (#4489).

  • Collapsed panel groups show the number of panels they contain (perses/shared#309).

  • Custom unit label: a panel can display its own unit label (e.g. trx/s) while keeping the formatting of a standard unit (perses/shared#297).

  • Grafana migration: the default time range, the refresh interval and the timezone of the Grafana dashboard are kept (#4555). Table thresholds are migrated to cell settings (perses/plugins#808), and datasource variables for Jaeger and OpenSearch are supported (perses/plugins#769).

  • Compressed requests: the API accepts request bodies compressed with gzip (Content-Encoding: gzip) (#4381).

  • Custom fetch: apps embedding Perses can override the fetch used by the client and the datasources through a fetch provider, e.g. to add custom headers (perses/shared#214, perses/shared#316, perses/plugins#854).