Use Envoy for authentication

With the popularity of the Microservices architecture, handling cross-cutting concerns became a widespread problem. When you start decomposing monolith to microservices and finally end up with dozen of microservices,  handling of operational problems such as networking , observability and communication between services will start to become a bottleneck for the growth. As the Kubernetes became the defacto stranded for solving the hosting/deployment hurdles for Microservices, Service Mesh has taken up the role of solving those operational problems.

If you closely look at most of the well established Service Mesh products, Envoy is at heart of most. Though most of the organizations started embracing Service Meshes to run their workloads they don't use much of the powerful underlying capabilities of them. I will discuss in series of  articles on how we can utilize Envoy on securing applications specifically on Authentication and Authorization. Although the given samples are in dotnet this can be applied to any workload as Envoy runs alongside any application language or framework.

In this first example I will show how we can use Envoy for Authentication. To do that we can use Envoy JWT Authentication HTTP filter.  

with dotnet  ...

If you are using dotnetcore, you may have already implemented authentication in your dotnetcore application code. Probably your code use the JwtBearer middleware which responsible for decrypting the token, extract the claim and verify the signature. It validates the token by checking for these data:

  • Audience: The token is targeted for the web API.
  • Sub: It was issued for an app that's allowed to call the web API.
  • Issuer: It was issued by a trusted security token service (STS).
  • Expiry: Its lifetime is in range.
  • Signature: It wasn't tampered with.

To do the above most likely you have code written such as below based on the dotnetcore version you use. Also has the required settings in your config file.

app.UseAuthentication();

// Add services to the container in dotnet 6
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
    
  OR dotnet core 3.1

services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
          .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));

with Envoy ...

Key idea here is to offload these aspects such as Authentication from the application code and keep the application code purely for the business logic. Then implement those with Envoy which runs as a sidecar. In a cloud native world, a sidecar is a well known pattern where functionalities of an application are segregated into a separate process to provide isolation and encapsulation. In this scenario, Envoy sidecar will let the incoming connection pass through the application only after a successful authentication. This pattern greatly helps a polyglot application.

Let's examine the code. (complete sample is here in github)

Firstly check the program.cs file. Other than the additionally added  /health endpoint, its same as dotnet 6 Minimal Api visual studio template. (note that it does not have any code for Authentication or Authorization).

If you look at the docker-compose.yml file, you will see there is a image for Envoy. And in the docker.compose.override.yml file you will see that there is config added for envoy as well as the ports like below.

version: '3.4'

services:
  envoy:
   volumes:
     - ./Envoy/config/envoy.yaml:/etc/envoy/envoy.yaml
   ports:
   - "5200:8000"
   - "15200:8001"

  dotnet6-api-sample:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:8080
    ports:
      - "8080:8080"

Finally, let's take a closer look at key parts in envoy.yaml file.

In the listeners section, we have port 8000 as below. Also note that I have forwarded port 8000 to 5200 in above docker.compose.override file.

  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 8000

Next take a look at the envoy.filters.http.jwt_authn Http filter. This is the key component which holds the configuration for the Authentication. You need Azure AD or Azue AD B2C app registration to try this out. You can follow the steps of my post on Azure AD B2C App registration. Though the post is about Azure AD B2C, steps are same for Azure AD app registration as well.

          http_filters:
          - name: envoy.filters.http.jwt_authn
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
              providers:
                provider_azb2c:
                  issuer: https://login.microsoftonline.com/{Tenant GUID}/v2.0
                  audiences: <client ID>
                  remote_jwks:
                    http_uri:
                      uri: "https://login.microsoftonline.com/{Tenant GUID}/discovery/v2.0/keys"
                      cluster: azb2c
                      timeout: 10s
                    cache_duration: 
                      seconds: 600
              rules:
              - match:
                  prefix: /health
              - match:
                  prefix: /weatherforecast
                requires:
                  provider_and_audiences:
                    provider_name: provider_azb2c
                    audiences:
                      <client ID>
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router         

Once created the app registration, replace  {Tenant GUID} and <client ID> with your values. Also note the cluster : azb2c which is remote_jwks cluster  configured to fetch JWKS.

In the requirement rules section you will notice that /health endpoint configured not to require authentication, whereas /weatherforecast endpoint requires provider_and_audiences.

Now lets test this...

1) Clone the git repo.

2) Create the Azure AD app registration as described here.

3) update the envoy.yaml with your app registration values({Tenant GUID} and <client ID>)

4) Assuming you are in same directory as docker-compose.yml file, run docker compose up. At this stage you will be able to see the logs like below.

src-envoy-1               | [2022-09-06 19:50:56.137][1][info][config] [source/server/configuration_impl.cc:97] loading 2 cluster(s)
src-envoy-1               | [2022-09-06 19:50:56.146][1][info][config] [source/server/configuration_impl.cc:101] loading 1 listener(s)
src-envoy-1               | [2022-09-06 19:50:56.151][1][info][config] [source/server/configuration_impl.cc:113] loading stats configuration
src-envoy-1               | [2022-09-06 19:50:56.154][1][info][main] [source/server/server.cc:907] starting main dispatch loop
src-envoy-1               | [2022-09-06 19:50:56.341][1][info][runtime] [source/common/runtime/runtime_impl.cc:463] RTDS has finished initialization
src-envoy-1               | [2022-09-06 19:50:56.341][1][info][upstream] [source/common/upstream/cluster_manager_impl.cc:225] cm init: all clusters initialized
src-envoy-1               | [2022-09-06 19:50:56.341][1][info][main] [source/server/server.cc:888] all clusters initialized. initializing init manager
src-envoy-1               | [2022-09-06 19:50:56.342][1][info][config] [source/server/listener_manager_impl.cc:841] all dependencies initialized. starting workers
src-dotnet6-api-sample-1  | info: Microsoft.Hosting.Lifetime[14]
src-dotnet6-api-sample-1  |       Now listening on: http://[::]:8080
src-dotnet6-api-sample-1  | info: Microsoft.Hosting.Lifetime[0]
src-dotnet6-api-sample-1  |       Application started. Press Ctrl+C to shut down.
src-dotnet6-api-sample-1  | info: Microsoft.Hosting.Lifetime[0]
src-dotnet6-api-sample-1  |       Hosting environment: Development
src-dotnet6-api-sample-1  | info: Microsoft.Hosting.Lifetime[0]
src-dotnet6-api-sample-1  |       Content root path: /app/

5) Now if you curl the below two endpoints you will see the result like below. Notice the /weatherforecast endpoint fails with "Jwt is missing".

$ curl http://localhost:5200/health
alive

$ curl http://localhost:5200/weatherforecast
Jwt is missing

6) Finally, go back to the this Azure AD app registration post and get an access_token as described. Now curl the /weatherforecast with the token as below.

TOKEN=<replace this with your access_token>
$ curl  -H "Authorization: Bearer ${TOKEN}"  http://localhost:5200/weatherforecast
[{"date":"2022-09-07T20:03:51.6100315+00:00","temperatureC":19,"summary":"Cool","temperatureF":66}]

That's All! Up next :- How to use Envoy for Authorization with OPA.