1--- 2title: Envoy 3kind: tutorial 4weight: 8 5--- 6 7[Envoy](https://www.envoyproxy.io/docs/envoy/v1.10.0/intro/what_is_envoy) is a L7 proxy and communication bus designed for large modern service oriented architectures. Envoy (v1.7.0+) supports an [External Authorization filter](https://www.envoyproxy.io/docs/envoy/v1.10.0/intro/arch_overview/ext_authz_filter.html) which calls an authorization service to check if the incoming request is authorized or not. 8 9This feature makes it possible to delegate authorization decisions to an external service and also makes the request context available to the service which can then be used to make an informed decision about the fate of the incoming request received by Envoy. 10 11## Goals 12 13The tutorial shows how Envoy’s External authorization filter can be used with OPA as an authorization service to enforce security policies over API requests received by Envoy. The tutorial also covers examples of authoring custom policies over the HTTP request body. 14 15## Prerequisites 16 17This tutorial requires Kubernetes 1.14 or later. To run the tutorial locally, we recommend using [minikube](https://kubernetes.io/docs/getting-started-guides/minikube) in version `v1.0+` with Kubernetes 1.14 (which is the default). 18 19## Steps 20 21### 1. Start Minikube 22 23```bash 24minikube start 25``` 26 27### 2. Create ConfigMap containing configuration for Envoy 28 29The Envoy configuration below defines an external authorization filter `envoy.ext_authz` for a gRPC authorization server. 30 31Save the configuration as **envoy.yaml**: 32 33```yaml 34static_resources: 35 listeners: 36 - address: 37 socket_address: 38 address: 0.0.0.0 39 port_value: 8000 40 filter_chains: 41 - filters: 42 - name: envoy.http_connection_manager 43 typed_config: 44 "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager 45 codec_type: auto 46 stat_prefix: ingress_http 47 route_config: 48 name: local_route 49 virtual_hosts: 50 - name: backend 51 domains: 52 - "*" 53 routes: 54 - match: 55 prefix: "/" 56 route: 57 cluster: service 58 http_filters: 59 - name: envoy.ext_authz 60 typed_config: 61 "@type": type.googleapis.com/envoy.config.filter.http.ext_authz.v2.ExtAuthz 62 with_request_body: 63 max_request_bytes: 8192 64 allow_partial_message: true 65 failure_mode_allow: false 66 grpc_service: 67 google_grpc: 68 target_uri: 127.0.0.1:9191 69 stat_prefix: ext_authz 70 timeout: 0.5s 71 - name: envoy.router 72 typed_config: {} 73 clusters: 74 - name: service 75 connect_timeout: 0.25s 76 type: strict_dns 77 lb_policy: round_robin 78 load_assignment: 79 cluster_name: service 80 endpoints: 81 - lb_endpoints: 82 - endpoint: 83 address: 84 socket_address: 85 address: 127.0.0.1 86 port_value: 8080 87admin: 88 access_log_path: "/dev/null" 89 address: 90 socket_address: 91 address: 0.0.0.0 92 port_value: 8001 93``` 94 95Create the ConfigMap: 96 97```bash 98kubectl create configmap proxy-config --from-file envoy.yaml 99``` 100 101### 3. Define a OPA policy 102 103The following OPA policy restricts access to the `/people` endpoint exposed by our sample app: 104 105* Alice is granted a **guest** role and can perform a `GET` request to `/people`. 106* Bob is granted an **admin** role and can perform a `GET` and `POST` request to `/people`. 107 108The policy also restricts an `admin` user, in this case `bob` from creating an employee with the same `firstname` as himself. 109 110**policy.rego** 111 112```live:example:module:openable 113package envoy.authz 114 115import input.attributes.request.http as http_request 116 117default allow = false 118 119token = {"valid": valid, "payload": payload} { 120 [_, encoded] := split(http_request.headers.authorization, " ") 121 [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) 122} 123 124allow { 125 is_token_valid 126 action_allowed 127} 128 129is_token_valid { 130 token.valid 131 token.payload.nbf <= time.now_ns() 132 time.now_ns() < token.payload.exp 133} 134 135action_allowed { 136 http_request.method == "GET" 137 token.payload.role == "guest" 138 glob.match("/people*", [], http_request.path) 139} 140 141action_allowed { 142 http_request.method == "GET" 143 token.payload.role == "admin" 144 glob.match("/people*", [], http_request.path) 145} 146 147action_allowed { 148 http_request.method == "POST" 149 token.payload.role == "admin" 150 glob.match("/people", [], http_request.path) 151 lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) 152} 153``` 154 155Store the policy in Kubernetes as a Secret. 156 157```bash 158kubectl create secret generic opa-policy --from-file policy.rego 159``` 160 161In the next step, OPA is configured to query for the `data.envoy.authz.allow` decision. If the response is `true` the operation is 162allowed, otherwise the operation is denied. Sample input received by OPA is shown below: 163 164```live:example:query:hidden 165data.envoy.authz.allow 166``` 167 168```live:example:input 169{ 170 "attributes": { 171 "request": { 172 "http": { 173 "method": "GET", 174 "path": "/people", 175 "headers": { 176 "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU" 177 } 178 } 179 } 180 } 181} 182``` 183 184With the input value above, the answer is: 185 186```live:example:output 187``` 188 189An example of the complete input received by OPA can be seen [here](https://github.com/open-policy-agent/opa-istio-plugin#example-input). 190 191> In typical deployments the policy would either be built into the OPA container 192> image or it would fetched dynamically via the [Bundle 193> API](https://www.openpolicyagent.org/docs/latest/bundles/). ConfigMaps are 194> used in this tutorial for test purposes. 195 196### 4. Create App Deployment with OPA and Envoy sidecars 197 198Our deployment contains a sample Go app which provides information about employees in a company. It exposes a `/people` endpoint to `get` and `create` employees. More information can on the app be found [here](https://github.com/ashutosh-narkar/go-test-server). 199 200OPA is started with a configuration that sets the listening address of Envoy External Authorization gRPC server and specifies the name of the policy decision to query. More information on the configuration options can be found [here](https://github.com/open-policy-agent/opa-istio-plugin#configuration). 201 202Save the deployment as **deployment.yaml**: 203 204```yaml 205kind: Deployment 206apiVersion: apps/v1 207metadata: 208 name: example-app 209 labels: 210 app: example-app 211spec: 212 replicas: 1 213 selector: 214 matchLabels: 215 app: example-app 216 template: 217 metadata: 218 labels: 219 app: example-app 220 spec: 221 initContainers: 222 - name: proxy-init 223 image: openpolicyagent/proxy_init:v2 224 args: ["-p", "8000", "-u", "1111"] 225 securityContext: 226 capabilities: 227 add: 228 - NET_ADMIN 229 runAsNonRoot: false 230 runAsUser: 0 231 containers: 232 - name: app 233 image: openpolicyagent/demo-test-server:v1 234 ports: 235 - containerPort: 8080 236 - name: envoy 237 image: envoyproxy/envoy:v1.10.0 238 securityContext: 239 runAsUser: 1111 240 volumeMounts: 241 - readOnly: true 242 mountPath: /config 243 name: proxy-config 244 args: 245 - "envoy" 246 - "--config-path" 247 - "/config/envoy.yaml" 248 - name: opa 249 # Note: openpolicyagent/opa:latest-istio is created by retagging 250 # the latest released image of OPA-Istio. 251 image: openpolicyagent/opa:{{< current_opa_istio_docker_version >}} 252 securityContext: 253 runAsUser: 1111 254 volumeMounts: 255 - readOnly: true 256 mountPath: /policy 257 name: opa-policy 258 args: 259 - "run" 260 - "--server" 261 - "--set=plugins.envoy_ext_authz_grpc.addr=:9191" 262 - "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow" 263 - "--set=decision_logs.console=true" 264 - "--ignore=.*" 265 - "/policy/policy.rego" 266 volumes: 267 - name: proxy-config 268 configMap: 269 name: proxy-config 270 - name: opa-policy 271 secret: 272 secretName: opa-policy 273``` 274 275```bash 276kubectl apply -f deployment.yaml 277``` 278 279> The `proxy-init` container installs iptables rules to redirect all container traffic through the Envoy proxy sidecar. More information can be found [here](https://github.com/open-policy-agent/contrib/tree/master/envoy_iptables). 280 281### 5. Create a Service to expose HTTP server 282 283```bash 284kubectl expose deployment example-app --type=NodePort --name=example-app-service --port=8080 285``` 286 287Set the `SERVICE_URL` environment variable to the service's IP/port. 288 289**minikube:** 290 291```bash 292export SERVICE_PORT=$(kubectl get service example-app-service -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}') 293export SERVICE_HOST=$(minikube ip) 294export SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT 295echo $SERVICE_URL 296``` 297 298**minikube (example):** 299 300```bash 301192.168.99.113:31056 302``` 303 304### 6. Exercise the OPA policy 305 306For convenience, we’ll want to store Alice's and Bob's tokens in environment variables. 307 308```bash 309export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU" 310export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8" 311``` 312 313Check that `Alice` can get employees **but cannot** create one. 314 315```bash 316curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" http://$SERVICE_URL/people 317curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" -d '{"firstname":"Charlie", "lastname":"OPA"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people 318``` 319 320Check that `Bob` can get employees and also create one. 321 322```bash 323curl -i -H "Authorization: Bearer "$BOB_TOKEN"" http://$SERVICE_URL/people 324curl -i -H "Authorization: Bearer "$BOB_TOKEN"" -d '{"firstname":"Charlie", "lastname":"Opa"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people 325``` 326 327Check that `Bob` **cannot** create an employee with the same firstname as himself. 328 329```bash 330curl -i -H "Authorization: Bearer "$BOB_TOKEN"" -d '{"firstname":"Bob", "lastname":"Rego"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people 331``` 332 333## Wrap Up 334 335Congratulations for finishing the tutorial ! 336 337This tutorial showed how to use OPA as an External authorization service to enforce custom policies by leveraging Envoy’s External authorization filter. 338 339This tutorial also showed a sample OPA policy that returns a `boolean` decision to indicate whether a request should be allowed or not. 340 341Envoy's external authorization filter allows optional response headers and body to be sent to the downstream client or upstream. An example of a rule that returns an object that not only indicates if a request is allowed or not but also provides optional response headers, body and HTTP status that can be sent to the downstream client or upstream can be seen [here](https://github.com/open-policy-agent/opa-istio-plugin#example-policy-with-object-response). 342