JWT Verification
This example demonstrates how to verify the Pomerium JWT assertion header using Envoy. This is useful for legacy or 3rd party applications which can't be modified to perform verification themselves.
This guide is a practical demonstration of some of the topics discussed in Mutual Authentication: A Component of Zero Trust.
Requirements
This guide assumes you already have a working IdP connection to provide user data. See our Identity Provider docs for more information.
Overview
Three services are configured in a docker-compose.yaml file:
- pomeriumrunning an all-in-one deployment of Pomerium on- *.localhost.pomerium.io
- envoy-jwt-checkerrunning envoy with a JWT Authn filter
- httpbinas our example legacy application without JWT verification.
In our Docker Compose configuration we'll define two networks. pomerium and envoy-jwt-checker will be on the frontend network, simulating your local area network (LAN). envoy-jwt-checker will also be on the backend network, along with httpbin. This means that envoy-jwt-checker is the only other service that can communicate with httpbin.
For a detailed explanation of this security model, see Mutual Authentication With a Sidecar
Once running, the user visits verify.localhost.pomerium.io, is authenticated through authenticate.localhost.pomerium.io, and then the HTTP request is sent to envoy which proxies it to the httpbin app.
Before allowing the request Envoy will verify the signed JWT assertion header using the public key defined by httpbin.localhost.pomerium.io/.well-known/pomerium/jwks.json.
Setup
The configuration presented here assumes a working route to the domain space *.localhost.pomerium.io. You can make entries in your hosts file for the domains used, or change this value to match your local environment.
Mac and Linux users can use DNSMasq to map the *.localhost.pomerium.io domain (including all subdomains) to a specified test address:
- 
Create a docker-compose.yamlfile containing:docker-compose.yamlversion: '3.9'
 networks:
 frontend:
 driver: 'bridge'
 backend:
 driver: 'bridge'
 services:
 pomerium:
 image: cr.pomerium.com/pomerium/pomerium:latest
 ports:
 - '443:443'
 volumes:
 - type: bind
 source: ./cfg/pomerium.yaml
 target: /pomerium/config.yaml
 - type: bind
 source: ./certs/_wildcard.localhost.pomerium.io.pem
 target: /pomerium/_wildcard.localhost.pomerium.io.pem
 - type: bind
 source: ./certs/_wildcard.localhost.pomerium.io-key.pem
 target: /pomerium/_wildcard.localhost.pomerium.io-key.pem
 networks:
 - frontend
 envoy-jwt-checker:
 image: envoyproxy/envoy:v1.17.1
 ports:
 - '10000:10000'
 volumes:
 - type: bind
 source: ./cfg/envoy.yaml
 target: /etc/envoy/envoy.yaml
 networks:
 frontend:
 aliases:
 - 'httpbin-sidecar'
 backend:
 httpbin:
 image: kennethreitz/httpbin
 ports:
 - '80:80'
 networks:
 - backend
- 
Using mkcert, generate a certificate for*.localhost.pomerium.ioin acertsdirectory:mkdir certs
 cd certs
 mkcert '*.localhost.pomerium.io'
- 
Create a cfgdirectory containing the followingenvoy.yamlfile. Envoy configuration can be quite verbose, but the crucial bit is the HTTP filter (highlighted below):envoy.yamladmin:
 access_log_path: /dev/null
 address:
 socket_address: {address: 127.0.0.1, port_value: 9901}
 static_resources:
 listeners:
 - name: ingress-http
 address:
 socket_address: {address: 0.0.0.0, port_value: 10000}
 filter_chains:
 - filters:
 - name: envoy.filters.network.http_connection_manager
 typed_config:
 '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
 stat_prefix: ingress_http
 codec_type: AUTO
 route_config:
 name: verify
 virtual_hosts:
 - name: httpbin
 domains: ['httpbin-sidecar']
 routes:
 - match:
 prefix: '/'
 route:
 cluster: egress-httpbin
 auto_host_rewrite: true
 http_filters:
 - name: envoy.filters.http.jwt_authn
 typed_config:
 '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
 providers:
 pomerium:
 issuer: httpbin.localhost.pomerium.io
 audiences:
 - httpbin.localhost.pomerium.io
 from_headers:
 - name: X-Pomerium-Jwt-Assertion
 remote_jwks:
 http_uri:
 uri: https://httpbin.localhost.pomerium.io/.well-known/pomerium/jwks.json
 cluster: egress-authenticate
 timeout: 1s
 rules:
 - match:
 prefix: /
 requires:
 provider_name: pomerium
 - name: envoy.filters.http.router
 clusters:
 - name: egress-httpbin
 connect_timeout: 0.25s
 type: STRICT_DNS
 lb_policy: ROUND_ROBIN
 load_assignment:
 cluster_name: httpbin
 endpoints:
 - lb_endpoints:
 - endpoint:
 address:
 socket_address:
 address: httpbin
 port_value: 80
 - name: egress-authenticate
 connect_timeout: '0.25s'
 type: STRICT_DNS
 lb_policy: ROUND_ROBIN
 load_assignment:
 cluster_name: authenticate
 endpoints:
 - lb_endpoints:
 - endpoint:
 address:
 socket_address:
 address: pomerium
 port_value: 443
 transport_socket:
 name: tls
 typed_config:
 '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
 sni: authenticate.localhost.pomerium.ioThis configuration pulls the JWT out of the X-Pomerium-Jwt-Assertionheader, verifies theissandaudclaims and checks the signature via the public key defined at thejwks.jsonendpoint. Documentation for additional configuration options is available here: Envoy JWT Authentication.
Audience and issuer claims
- 
Create a pomerium.yamlfile in thecfgdirectory containing:pomerium.yamlauthenticate_service_url: https://authenticate.localhost.pomerium.io
 certificate_file: '/pomerium/_wildcard.localhost.pomerium.io.pem'
 certificate_key_file: '/pomerium/_wildcard.localhost.pomerium.io-key.pem'
 idp_provider: google
 idp_client_id: REPLACE_ME
 idp_client_secret: REPLACE_ME
 cookie_secret: REPLACE_ME
 shared_secret: REPLACE_ME
 signing_key: REPLACE_ME
 routes:
 - from: https://httpbin.localhost.pomerium.io
 to: http://httpbin-sidecar:10000
 pass_identity_headers: true
 policy:
 - allow:
 or:
 - domain:
 is: example.com
Replace the identity provider credentials, secrets, and signing key. Adjust the policy to match your configuration.
Run
You should now be able to run the example with:
- 
Turn on the example configuration in Docker: docker-compose up
- 
Visit httpbin.localhost.pomerium.io. Login and you will be redirected to the httpbin page. 
- 
In this network configuration you cannot access httpbindirectly. However, visiting Envoy directly via localhost.pomerium.io:10000/ will return aJwt is missingerror, confirming that you must authenticate with Pomerium to access Envoy, and any services accessible through it.