Skip to main content

Build Advanced Routes

Now that you’ve built advanced policies, let’s build on your routes a bit more.

Before You Start

Make sure you’ve completed the following tutorials:

Each tutorial builds on the same configuration files. In this tutorial, you’ll build new routes with some of Pomerium’s route-level settings.

Advanced route configuration settings

Pomerium provides route-level settings that allow you to customize how the proxy service handles requests. More advanced configurations allow identity header pass-through, path and prefix rewrites, and request and response header modifications.

For the purposes of this tutorial, we will only review the following settings to give you an idea of how you can further configure your routes for your use case:

  • Remove Request Headers
  • Host Rewrite Headers
  • Set Response Headers
  • Redirects
  • Prefix & Prefix Rewrite

Configure HTTPBin

To see how some of these settings work, we will configure Pomerium and Docker Compose to host an HTTPBin server. If you’re not familiar with HTTPBin, it’s a call-and-response HTTP server you can use to test (you guessed it) HTTP requests and responses.

Add the httpbin service to your Docker Compose file:

docker-compose.yaml
httpbin:
image: kennethreitz/httpbin
ports:
- 80:80

Add the httpbin route in your Pomerium configuration file:

config.yaml
- from: https://httpbin.localhost.pomerium.io
to: http://httpbin:80

Add pass_identity_headers to your httpbin route:

config.yaml
pass_identity_headers: true

Then add a simple policy:

policy:
allow:
and:
- domain:
is: example.com

Remember: All of these are being added to your httpbin route!

Now, run Docker Compose, access the httpbin route, and inspect a request:

  1. Navigate to: https://httpbin.localhost.pomerium.io

  2. Select Request inspection

  3. Select the /headers row

  4. Select Try it out, Execute

Now, scroll down to Response body. You should see a payload like this:

{
"headers": {
"Accept": "application/json",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-US,en;q=0.9",
"Cookie": "",
"Host": "httpbin",
"Referer": "https://httpbin.localhost.pomerium.io/",
"Sec-Ch-Ua": "\"Google Chrome\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "\"macOS\"",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"X-Envoy-Expected-Rq-Timeout-Ms": "30000",
"X-Envoy-Internal": "true",
"X-Pomerium-Jwt-Assertion": "..."
}
}

Because we added pass_identity_headers, we can see that the request includes the X-Pomerium-Jwt-Assertion header. This tells us that the identity header has been correctly passed to the upstream application (in this case, to HTTPBin).

Since we’re forwarding the JWT, let’s try adding the JWT Claims Headers global setting to your configuration file, right under the signing_key:

signing_key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUVSNThaeDA2SHJXTW9PUTRaNjlMaDdMZUtFZW5TSmJZcHJvZ3V3TEl0blNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFK1FtamZKQ2ovdzkrOUhrRDVlbTlIZFhRM3ViUEhIdWNOMTlNOXJxR05PeEpTRmR3VHgvaAphdVkvcVFSWWR0YVpnVEpEUWZSYVQ2Q1pPYndSYTl2TXNnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

jwt_claims_headers:
X-Pomerium-Claim-Email: email
X-Pomerium-Claim-User: user
X-Pomerium-Claim-Name: name

This setting sends JWT claims as unsigned headers to the upstream application (unlike the signed JWT assertion header). If you restart the Pomerium Docker instance and send another request to HTTPBin, you’ll notice these claims are included in the request:

View JWT claims as unsigned headers in the request

So, this adds headers from the JWT to our request, but what if you wanted to remove them for privacy or security reasons? Or what if you wanted to add other headers to your request?

This is where Pomerium’s flexibility comes in! Let’s try a few examples using our header settings.

Set and remove request headers

Under your httpbin route, add the following settings:

- from: https://httpbin.localhost.pomerium.io
to: http://httpbin:80
set_request_headers:
X-SET-REQUEST-HEADERS: X-VALUE
remove_request_headers:
- X-Pomerium-Claim-User
- X-Pomerium-Claim-Name

