I recently came across the need for a reusable microservice in the form of an OCI / Docker image to meet different authentication and authorization requirements depending on the end project and environment. While the current limited set of possible security configurations is handled by the microservice itself, I analyzed the possibility of delegating this aspect to a specialized sidecar container living in the same Kubernetes pod.

After a bit of prompt engineering and wanting to consider something else than NGINX I decided to first try Envoy Proxy. Long story short, here’s a Docker Compose setup:

version: '3'
services:
  envoy:
    image: envoyproxy/envoy:dev-c59c8658912ecf0d451e27a2d2f3c1c29feb5b1d
    ports:
      - "10000:10000"
      - "9901:9901"
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml

And the associated envoy.yaml configuration that covers:

  • JWT validation
  • RBAC authorization for a specific role in the roles claim
  • A sample Lua script
static_resources:
  listeners:
    - name: listener_0
      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
                route_config:
                  virtual_hosts:
                    - name: backend
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: backend_service
                http_filters:
                  # Ensure a valid JWT is present on the request
                  - name: envoy.filters.http.jwt_authn
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                      providers:
                        oidc_provider:
                          issuer: "<issuer URL>"
                          remote_jwks:
                            http_uri:
                              uri: "<JWKS URL>"
                              cluster: oidc_provider
                              timeout: 5s
                          forward: true
                          from_headers:
                            - name: Authorization
                              value_prefix: "Bearer "
                          payload_in_metadata: jwt_payload
                      rules:
                        - match:
                            prefix: "/"
                          requires:
                            provider_name: oidc_provider
                  # Ensure that the JWT contains a FLOWX_ROLE roles
                  - name: envoy.filters.http.rbac
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
                      rules:
                        action: ALLOW
                        policies:
                          allow_roles:
                            permissions:
                              - any: true
                            principals:
                              - authenticated:
                                  principal_name:
                                    exact: "*"
                              - metadata:
                                  filter: envoy.filters.http.jwt_authn
                                  path:
                                    - key: jwt_payload
                                    - key: roles
                                  value:
                                    list_match:
                                      one_of:
                                        string_match:
                                          exact: "FLOWX_ROLE"
                  # Sample LUA script
                  - name: envoy.filters.http.lua
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                      inline_code: |
                        function envoy_on_request(handle)
                          handle:logInfo("Incoming request logged by Lua!")
                        end
                  # Final routing
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
    - name: backend_service
      type: STRICT_DNS
      connect_timeout: 0.25s
      load_assignment:
        cluster_name: backend_service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: host.docker.internal
                      port_value: 8080
    - name: oidc_provider
      type: STRICT_DNS
      connect_timeout: 5s
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
      load_assignment:
        cluster_name: oidc_provider
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: <Authorization server URL>
                      port_value: 443

admin:
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

The backend service referenced above, to which Envoy relays requests after the security filters, is the reusable microservice that I mentioned earlier and running on port 8080 on my Docker Desktop host.

One thing that I found Envoy missing out of the box is the ability to check the roles mapping agains the user info or token introspection endpoints of the OIDC provider. The workaround for this would be a custom LUA script to be PoC-ed at a later time.