# Secure ToolJet with Pomerium

## What this guide does

[ToolJet](https://www.tooljet.com/) is a self-hosted, low-code platform for building internal tools. This guide puts a self-hosted ToolJet instance behind Pomerium so every request is authenticated against your identity provider before it reaches the app. Pomerium acts as the front door: unauthenticated visitors are redirected to your IdP and never touch ToolJet.

ToolJet keeps its own accounts and role-based access control, so Pomerium does not replace ToolJet's login. Instead, Pomerium gates network access to the instance, and ToolJet's RBAC governs what each signed-in user can do once inside.

## When to use this guide

Use this guide when you run ToolJet yourself with Docker Compose and want a single, identity-aware entry point in front of it. It is a good fit if you already protect other services with Pomerium and want ToolJet to follow the same access policy.

If you only need ToolJet's built-in SSO and have no other services to protect, you may not need Pomerium at all. The value here is a consistent policy boundary across all of your internal tools.

## Prerequisites

- A working Pomerium deployment. If you don't have one, follow the [quickstart](https://www.pomerium.com/docs/get-started/quickstart.md) first.
- [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/).
- A domain you control, with a DNS record pointing the ToolJet hostname (for example `tooljet.yourdomain.com`) at your Pomerium instance.

## Configure Pomerium

**Pomerium Zero:**

In the [Zero Console](https://console.pomerium.app), create a route for ToolJet:

1. Set **From** to the external URL you want, for example `https://tooljet.yourdomain.com`.
2. Set **To** to the ToolJet service, for example `http://tooljet:80`.
3. Under **Headers**, enable **Preserve host header**. ToolJet validates the incoming `Host` against its `TOOLJET_HOST` value, so the forwarded request must carry the public hostname rather than the internal service name.
4. Attach a policy that allows the users who should reach ToolJet.

That's the whole Pomerium side. Zero manages the hosted authenticate service and TLS for you.

**Pomerium Core:**

Create a `config.yaml` for Pomerium Core:

Replace `tooljet.yourdomain.com` with your hostname and `you@example.com` with the identity that should be allowed in. The `preserve_host_header: true` setting is what makes ToolJet accept the proxied request: ToolJet rejects any request whose `Host` header does not match `TOOLJET_HOST`. Because the route's external hostname already equals `TOOLJET_HOST`, preserving the incoming `Host` forwards the right value to the upstream.

This config uses the hosted authenticate service (`authenticate.pomerium.app`), so you don't run your own identity provider.

To run your own IdP instead, point `authenticate_service_url` at your own authenticate service and configure an OIDC provider. See the [Keycloak fallback](https://www.pomerium.com/docs/integrations/user-identity/oidc.md) for a fully self-hosted setup.

## Configure ToolJet

ToolJet needs a PostgreSQL database and a few secrets. Generate the two encryption keys before you start:

```bash
# LOCKBOX_MASTER_KEY: 32 bytes of hex
openssl rand -hex 32
# SECRET_KEY_BASE: 64 bytes of hex
openssl rand -hex 64
```

Put those values into the `LOCKBOX_MASTER_KEY` and `SECRET_KEY_BASE` environment variables in the Compose file below, and set `TOOLJET_HOST` to the same external URL you used in your Pomerium route. The `PG_*` variables point ToolJet at the bundled PostgreSQL service, and the `TOOLJET_DB_*` variables back ToolJet's built-in database, which reuses the same PostgreSQL server here.

You do not need to run migrations by hand. The `tooljet/tooljet-ce` image waits for PostgreSQL and runs its database setup automatically on first boot, which is why the first start takes a few minutes. The image also bundles Redis, so no separate Redis service is required for a single-instance deployment.

See ToolJet's [environment variable reference](https://docs.tooljet.com/docs/setup/env-vars) for the full list of options.

## Run the stack

The Compose file below runs Pomerium Core, ToolJet, and PostgreSQL together:

Start everything:

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

The first boot runs ToolJet's database migrations, so give it a few minutes. Watch `docker compose logs -f tooljet` until ToolJet reports it is ready.

## Verify the setup

1. In a private browser window, go to `https://tooljet.yourdomain.com`. Pomerium should redirect you to your identity provider rather than showing ToolJet.

2. Sign in with a user your policy allows. After login you should land on ToolJet's own setup screen, served through Pomerium:

   \[ToolJet's admin setup screen reached through Pomerium after SSO]

3. Complete ToolJet's first-run workspace setup and create your admin account. From here ToolJet's own RBAC governs the workspace.

4. **A disallowed user is blocked.** Sign in as a user your policy excludes and open `https://tooljet.yourdomain.com`. Pomerium should deny access, so you never reach ToolJet.

If an unauthenticated request reaches ToolJet directly, or an allowed user can't get past the IdP, revisit the route policy and the preserve-host-header setting.

## Common failure modes

- **ToolJet returns a host or "invalid host" error.** The forwarded `Host` header doesn't match `TOOLJET_HOST`. Confirm the route forwards the public hostname (`preserve_host_header: true` in Core, or **Preserve host header** in Zero) and that `TOOLJET_HOST` exactly matches your external URL, including the scheme.
- **ToolJet never becomes healthy on first boot.** Migrations run against PostgreSQL at startup; check `docker compose logs tooljet` and `docker compose logs postgres`. A wrong `PG_HOST`, `PG_USER`, `PG_PASS`, or `PG_DB` will stall the setup step.
- **Login loops or "signature" errors.** Make sure `LOCKBOX_MASTER_KEY` and `SECRET_KEY_BASE` are set to stable, sufficiently long values and aren't regenerated between restarts.

## Security considerations

Pomerium only protects ToolJet if ToolJet is unreachable except through Pomerium. Do not publish ToolJet's port directly; keep it on the internal Compose network so the proxy is the only path in. In the example, ToolJet is reachable on the Compose network as `http://tooljet:80` and is never published to the host.

Because ToolJet runs its own authentication and RBAC, users sign in twice: once at your IdP through Pomerium, and once to ToolJet. That is expected. Treat Pomerium as the network gate and ToolJet's RBAC as the in-app authorization layer, and keep both policies aligned so the two layers don't drift apart.

## Next steps

- Tighten the route policy to specific groups or domains. See [policy](https://www.pomerium.com/docs/reference/routes/policy.md).
- Review ToolJet's own [permissions model](https://docs.tooljet.com/docs/org-management/permissions) to map IdP groups onto ToolJet roles.
- Put your other internal tools behind the same Pomerium instance for one consistent access boundary.
