Envoy is a L7 proxy and communication bus designed for large modern service oriented architectures. Envoy (v1.7.0+) supports an External Authorization filter which calls an authorization service to check if the incoming request is authorized or not.
This 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.
Goals
The 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.
Prerequisites
This tutorial requires Kubernetes 1.14 or later. To run the tutorial locally, we recommend using minikube in version v1.0+
with Kubernetes 1.14 (which is the default).
Steps
1. Start Minikube
minikube start
2. Create ConfigMap containing configuration for Envoy
The Envoy configuration below defines an external authorization filter envoy.ext_authz
for a gRPC authorization server.
Save the configuration as envoy.yaml:
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 8000
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: service
http_filters:
- name: envoy.ext_authz
config:
with_request_body:
max_request_bytes: 8192
allow_partial_message: true
failure_mode_allow: false
grpc_service:
google_grpc:
target_uri: 127.0.0.1:9191
stat_prefix: ext_authz
timeout: 0.5s
- name: envoy.router
typed_config: {}
clusters:
- name: service
connect_timeout: 0.25s
type: strict_dns
lb_policy: round_robin
load_assignment:
cluster_name: service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8080
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 8001
Create the ConfigMap:
kubectl create configmap proxy-config --from-file envoy.yaml
3. Define a OPA policy
The following OPA policy restricts access to the /people
endpoint exposed by our sample app:
- Alice is granted a guest role and can perform a
GET
request to/people
. - Bob is granted an admin role and can perform a
GET
andPOST
request to/people
.
The policy also restricts an admin
user, in this case bob
from creating an employee with the same firstname
as himself.
policy.rego
package envoy.authz
import input.attributes.request.http as http_request
default allow = false
token = {"valid": valid, "payload": payload} {
[_, encoded] := split(http_request.headers.authorization, " ")
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}
allow {
is_token_valid
action_allowed
}
is_token_valid {
token.valid
token.payload.nbf <= time.now_ns() < token.payload.exp
}
action_allowed {
http_request.method == "GET"
token.payload.role == "guest"
glob.match("/people*", [], http_request.path)
}
action_allowed {
http_request.method == "GET"
token.payload.role == "admin"
glob.match("/people*", [], http_request.path)
}
action_allowed {
http_request.method == "POST"
token.payload.role == "admin"
glob.match("/people", [], http_request.path)
lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
}
Store the policy in Kubernetes as a Secret.
kubectl create secret generic opa-policy --from-file policy.rego
In the next step, OPA is configured to query for the data.envoy.authz.allow
decision. If the response is true
the operation is allowed, otherwise the operation is denied. Sample input received by OPA is shown below:
data.envoy.authz.allow
{
"attributes": {
"request": {
"http": {
"method": "GET",
"path": "/people",
"headers": {
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU"
}
}
}
}
}
With the input value above, the answer is:
false
An example of the complete input received by OPA can be seen here.
In typical deployments the policy would either be built into the OPA container image or it would fetched dynamically via the Bundle API. ConfigMaps are used in this tutorial for test purposes.
4. Create App Deployment with OPA and Envoy sidecars
Our 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.
OPA 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.
Save the deployment as deployment.yaml:
kind: Deployment
apiVersion: apps/v1
metadata:
name: example-app
labels:
app: example-app
spec:
replicas: 1
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
initContainers:
- name: proxy-init
image: openpolicyagent/proxy_init:v2
args: ["-p", "8000", "-u", "1111"]
securityContext:
capabilities:
add:
- NET_ADMIN
runAsNonRoot: false
runAsUser: 0
containers:
- name: app
image: openpolicyagent/demo-test-server:v1
ports:
- containerPort: 8080
- name: envoy
image: envoyproxy/envoy:v1.10.0
securityContext:
runAsUser: 1111
volumeMounts:
- readOnly: true
mountPath: /config
name: proxy-config
args:
- "envoy"
- "--config-path"
- "/config/envoy.yaml"
- name: opa
# Note: openpolicyagent/opa:latest-istio is created by retagging
# the latest released image of OPA-Istio.
image: openpolicyagent/opa:0.15.1-istio
securityContext:
runAsUser: 1111
volumeMounts:
- readOnly: true
mountPath: /policy
name: opa-policy
args:
- "run"
- "--server"
- "--set=plugins.envoy_ext_authz_grpc.addr=:9191"
- "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow"
- "--set=decision_logs.console=true"
- "--ignore=.*"
- "/policy/policy.rego"
volumes:
- name: proxy-config
configMap:
name: proxy-config
- name: opa-policy
secret:
secretName: opa-policy
kubectl apply -f deployment.yaml
The
proxy-init
container installs iptables rules to redirect all container traffic through the Envoy proxy sidecar. More information can be found here.
5. Create a Service to expose HTTP server
kubectl expose deployment example-app --type=NodePort --name=example-app-service --port=8080
Set the SERVICE_URL
environment variable to the service’s IP/port.
minikube:
export SERVICE_PORT=$(kubectl get service example-app-service -o jsonpath='{.spec.ports[?(@.port==8080)].nodePort}')
export SERVICE_HOST=$(minikube ip)
export SERVICE_URL=$SERVICE_HOST:$SERVICE_PORT
echo $SERVICE_URL
minikube (example):
192.168.99.113:31056
6. Exercise the OPA policy
For convenience, we’ll want to store Alice’s and Bob’s tokens in environment variables.
export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZ3Vlc3QiLCJzdWIiOiJZV3hwWTJVPSIsIm5iZiI6MTUxNDg1MTEzOSwiZXhwIjoxNjQxMDgxNTM5fQ.K5DnnbbIOspRbpCr2IKXE9cPVatGOCBrBQobQmBmaeU"
export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiJZbTlpIiwibmJmIjoxNTE0ODUxMTM5LCJleHAiOjE2NDEwODE1Mzl9.WCxNAveAVAdRCmkpIObOTaSd0AJRECY2Ch2Qdic3kU8"
Check that Alice
can get employees but cannot create one.
curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" http://$SERVICE_URL/people
curl -i -H "Authorization: Bearer "$ALICE_TOKEN"" -d '{"firstname":"Charlie", "lastname":"OPA"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people
Check that Bob
can get employees and also create one.
curl -i -H "Authorization: Bearer "$BOB_TOKEN"" http://$SERVICE_URL/people
curl -i -H "Authorization: Bearer "$BOB_TOKEN"" -d '{"firstname":"Charlie", "lastname":"Opa"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people
Check that Bob
cannot create an employee with the same firstname as himself.
curl -i -H "Authorization: Bearer "$BOB_TOKEN"" -d '{"firstname":"Bob", "lastname":"Rego"}' -H "Content-Type: application/json" -X POST http://$SERVICE_URL/people
Wrap Up
Congratulations for finishing the tutorial !
This tutorial showed how to use OPA as an External authorization service to enforce custom policies by leveraging Envoy’s External authorization filter.
This tutorial also showed a sample OPA policy that returns a boolean
decision to indicate whether a request should be allowed or not.
Envoy’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.