Created
July 25, 2026 01:04
-
-
Save mohashari/a872f63d3e3963d4fbc975cbf440bb02 to your computer and use it in GitHub Desktop.
Implementing Zero-Trust Service-to-Service Authorization Using Envoy Proxies and Open Policy Agent (OPA) — code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Envoy HTTP filter configuration for OPA external authorization | |
| http_filters: | |
| - name: envoy.filters.http.ext_authz | |
| typed_config: | |
| "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz | |
| grpc_service: | |
| envoy_grpc: | |
| cluster_name: ext-authz-opa | |
| timeout: 0.15s | |
| transport_api_version: V3 | |
| with_request_body: | |
| max_request_bytes: 1024 | |
| pack_as_bytes: true | |
| failure_mode_allow: false | |
| status_on_error: | |
| code: ServiceUnavailable | |
| include_peer_certificate: true | |
| # The corresponding Envoy cluster definition pointing to local OPA | |
| clusters: | |
| - name: ext-authz-opa | |
| type: STATIC | |
| connect_timeout: 0.25s | |
| lb_policy: ROUND_ROBIN | |
| http2_protocol_options: {} | |
| load_assignment: | |
| cluster_name: ext-authz-opa | |
| endpoints: | |
| - lb_endpoints: | |
| - endpoint: | |
| address: | |
| socket_address: | |
| address: 127.0.0.1 | |
| port_value: 9001 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # OPA Daemon configuration for Envoy gRPC ext_authz plugin | |
| services: | |
| control-plane-service: | |
| url: https://control-plane.internal.net | |
| credentials: | |
| bearer: | |
| token: "env-var-expanded-token" | |
| keys: | |
| my-key: | |
| key: | | |
| -----BEGIN PUBLIC KEY----- | |
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0G4P9Kz1r8bXpLzWwQID | |
| -----END PUBLIC KEY----- | |
| bundles: | |
| authz: | |
| service: control-plane-service | |
| resource: /v1/bundles/service-authz | |
| signing: | |
| keyid: my-key | |
| polling: | |
| min_delay_seconds: 10 | |
| max_delay_seconds: 30 | |
| plugins: | |
| envoy_ext_authz_grpc: | |
| addr: 127.0.0.1:9001 | |
| path: envoy/authz/allow | |
| query: data.envoy.authz.allow |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package envoy.authz | |
| import rego.v1 | |
| # Default deny all traffic | |
| default allow = false | |
| # Main entry point specified in OPA config | |
| allow if { | |
| client_identity_allowed | |
| jwt_claims_validated | |
| endpoint_permissions_allowed | |
| } | |
| # 1. Parse client certificate SPIFFE identity from Envoy source principal | |
| spiffe_id := input.attributes.source.principal | |
| client_identity_allowed if { | |
| # Ensure client identity presents a valid SPIFFE ID within our trust domain | |
| startswith(spiffe_id, "spiffe://prod.internal/ns/core-services/sa/") | |
| } | |
| # 2. Extract and validate JWT bearer token passed in authorization header | |
| jwt_token := token if { | |
| auth_header := input.attributes.request.http.headers.authorization | |
| startswith(auth_header, "Bearer ") | |
| token := substring(auth_header, 7, -1) | |
| } | |
| # Helper to decode the JWT payload | |
| jwt_payload := payload if { | |
| [_, payload, _] := io.jwt.decode(jwt_token) | |
| } | |
| jwt_claims_validated if { | |
| # Validate expiration time (unix timestamp comparison) | |
| jwt_payload.exp > time.now_ns() / 1000000000 | |
| # Verify trusted issuer | |
| jwt_payload.iss == "https://auth.internal.net" | |
| } | |
| # 3. Path and HTTP Method check matching client roles to actions | |
| endpoint_permissions_allowed if { | |
| request_method := input.attributes.request.http.method | |
| request_path := split(trim(input.attributes.request.http.path, "/"), "/") | |
| match_path_and_roles(request_method, request_path, jwt_payload.roles) | |
| } | |
| # Define rule permissions based on roles and paths | |
| match_path_and_roles("GET", ["payments", _], roles) if { | |
| "payment-viewer" in roles | |
| } | |
| match_path_and_roles("POST", ["payments"], roles) if { | |
| "payment-creator" in roles | |
| } | |
| # 4. Propagate authorized metadata back to Envoy to inject into upstream request | |
| headers := { | |
| "x-auth-user-id": jwt_payload.sub, | |
| "x-auth-client-spiffe": spiffe_id, | |
| "x-auth-user-roles": concat(",", jwt_payload.roles) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| apiVersion: apps/v1 | |
| kind: Deployment | |
| metadata: | |
| name: payment-service | |
| namespace: core-services | |
| spec: | |
| replicas: 3 | |
| selector: | |
| matchLabels: | |
| app: payment-service | |
| template: | |
| metadata: | |
| labels: | |
| app: payment-service | |
| spec: | |
| containers: | |
| # 1. Main Application Container | |
| - name: app | |
| image: internal-registry.net/core/payment-service:v2.4.1 | |
| ports: | |
| - containerPort: 8080 | |
| env: | |
| - name: PORT | |
| value: "8080" | |
| resources: | |
| limits: | |
| cpu: "1" | |
| memory: "512Mi" | |
| requests: | |
| cpu: "100m" | |
| memory: "128Mi" | |
| # 2. Envoy Proxy Container (Sidecar) | |
| - name: envoy-proxy | |
| image: envoyproxy/envoy:v1.28.0 | |
| args: ["-c", "/etc/envoy/envoy.yaml", "--log-level", "info"] | |
| ports: | |
| - name: ingress-http | |
| containerPort: 443 | |
| volumeMounts: | |
| - name: envoy-config | |
| mountPath: /etc/envoy | |
| - name: spiffe-workload-api | |
| mountPath: /run/spire/sockets | |
| readOnly: true | |
| resources: | |
| limits: | |
| cpu: "500m" | |
| memory: "256Mi" | |
| requests: | |
| cpu: "50m" | |
| memory: "64Mi" | |
| # 3. Open Policy Agent (OPA) Container (Sidecar) | |
| - name: opa | |
| image: openpolicyagent/opa:0.61.0-envoy | |
| args: | |
| - "run" | |
| - "--server" | |
| - "--config-file=/etc/opa/opa-config.yaml" | |
| - "/etc/opa/policy.rego" | |
| volumeMounts: | |
| - name: opa-config | |
| mountPath: /etc/opa | |
| ports: | |
| - name: grpc-authz | |
| containerPort: 9001 | |
| resources: | |
| limits: | |
| cpu: "500m" | |
| memory: "512Mi" | |
| requests: | |
| cpu: "100m" | |
| memory: "128Mi" | |
| volumes: | |
| - name: envoy-config | |
| configMap: | |
| name: payment-envoy-config | |
| - name: opa-config | |
| configMap: | |
| name: payment-opa-config | |
| - name: spiffe-workload-api | |
| csi: | |
| driver: "spiffe.csi.spiffe.io" | |
| readOnly: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "testing" | |
| "github.com/open-policy-agent/opa/rego" | |
| ) | |
| func TestRegoPolicy_Allow(t *testing.T) { | |
| ctx := context.Background() | |
| // Initialize the Rego engine with policy files and rules query | |
| r := rego.New( | |
| rego.Query("data.envoy.authz.allow"), | |
| rego.Load([]string{"policy.rego"}, nil), | |
| ) | |
| preparedQuery, err := r.PrepareForEval(ctx) | |
| if err != nil { | |
| t.Fatalf("Failed to prepare query: %v", err) | |
| } | |
| // Construct mock Envoy ext_authz payload | |
| mockInput := map[string]interface{}{ | |
| "attributes": map[string]interface{}{ | |
| "source": map[string]interface{}{ | |
| "principal": "spiffe://prod.internal/ns/core-services/sa/billing-service", | |
| }, | |
| "request": map[string]interface{}{ | |
| "http": map[string]interface{}{ | |
| "method": "POST", | |
| "path": "/payments", | |
| "headers": map[string]string{ | |
| "authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOTg3NiIsImV4cCI6NDA5MzUyOTYwMCwiaXNzIjoiaHR0cHM6Ly9hdXRoLmludGVybmFsLm5ldCIsInJvbGVzIjpbInBheW1lbnQtY3JlYXRvciJdfQ.sig", | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| results, err := preparedQuery.Eval(ctx, rego.EvalInput(mockInput)) | |
| if err != nil { | |
| t.Fatalf("Failed to evaluate policy: %v", err) | |
| } | |
| if len(results) == 0 { | |
| t.Fatal("Expected evaluation results, got none") | |
| } | |
| allowed, ok := results[0].Expressions[0].Value.(bool) | |
| if !ok || !allowed { | |
| t.Errorf("Expected policy to allow request, got allowed=%v", results[0].Expressions[0].Value) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment