skip to Main Content

I have a number of services that are authenticated using OAuth Bearer tokens. I can obtain the tokens using either an OAuth Client Credentials grant or a Resource Owner Credentials Grant.

However I have a number of existing systems that are only capable of making calls authenticated using Mutual TLS authenticated connections.

Rather than updating all the calling applications to be able to obtain OAuth bearer tokens I’d instead like to build a gateway proxy that:

  1. Receives TLS Authenticated connections
  2. Uses the subject of the certificate to identify the system actor
  3. Obtain a token on that system’s behalf using Client Credentials or Resource Owner grant
  4. Make the call to the underlying service and return the results to the client

Essentially I want to hide the fact that OAuth is in use from the old clients and allow them to work exclusively with Mutal TLS Authentication.

Are there existing reverse proxies or ways or modules for Ngnix, Apache, Envoy or similar HTTP reverse proxies that would achieve this without building an entire proxy?

I’ve found lots of modules that handle the case of setting up Apache and Ngnix to be a OAuth relaying party or resource server using various modules such as

But can’t find any examples of them acting as an OAuth or Open ID Connect client as a proxy for the Mutual TLS authenticated client.

Particularly I want to avoid writing the proxy part. Even if I have to script the actual OAuth interactions. The closest thing I’ve found is this blog post on implementing an OAuth RP in Envoy lua scripts.

I struggle to imagine this is a unique need so I’m wondering if there is any standard implementation of this pattern out there that I haven’t found.

2

Answers


  1. Chosen as BEST ANSWER

    It turns out that this is easy to accomplish with Envoy Proxy. Specifically by making use of the External Authorization HTTP Filters.

    Envoy proxy can be configured to do the SSL termination and require a client certificate by setting the Downstream TLS Context on the listener and setting require_client_certificate to true.

    It's possible to configure the HTTP Connection Manager Network Filter to set the x-forwarded-client-cert header on the request to the upstream service. Specifically setting forward-client-cert-details to SANITIZE_SET will cause envoy to set the header. What is included in the header can be configured by setting set-current-client-cert-details.

    But to actually have Envoy do the token exchange we need to configure an External Authorization filter. This allows envoy to call a service with details of the request (including the certificate) and that service can decide if the request is allowed or not. Crucially, when allowed it's able to add headers to the request made to the upstream services which allows it to add the bearer token needed for oauth.

    There are both Network Filter and HTTP Filter versions of the External Authorization filter. You must use the HTTP one if you want to add headers to the upstream request.

    The resulting envoy config looks like:

    static_resources:
      listeners:
      - name: listener_0
        address:
          socket_address:
            protocol: TCP
            address: 0.0.0.0
            port_value: 10000
        filter_chains:
        - filters:
          - name: envoy.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
              stat_prefix: ingress_http
              forward_client_cert_details: SANITIZE_SET # Include details of the client certificate in the x-forwarded-client-cert header when calling the upstream service
              set_current_client_cert_details:
                subject: True # Include the subject of the certificate in the x-forwarded-client-cert header rather than just the certificate hash.
              route_config:
                name: local_route
                virtual_hosts:
                - name: local_service
                  domains: ["*"]
                  routes:
                  - match:
                      prefix: "/"
                    route:
                      cluster: service_backend
              http_filters:
              - name: envoy.ext_authz # Call an authorization service to do the OAuth token exchange
                config:
                  grpc_service:
                    envoy_grpc:
                      cluster_name: auth_service
                    timeout: 5s # The timeout before envoy will give up waiting for an auth service response and deny access
              - name: envoy.router
          tls_context:
            require_client_certificate: True # Require downstream callers to provide a client certificate
            common_tls_context:
              validation_context:
                trusted_ca:
                  filename: /etc/envoy/certs/ca-chain.cert.pem # CA certificate that client certificate must be signed with to be accepted
              tls_certificates:
              - certificate_chain:
                  filename: /etc/envoy/certs/server-cert.pem
                private_key:
                  filename: /etc/envoy/certs/server-key.pem
                password:
                  inline_string: password
      clusters:
      - name: auth_service
        connect_timeout: 1s # The timeout before envoy will give up trying to make a TCP  connectio to an auth service
        type: LOGICAL_DNS
        dns_lookup_family: V4_ONLY
        lb_policy: ROUND_ROBIN
        http2_protocol_options: {} # GRPC services must be HTTP/2 so force HTTP/2
        load_assignment:
          cluster_name: auth_service
          endpoints:
          - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: localhost
                    port_value: 8080
    

    The trick then is to implement a GRPC service that implements the External Authorization Protcol to do the token exchange and either reject the request or provide the Authorization header to include the bearer token in the upstream request.


  2. If you still need this, I built an authorization server that works using a local auth DB. It’s very easy to convert it to call an OIDC and receive a token to be injected.
    https://github.com./fams/tlsjwt-gw

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search