emissary-ingress / emissary

open source Kubernetes-native API gateway for microservices built on the Envoy Proxy
https://www.getambassador.io
Apache License 2.0
4.37k stars 687 forks source link

Advanced mTLS support (extract Client TLS cert) #3607

Closed dzmitry-kankalovich closed 3 years ago

dzmitry-kankalovich commented 3 years ago

Please describe your use case / problem. We're using Ambassador Edge Stack (AES) to front the API for the IoT devices via Emissary-Ingress acting as API gateway. In the case of IoT, a typical approach for the authentication with remote API is to use Mutual TLS (mTLS).

So, the IoT device presents an X509 certificate, which does the following:

  1. Cryptographically proves the identity of the caller - IoT device
  2. Carries the metadata about the caller - Device ID in CN attribute and public key

Now, in the context of API GW, we have no problems establishing mTLS to achieve Num.1 - we provision a standalone Host with mTLS enabled, supply the CA chain & keys and we're good. But the troubles arise with Num.2 - attempt to read and pass forward the peer (client) certificate metadata.

As of now, there seems to be no way to extract certificate meta and pass it forward to the upstream service. There were two ways to do that which we explored:

  1. Via request transformation based on Lua Script. Envoy since v1.16 provides the capability in Lua script filter to extract peer cert meta and pass it to the upstream via headers. Unfortunately, it seems like Emissary is based on Envoy v1.15 which does not have such a feature.
  2. Via Filter based on Go Plugin. We're not sure about architecture here but looks like Emissary forwards the request to a standalone sidecar process running those filters - of course, the original peer certificate gets lost in such case, because it's essentially Emissary <-> Sidecar communication, not Device <-> Emissary. So there is no way to access the peer certificate in the FilterPlugin.

Describe the solution you'd like Given the current architecture of Emissary, it's probably simpler to upgrade to Envoy v1.16 than to re-do the architecture of the FilterPlugin. We have only one concern here - these Lua scripts doing request transformation are global, affecting every request, regardless of the host and path. Ideally, that should be host-path scoped, just like Filters.

Another way to do it is to automatically extract this data and pass it further upstream, like Kong does it. Still, it will cover only basic cases, so programmatic access to the underlying cert is more preferable.

Describe alternatives you've considered As of now, looks like we'll be forced to duplicate some cert meta in the API request body, headers, or params. Not as elegant as supplying them via peer certificate, and potentially error-prone.

Additional context N/A

dzmitry-kankalovich commented 3 years ago

TL;DR: is it possible to extract peer (client) TLS certificate either via Lua plugin or Go plugin?

dzmitry-kankalovich commented 3 years ago

It seems like we've found a workaround on the basis of this changeset and likely proceed with that.

dzmitry-kankalovich commented 3 years ago

So, in a nutshell, this is a solution to what we asked originally:

Besides the ability to forward the XFCC (x-forwarded-client-cert) header to the upstream service, we've just confirmed that we can access the raw Client TLS Certificate, its certificate chain, and its metadata by reading the same XFCC header value in the Go plugin (Filter). Apparently, this header is conveniently added before the request gets processed by Filters.

So, I guess even though the ExtAuth protocol by itself won't allow the forward of the underlying Client TLS Cert to the Filter, the workaround with the XFCC header still gets us what we need.

PS: we haven't checked if the same is possible with Lua plugins.

dzmitry-kankalovich commented 3 years ago

At this point, there is a viable workaround, at least for the Go Plugin (Filter) case, so this feature request is no more "must", but rather a "nice" to have a thing in my mind.

We are though relying on the fact that XFCC is attached to the original request before Filters execution, and IDK if this is guaranteed to stay the same in future versions of Ambassador - this is one thing. Another thing is that documentation and overall plugin development framework does not feel on par with the competition like Kong, and this by itself slows down the progress, but it goes beyond the scope of what we're discussing here.

I feel like this feature request might be closed. I've added some more keywords here so that other people can find this issue and a workaround solution.

dzmitry-kankalovich commented 3 years ago

UPDATE: As of AEM 1.14.0 it is now possible to extract client cert via Lua scripts:

lua_scripts: |
  function envoy_on_request(handle)
    local si = handle:streamInfo():downstreamSslConnection()
    if si == nil then
      handle:headers():add("X-Lua-XFCC", "No SSL Context")
    else
      if si:peerCertificatePresented() then
        handle:headers():add("X-Lua-XFCC", si:subjectPeerCertificate())
      else
        handle:headers():add("X-Lua-XFCC", "None")
      end
    end
  end