# Secure Grafana with Pomerium

## What this guide does

Put Grafana behind Pomerium to gate it with SSO and route policy, then pass a signed identity JWT that lets Grafana sign users in automatically. Grafana verifies that JWT on each request, so there's no second login prompt or separate Grafana password for users.

```mermaid
flowchart LR
    Browser --> Pomerium["Pomerium<br/>authN + authZ<br/>signs identity JWT"]
    Pomerium -.->|"sign in"| IdP[Identity provider]
    Pomerium -->|"signed JWT"| Grafana["Grafana<br/>verifies JWT, auto sign-in"]
```

## When to use this guide

Use it when you want one front door for Grafana with your existing identity, and you want Grafana to trust Pomerium for authentication instead of running its own login. If you only need to reach Grafana over a private network without browser SSO, a plain [TCP route](https://www.pomerium.com/docs/capabilities/non-http.md) is a better fit.

## Prerequisites

This guide assumes you've completed the [Quickstart](https://www.pomerium.com/docs/get-started/quickstart.md), so you already have Pomerium running and signing users in through the hosted authenticate service.

You also need:

- [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/)
- A domain you control for the Grafana route (this guide uses `grafana.yourdomain.com`)

This guide uses the hosted authenticate service so you don't have to run an IdP. To run your own instead, follow [Keycloak + Pomerium](https://www.pomerium.com/docs/integrations/user-identity/oidc.md) and swap the `authenticate_service_url` / `idp_*` settings into the config below.

## Configure Pomerium

**Pomerium Zero:**

In the [Zero Console](https://console.pomerium.app):

1. Create a **Route**. In **From**, enter `https://grafana.<your-starter-domain>`; in **To**, enter `http://grafana:3000`.

2. Set the policy to **Any Authenticated User**.

   \[Creating a Grafana route in the Zero Console]

3. On the **Headers** tab, enable **Pass Identity Headers** and save.

   \[Configuring the headers settings for the Grafana route in the Zero Console]

Zero manages the route's TLS certificate and the signing key behind its starter domain, so Grafana's JWKS URL is your authenticate service: `https://authenticate.<your-starter-domain>/.well-known/pomerium/jwks.json`. Use that value for `GF_AUTH_JWT_JWK_SET_URL` in the next section.

**Pomerium Core:**

Create a `config.yaml`. It routes `grafana.yourdomain.com` to the Grafana container, passes identity headers, and sets a `signing_key` so Pomerium publishes a JWKS that Grafana can verify the forwarded assertion against.

Replace `grafana.yourdomain.com` with your domain, `you@example.com` with your email, and generate your own `signing_key` with the command in the comment. With Core, Grafana's JWKS URL is the route's own well-known endpoint (`https://grafana.yourdomain.com/.well-known/pomerium/jwks.json`), which is what the Compose file below uses.

## Configure Grafana

Grafana's [JWT authentication](https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/jwt/) reads the assertion Pomerium forwards and signs the user in. The key settings:

- `GF_AUTH_JWT_HEADER_NAME=X-Pomerium-Jwt-Assertion` — the header Pomerium sends the signed identity JWT in.
- `GF_AUTH_JWT_JWK_SET_URL` — where Grafana fetches Pomerium's public keys to verify the JWT (see the URL for your path above).
- `GF_AUTH_JWT_EMAIL_CLAIM` / `GF_AUTH_JWT_USERNAME_CLAIM=email` — which claim becomes the Grafana account.
- `GF_AUTH_JWT_AUTO_SIGN_UP=true` — create the Grafana user on first sign-in.

## Run the stack

The Compose file below runs Pomerium and Grafana together. Wire up Pomerium for your deployment, then start the stack:

**Pomerium Zero:**

Drop the `pomerium` service and use the `compose.yaml` from the Quickstart with your `POMERIUM_ZERO_TOKEN`, keeping the `grafana` service shown below.

**Pomerium Core:**

Keep the `pomerium` service shown below and place the `config.yaml` from the previous step next to this `docker-compose.yaml`.

```bash
docker compose up -d
```

## Verify the setup

1. **The route requires authentication.** In a fresh browser, open `https://grafana.yourdomain.com`. You should be redirected to sign in, not straight into Grafana.

2. **An allowed user gets in.** Sign in. Pomerium redirects you back to Grafana.

3. **JWT SSO works.** Grafana signs you in automatically. Open `/profile` and confirm your name, email, and username show **Synced via JWT**.

   \[Grafana profile page with Name, Email, and Username marked "Synced via JWT"]

4. **A disallowed user is blocked.** Sign in as a user your policy excludes and open `https://grafana.yourdomain.com`. Pomerium denies access, so no JWT is forwarded and you never reach Grafana.

## Common failure modes

- **Grafana shows its own login form instead of signing you in.** The JWT didn't verify. Check `GF_AUTH_JWT_JWK_SET_URL` is reachable from the Grafana container and that Pomerium has a `signing_key` (Core) so the JWKS isn't empty.
- **`failed to verify JWT: no keys found` in Grafana logs.** Same cause: no published signing key. Generate and set `signing_key` in `config.yaml`.
- **Redirect loop or certificate errors.** Make sure DNS for `grafana.yourdomain.com` points at Pomerium and that Pomerium can obtain a TLS certificate. On the Core path, `autocert` needs ports 80 and 443 reachable for Let's Encrypt; Zero manages certificates for you.

## Security considerations

- Grafana trusts any request carrying a JWT it can verify, so **don't expose Grafana directly** — only Pomerium should reach `grafana:3000`. Keep it off published ports and on the internal Docker network.
- `GF_AUTH_JWT_AUTO_SIGN_UP=true` grants a Grafana account to every user your Pomerium policy allows. Scope the route policy (group or domain) to who should have access, and manage Grafana org roles separately.

## Next steps

- [Build policies](https://www.pomerium.com/docs/get-started/fundamentals/zero/zero-build-policies.md)
- [Pass identity headers](https://www.pomerium.com/docs/reference/routes/pass-identity-headers-per-route.md)
- [Custom domains](https://www.pomerium.com/docs/capabilities/custom-domains.md)