We’re telling Pomerium to add a header to the request called X-Set-Request-Headers with a value of X-Value. We’re also telling Pomerium to remove the User and Name claims that are included as unsigned claims headers. This ensures that these specific headers do not reach the upstream application.

If you run docker compose up and check HTTPBin again, you’ll notice both the claims headers have been removed, and the test X-Set-Request-Headers header is there, too.

Set response headers

Similarly, you can configure responses as well:

- from: https://httpbin.localhost.pomerium.io
to: http://httpbin:80
set_response_headers:
X-SET-RESPONSE-HEADERS: X-VALUE
set_request_headers:
X-SET-REQUEST-HEADERS: X-VALUE
remove_request_headers:
- X-Pomerium-Claim-User
- X-Pomerium-Claim-Name

If you go HTTPBin’s Response inspection row and test a request, you’ll notice the test X-Set-Response-Headers header is included in the response.

View response headers

Set the Host header

You can also control the Host: header’s behavior, which is useful if your upstream server expects a certain value for this header.

For example, the value of the Host header is currently httpbin.

However, if you add preserve_host_header and set it to true, you’ll notice the value changes from httpbin to httpbin.localhost.pomerium.io:

config.yaml
- from: https://httpbin.localhost.pomerium.io
to: https://httpbin:80
set_response_headers:
X-SET-RESPONSE-HEADERS: X-VALUE
set_request_headers:
X-SET-REQUEST-HEADERS: X-VALUE
remove_request_headers:
- X-Pomerium-Claim-User
- X-Pomerium-Claim-Name
preserve_host_header: true

That’s because this setting preserves the Host header from the proxied request instead of taking the value from the destination’s hostname.

Alternatively, you can also use host_rewrite_header to change the Host header’s value to that of any incoming request (in this case, it would still be httpbin.localhost.pomerium.io).

config.yaml
- from: https://httpbin.localhost.pomerium.io
to: https://httpbin:80
set_response_headers:
X-SET-RESPONSE-HEADERS: X-VALUE
set_request_headers:
X-SET-REQUEST-HEADERS: X-VALUE
remove_request_headers:
- X-Pomerium-Claim-User
- X-Pomerium-Claim-Name
# preserve_host_header: true
host_rewrite_header: true

Configure redirects

What if you wanted to redirect users?

Pomerium makes this simple to implement. Just swap out to: with the redirect: syntax:

config.yaml
- from: https://httpbin.localhost.pomerium.io
redirect: {'host_redirect': 'verify.localhost.pomerium.io'}

Now, when you access httpbin, Pomerium will redirect you to the Verify service.

Configure prefix and path settings

Configure Node server

To demonstrate these settings, we must add one more service to our configuration: A simple Node HTTP server.

First, you must have Node.js installed.

After you install Node.js:

  1. Create a new directory called app

  2. cd into app

  3. Initiate a Node application: npm init

  4. Create an index.js file: touch index.js

  5. Install Express: npm i express

Add the following code inside index.js:

index.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
console.log(req);
res.send('Hello World!');
});

app.get('/admin', (req, res) => {
res.send('This is an admin only page');
});

app.listen(5001, () => console.log('Server is up and running'));

This mini server builds two different endpoints:

  • /
  • /admin

Your app directory should now have a package.json file and a node_modules folder.

Next, test your server:

node index.js

Navigate to localhost:5001 to see if your server serves the Hello World! message.

The 'Hello World' page from our Node server

Similarly, if you go to localhost:5001/admin, you should see This is an admin only page.

The 'Admin only' page from our Node server

Dockerize Node server

Next, we will Dockerize our Node server. In the ./app directory, create the following files:

  • touch Dockerfile
  • touch .dockerignore

In Dockerfile, add the following instructions:

Dockerfile
# pull the Node.js Docker image
FROM node:alpine

# create the directory inside the container
WORKDIR /usr/src/app

# copy the package.json files from local machine to the workdir in container
COPY package*.json ./

# run npm install in our local machine
RUN npm install

# copy the generated modules and all other files to the container
COPY . .

# our app is running on port 5001 within the container, so need to expose it
EXPOSE 5001

