# 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.

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](#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](https://www.pomerium.com/docs/get-started/quickstart.md) with Docker for a quick proof of concept.
- [`mkcert`](https://github.com/FiloSottile/mkcert#installation), to issue certificates from a locally-trusted certificate authority (CA).

`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](#create-a-client-certificate).

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

   ```bash
   mkcert -install
   ```

   This creates a trusted root CA for local development.

2. **Create a Wildcard TLS Certificate**

   ```bash
   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:
   ```yaml
   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

```bash
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`:

```yaml
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`](https://www.pomerium.com/docs/reference/routes/tls.md). 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](https://www.openssl.org/docs/manmaster/man1/openssl-s_server.html) 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**

   ```bash
   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**

   ```bash
   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**

   ```yaml
   - 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`:

   ```bash
   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:
   ```yaml
   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

- [Mutual Authentication: A Component of Zero Trust](https://www.pomerium.com/docs/internals/mutual-auth.md)
- [Client-Side mTLS With Pomerium](https://www.pomerium.com/docs/internals/mutual-auth.md)
- [JWT Verification](https://www.pomerium.com/docs/capabilities/getting-users-identity.md)

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.
