Skip to main content

Certificates and TLS with Pomerium

This page covers downstream TLS (the connection between end users and Pomerium) and upstream TLS (the connection between Pomerium and the services behind it). Both can leverage mutual TLS (mTLS) for additional security in a zero-trust environment.

Downstream TLS

Downstream mTLS refers to a requirement that end users present a trusted client certificate when connecting to services secured by Pomerium. With ordinary TLS, only the server presents a certificate. With mTLS, the client must also present a certificate, and the server will only allow requests if the client's certificate is trusted.

note

Pomerium uses the term “downstream mTLS” for the client-to-Pomerium connection, and “upstream mTLS” for the connection between Pomerium and protected services. See Upstream TLS below for details on securing the connection to your upstream services.

Why use downstream mTLS?

  • Stronger Client Authentication: Each connecting user or device has its own certificate.
  • Zero-Trust: Certificates verify user identity at the network level, not just via credentials/SSO.
  • Compliance: Many security standards (e.g., PCI-DSS, HIPAA) recommend or require mutual authentication for sensitive data transport.

Before You Begin (Downstream TLS)

You will need:

  • A working Pomerium instance. Complete the Pomerium Core quickstart with Docker for a quick proof of concept.
  • mkcert, to issue certificates from a locally-trusted certificate authority (CA).
caution

mkcert is designed for local testing. Production environments require a more advanced certificate management system.

Configure Pomerium with a server certificate

If Pomerium already has a server certificate configured, you can skip to Create a client certificate.

  1. Create a Root CA
    Install mkcert, then run:

    mkcert -install

    This creates a trusted root CA for local development.

  2. Create a Wildcard TLS Certificate

    mkcert '*.localhost.pomerium.io'

    This produces _wildcard.localhost.pomerium.io.pem (certificate) and _wildcard.localhost.pomerium.io-key.pem (key).

  3. Update Pomerium Configuration
    Point certificate_file and certificate_key_file to these files:

    certificate_file: '_wildcard.localhost.pomerium.io.pem'
    certificate_key_file: '_wildcard.localhost.pomerium.io-key.pem'

    Make sure the paths align with your environment (e.g., Docker bind mounts).

Create a client certificate

mkcert -client -pkcs12 "yourUsername@localhost.pomerium.io"

This produces a file like yourUsername@localhost.pomerium.io-client.p12, containing both the client certificate and its private key.

Require mTLS in Pomerium

Add the downstream_mtls key to your config.yaml or environment variables, pointing to the rootCA.pem created by mkcert:

downstream_mtls:
ca_file: '/YOUR/MKCERT/CAROOT/rootCA.pem'

Pomerium now requires a client certificate for any route. Browsers without a valid client certificate will see a Pomerium error page.

Installing the client certificate (example: Chrome on Linux)

  1. Navigate to chrome://settings/certificates.
  2. Under Your Certificates, click Import and select yourUsername@localhost.pomerium.io-client.p12.
  3. Enter the default password changeit (from mkcert).
  4. A new “org-mkcert development certificate” entry appears in your certificate list.

When you visit a route like https://verify.localhost.pomerium.io, Chrome should prompt you to select the newly imported certificate. If everything is correct, you'll be granted access.

chrome settings

import client certificate

enter certificate password

certificate list

choose client certificate

Upstream TLS

Upstream TLS ensures that Pomerium and your protected services authenticate each other. By default, Pomerium authenticates user traffic but does not require or verify the identity of the upstream service. For zero-trust consistency, the upstream service should also present a valid TLS certificate, and Pomerium can optionally present its own client certificate (mTLS) to the service.

How Pomerium verifies upstream services

  • Custom CA: Point Pomerium to a trusted certificate authority using tls_custom_ca_file or tls_upstream_server_name. This ensures Pomerium will only trust upstreams signed by your preferred internal CA or other recognized CAs.
  • Client Certificate: If required by the upstream, Pomerium can present its own certificate and key (tls_client_cert_file and tls_client_key_file) to complete an mTLS handshake.

Example: mkcert + OpenSSL

This guide demonstrates a proof-of-concept using mkcert to generate self-signed certificates and an OpenSSL s_server as the upstream application.

Prerequisites

  • A root CA created by mkcert (as above).
  • A Pomerium configuration already set up (in all-in-one mode).
  • openssl installed on your system.

Steps

  1. Create Upstream Certificates

    mkcert openssl.localhost
    mkcert -client "pomerium@localhost"

    Move the resulting files to appropriate locations (e.g., /etc/pomerium/) and adjust ownership as needed.

  2. Start an OpenSSL Server

    openssl s_server \
    -key ./openssl.localhost-key.pem \
    -cert ./openssl.localhost.pem \
    -accept 44330 \
    -www

    This listens on port 44330 for TLS connections.

  3. Configure a Pomerium Route

    - from: https://openssl.localhost.pomerium.io
    to: https://localhost:44330
    tls_upstream_server_name: openssl.localhost
    policy:
    - allow:
    or:
    - email:
    is: user@example.com

    Visiting https://openssl.localhost.pomerium.io should show the following notice in your browser:

    OpenSSL Upstream without client certificate verification

    “no client certificate available” indicates that Pomerium is connecting but not supplying a certificate to the upstream yet.

  4. Require mTLS on the Upstream
    Restart the OpenSSL server with -Verify 1:

    openssl s_server \
    -Verify 1 \
    -key ./openssl.localhost-key.pem \
    -cert ./openssl.localhost.pem \
    -accept 44330 \
    -www

    Now the upstream expects a client certificate from its connection peer.

  5. Provide Pomerium's Client Certificate
    Update the route to include:

    tls_client_cert_file: /etc/pomerium/pomerium@localhost-client.pem
    tls_client_key_file: /etc/pomerium/pomerium@localhost-client-key.pem

    Refresh https://openssl.localhost.pomerium.io. The OpenSSL server's output should now show a validated client certificate from Pomerium.

More Resources

With both downstream TLS and upstream TLS in place, Pomerium ensures secure connections at every hop—users prove their identity to Pomerium, and Pomerium proves its identity to the protected services (and vice versa, if the service requires a client certificate). This consistent end-to-end encryption and mutual authentication is a key building block for a robust zero-trust architecture.