# the command that starts our app
CMD ["node", "index.js"]

In .dockerignore, add:

.dockerignore
node_modules npm-debug.log

In Docker Compose, add your Node server:

docker-compose.yaml
nodeserver:
build:
context: ./app
ports:
- '5001:5001'

In your configuration file, add a route to the Node server:

config.yaml
routes:
- from: https://nodeserver.localhost.pomerium.io
to: http://nodeserver:5001
policy:
allow:
and:
- domain:
is: example.com

Now, cd back into your root project and run the following command to build your Node server so it’s accessible inside your container:

docker compose up --build

With your containers running, go to https://nodeserver.localhost.pomerium.io.

You should see Hello World!!.

If you go to https://nodeserver.localhost.pomerium.io/admin, you should see the /admin page.

Next, in your Pomerium configuration file, add the prefix: setting and give it the value of /admin:

- from: https://nodeserver.localhost.pomerium.io
to: http://nodeserver:5001
prefix: /admin

Restart Docker and navigate to https://nodeserver.localhost.pomerium.io. Because this URL doesn’t include the /admin prefix in its path, Pomerium won’t match the route, which results in a 404 error.

The prefix setting will only match the route if the request has the prefix in its path. So, if you try https://nodeserver.localhost.pomerium.io/admin, Pomerium should direct you to the /admin only page.

Next, let’s add prefix_rewrite. To use it, give prefix_rewrite the value of /. If the incoming request’s prefix matches the value of prefix (/admin), Pomerium will rewrite prefix to match the value of prefix_rewrite:

- from: https://nodeserver.localhost.pomerium.io
to: http://nodeserver:5001
prefix: /admin
prefix_rewrite: /

Now, if you navigate to https://nodeserver.localhost.pomerium.io/admin, Pomerium will redirect you to the / page.

If you don’t include the /admin prefix, the request will 404.

Clean up

This was just to show you Pomerium’s flexibility when it comes to advanced routes. We won’t be using the HTTPbin or Node server services going forward. To keep your configuration files clean and easy to manage, remove the following services and routes:

In your configuration file:

  • Remove the httpbin and nodeserver routes and their attached policies from - routes
  • Remove jwt_claims_headers

In your project’s root folder:

  • Delete app and its contents

In your Docker Compose file:

  • Remove the httpbin and nodeserver services

Summary

In this tutorial, you added several new services and built routes to them.

You also added per-route settings that handle redirects and modify requests, responses, header values, and URL paths and prefixes.

Now that you’ve seen what you can do with routes, it’s time to learn how to proxy TCP connections with Pomerium!

Configuration file state

After cleaning up your files, your configuration should look similar to this:

authenticate_service_url: https://authenticate.pomerium.app

signing_key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUVSNThaeDA2SHJXTW9PUTRaNjlMaDdMZUtFZW5TSmJZcHJvZ3V3TEl0blNvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFK1FtamZKQ2ovdzkrOUhrRDVlbTlIZFhRM3ViUEhIdWNOMTlNOXJxR05PeEpTRmR3VHgvaAphdVkvcVFSWWR0YVpnVEpEUWZSYVQ2Q1pPYndSYTl2TXNnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=

routes:
- from: https://verify.localhost.pomerium.io
to: http://verify:8000
pass_identity_headers: true
policy:
allow:
or:
- domain:
is: example.com
- claim/Name: <"Your Name">
deny:
and:
- email:
starts_with: admin
- from: https://grafana.localhost.pomerium.io
to: http://grafana:3000
pass_identity_headers: true
policy:
allow:
and:
- domain:
is: example.com

Docker Compose

services:
pomerium:
image: cr.pomerium.com/pomerium/pomerium:latest
volumes:
- ./config.yaml:/pomerium/config.yaml:ro
ports:
- 443:443
verify:
image: cr.pomerium.com/pomerium/verify:latest
expose:
- 8000
environment:
- JWKS_ENDPOINT=https://pomerium/.well-known/pomerium/jwks.json
grafana:
image: grafana/grafana:latest
ports:
- 3000:3000