Skip to main content

Secure Jenkins with Pomerium

What this guide does

You'll put Jenkins behind Pomerium so that Pomerium handles single sign-on and authorization, then forwards a cryptographically signed identity JWT to Jenkins. The Jenkins JWT Auth plugin verifies that assertion against Pomerium's public keys and signs the user in automatically, so there's no second login and no Jenkins-managed password to store or share.

Jenkins verifies the assertion by fetching Pomerium's public keys from a JSON Web Key Set (JWKS) endpoint that Pomerium publishes, so the rest of this guide refers to that JWKS URL.

When to use this guide

Use it when you want one front door for Jenkins with your existing identity, and you want Jenkins to trust Pomerium for authentication instead of running its own user database. Jenkins has no built-in JWT support, so this flow depends on the JWT Auth plugin. If you only need to reach Jenkins over a private network without browser SSO, a plain TCP route is a better fit.

Prerequisites

This guide assumes you've completed the Quickstart, so you already have Pomerium running and signing users in through the hosted authenticate service.

You also need:

  • Docker and Docker Compose
  • A domain you control for the Jenkins route (this guide uses jenkins.yourdomain.com)
Prefer to self-host the identity provider (IdP)?

This guide uses the hosted authenticate service so you don't have to run an IdP. To run your own instead, follow Keycloak + Pomerium and swap the authenticate_service_url / idp_* settings into the config below.

Configure Pomerium

In the Zero Console:

  1. Create a Route. In From, enter https://jenkins.<your-starter-domain>; in To, enter http://jenkins:8080.

  2. Set the policy to Any Authenticated User (or scope it to a group or domain). To restrict by email domain, create a policy with an ALLOW block matching the Domain criteria.

    Zero Console policy builder with an ALLOW block matching the Domain criteria to pomerium.com

  3. On the Headers tab, enable Pass Identity Headers, then save the route.

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

Configure Jenkins

Jenkins doesn't read a JWT out of the box, so install the JWT Auth plugin and point it at Pomerium. After the stack is running and you've completed the Jenkins first-run wizard:

  1. Go to Manage Jenkins -> Plugins -> Available plugins, search for JWT Auth Plugin, install it, and restart Jenkins.

  2. Go to Manage Jenkins -> Security. In the Security Realm dropdown, select JWT Header Authentication Plugin and fill in the fields:

    FieldValue
    Header nameX-Pomerium-Jwt-Assertion
    Username claim nameemail
    Email claim nameemail
    Full Name claim namename
    Groups claim namegroups
    Groups claim list separator,
    Acceptable issuersyour route host
    Acceptable audiencesyour route host
    JWKS JSON URLthe JWKS URL for your setup (see the tab above)
    Leeway seconds30 (clock-skew tolerance when validating exp / nbf)

    Pomerium sets the JWT's iss and aud to the route host, so use the host from your From URL: jenkins.<your-starter-domain> on Zero, or jenkins.yourdomain.com on Core.

  3. Leave Allow verification failures unchecked once the flow works. While you're first wiring it up, you can enable it temporarily so a misconfiguration doesn't lock you out, then turn it off so only verified Pomerium requests are trusted.

  4. Under Authorization, pick a strategy. Logged-in users can do anything is a reasonable start, since Pomerium's route policy is the real access gate; tighten it with matrix-based security as needed. With Matrix-based security, click Add user... and grant the signed-in user the permissions they need (for example, Administer) so you aren't locked out.

    Jenkins matrix-based security with a user added and the Administer permission granted

Save and restart Jenkins.

Run the stack

The Compose file runs Pomerium Core and Jenkins together (for Zero, drop the pomerium service and use the compose.yaml from the Quickstart with your POMERIUM_ZERO_TOKEN, keeping the jenkins service below):

docker-compose.yaml
services:
pomerium:
image: pomerium/pomerium@sha256:e10d1d267af24f581157f485d9b0bc08469e2428675b696a08e42ceb09b2279c # v0.32.7
volumes:
- ./config.yaml:/pomerium/config.yaml:ro
- pomerium-cache:/data
ports:
- 443:443
- 80:80
restart: always

jenkins:
image: jenkins/jenkins@sha256:01c992ffef29dcf41c7164e8c16285c657c2368c4943dda8d68c93fdf54447d5 # lts (2.555.2)
# Jenkins is reached only through Pomerium, so no host ports are published.
# 8080 stays on the internal Docker network. You complete the first-run
# setup wizard and install the JWT Auth plugin through the browser, as the
# guide describes.
volumes:
- jenkins-home:/var/jenkins_home
restart: always

volumes:
pomerium-cache:
jenkins-home:

Start it:

docker compose up -d

Jenkins prints an initial admin password to the container log on first start (docker compose logs jenkins), which you'll need to complete the setup wizard the first time you reach it through Pomerium.

Verify the setup

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

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

  3. JWT SSO works. Once the JWT Auth plugin is configured, Jenkins signs you in automatically as the identity in the assertion. The top-right corner shows your account, and the Who Am I page (/whoAmI) reports your email under Name and IsAuthenticated?: true.

    Jenkins Who Am I page showing the signed-in user name, IsAuthenticated true, and the groups from the Pomerium JWT

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

Common failure modes

SymptomCauseFix
Jenkins shows its own login form instead of signing you inThe assertion didn't verifyConfirm the JWKS JSON URL is reachable from the Jenkins container, and that Pomerium has a signing_key (Core) so the JWKS isn't empty
unable to find valid certification path to requested target in the Jenkins logJenkins (the Java runtime) doesn't trust the certificate on the JWKS endpointWith the hosted authenticate service or a public certificate this won't happen; if you front Jenkins with a private CA, import that CA into the Java truststore
Unable to find a suitable verification key for the JSON Web Signature (JWS)Empty or unreachable JWKS: no published signing key, or the plugin can't fetch itSet a signing_key (Core) and confirm the JWKS URL resolves from inside the container
Redirect loop or certificate errorsDNS or autocertMake sure DNS for jenkins.yourdomain.com points at Pomerium and that Pomerium can get a TLS certificate (Core needs ports 80/443 open for autocert; Zero manages it)

Security considerations

  • The JWT Auth plugin trusts any request carrying an assertion it can verify, so don't expose Jenkins directly. Only Pomerium should reach jenkins:8080: keep it off published host ports and on the internal Docker network. If Jenkins is reachable directly, an attacker who can craft or replay an accepted header bypasses Pomerium entirely.
  • Keep Allow verification failures off in production. It's a recovery hatch while configuring the plugin, not a steady state: with it on, Jenkins accepts requests whose JWT didn't verify.
  • Scope the route policy (group or domain) to who should have access, and manage Jenkins permissions (matrix-based security) separately for fine-grained authorization.

Operations

  • Tear the stack down with docker compose down. Add -v to also delete the jenkins-home and pomerium-cache volumes, which resets Jenkins (including its JWT configuration) to a fresh first run.
  • Rotating the signing_key re-signs assertions under a new key. After rotation, Jenkins picks up the new key on its next JWKS fetch; restart Jenkins if you want it to re-fetch immediately.
  • Last tested with Jenkins LTS (jenkins/jenkins:lts, 2.555.2), the JWT Auth plugin, and Pomerium Core v0.32.7.

Next steps