Policy based authorization using Kyverno
Delegate Layer 7 authorization decision logic using Kyverno's Authz Server, leveraging policies based on CEL.
Istio supports integration with many different projects. The Istio blog recently featured a post on L7 policy functionality with OpenPolicyAgent. Kyverno is a similar project, and today we will dive how Istio and the Kyverno Authz Server can be used together to enforce Layer 7 policies in your platform.
We will show you how to get started with a simple example. You will come to see how this combination is a solid option to deliver policy quickly and transparently to application team everywhere in the business, while also providing the data the security teams need for audit and compliance.
Try it out
When integrated with Istio, the Kyverno Authz Server can be used to enforce fine-grained access control policies for microservices.
This guide shows how to enforce access control policies for a simple microservices application.
Prerequisites
- A Kubernetes cluster with Istio installed.
- The
istioctl
command-line tool installed.
Install Istio and configure your mesh options to enable Kyverno:
$ istioctl install -y -f - <<EOF
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
meshConfig:
accessLogFile: /dev/stdout
accessLogFormat: |
[KYVERNO DEMO] my-new-dynamic-metadata: '%DYNAMIC_METADATA(envoy.filters.http.ext_authz)%'
extensionProviders:
- name: kyverno-authz-server
envoyExtAuthzGrpc:
service: kyverno-authz-server.kyverno.svc.cluster.local
port: '9081'
EOF
Notice that in the configuration, we define an extensionProviders
section that points to the Kyverno Authz Server installation:
[...]
extensionProviders:
- name: kyverno-authz-server
envoyExtAuthzGrpc:
service: kyverno-authz-server.kyverno.svc.cluster.local
port: '9081'
[...]
Deploy the Kyverno Authz Server
The Kyverno Authz Server is a GRPC server capable of processing Envoy External Authorization requests.
It is configurable using Kyverno AuthorizationPolicy
resources, either stored in-cluster or provided externally.
$ kubectl create ns kyverno
$ kubectl label namespace kyverno istio-injection=enabled
$ helm install kyverno-authz-server --namespace kyverno --wait --repo https://kyverno.github.io/kyverno-envoy-plugin kyverno-authz-server
Deploy the sample application
httpbin is a well-known application that can be used to test HTTP requests and helps to show quickly how we can play with the request and response attributes.
$ kubectl create ns my-app
$ kubectl label namespace my-app istio-injection=enabled
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.24/samples/httpbin/httpbin.yaml -n my-app
Deploy an Istio AuthorizationPolicy
An AuthorizationPolicy
defines the services that will be protected by the Kyverno Authz Server.
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: my-kyverno-authz
namespace: istio-system # This enforce the policy on all the mesh, istio-system being the mesh root namespace
spec:
selector:
matchLabels:
ext-authz: enabled
action: CUSTOM
provider:
name: kyverno-authz-server
rules: [{}] # Empty rules, it will apply to selectors with ext-authz: enabled label
EOF
Notice that in this resource, we define the Kyverno Authz Server extensionProvider
you set in the Istio configuration:
[...]
provider:
name: kyverno-authz-server
[...]
Label the app to enforce the policy
Let’s label the app to enforce the policy. The label is needed for the Istio AuthorizationPolicy
to apply to the sample application pods.
$ kubectl patch deploy httpbin -n my-app --type=merge -p='{
"spec": {
"template": {
"metadata": {
"labels": {
"ext-authz": "enabled"
}
}
}
}
}'
Deploy a Kyverno AuthorizationPolicy
A Kyverno AuthorizationPolicy
defines the rules used by the Kyverno Authz Server to make a decision based on a given Envoy CheckRequest.
It uses the CEL language to analyze an incoming CheckRequest
and is expected to produce a CheckResponse in return.
The incoming request is available under the object
field, and the policy can define variables
that will be made available to all authorizations
.
$ kubectl apply -f - <<EOF
apiVersion: envoy.kyverno.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: demo-policy.example.com
spec:
failurePolicy: Fail
variables:
- name: force_authorized
expression: object.attributes.request.http.?headers["x-force-authorized"].orValue("")
- name: allowed
expression: variables.force_authorized in ["enabled", "true"]
authorizations:
- expression: >
variables.allowed
? envoy.Allowed().Response()
: envoy.Denied(403).Response()
EOF
Notice that you can build the CheckResponse
by hand or use CEL helper functions like envoy.Allowed()
and envoy.Denied(403)
to simplify creating the response message:
[...]
- expression: >
variables.allowed
? envoy.Allowed().Response()
: envoy.Denied(403).Response()
[...]
How it works
When applying the AuthorizationPolicy
, the Istio control plane (istiod) sends the required configurations to the sidecar proxy (Envoy) of the selected services in the policy.
Envoy will then send the request to the Kyverno Authz Server to check if the request is allowed or not.
The Envoy proxy works by configuring filters in a chain. One of those filters is ext_authz
, which implements an external authorization service with a specific message. Any server implementing the correct protobuf can connect to the Envoy proxy and provide the authorization decision; The Kyverno Authz Server is one of those servers.
Reviewing Envoy’s Authorization service documentation, you can see that the message has these attributes:
Ok response
{ "status": {...}, "ok_response": { "headers": [], "headers_to_remove": [], "response_headers_to_add": [], "query_parameters_to_set": [], "query_parameters_to_remove": [] }, "dynamic_metadata": {...} }
Denied response
{ "status": {...}, "denied_response": { "status": {...}, "headers": [], "body": "..." }, "dynamic_metadata": {...} }
This means that based on the response from the authz server, Envoy can add or remove headers, query parameters, and even change the response body.
We can do this as well, as documented in the Kyverno Authz Server documentation.
Testing
Let’s test the simple usage (authorization) and then let’s create a more advanced policy to show how we can use the Kyverno Authz Server to modify the request and response.
Deploy an app to run curl commands to the httpbin sample application:
$ kubectl apply -n my-app -f https://raw.githubusercontent.com/istio/istio/release-1.24/samples/curl/curl.yaml
Apply the policy:
$ kubectl apply -f - <<EOF
apiVersion: envoy.kyverno.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: demo-policy.example.com
spec:
failurePolicy: Fail
variables:
- name: force_authorized
expression: object.attributes.request.http.?headers["x-force-authorized"].orValue("")
- name: allowed
expression: variables.force_authorized in ["enabled", "true"]
authorizations:
- expression: >
variables.allowed
? envoy.Allowed().Response()
: envoy.Denied(403).Response()
EOF
The simple scenario is to allow requests if they contain the header x-force-authorized
with the value enabled
or true
.
If the header is not present or has a different value, the request will be denied.
In this case, we combined allow and denied response handling in a single expression. However it is possible to use multiple expressions, the first one returning a non null response will be used by the Kyverno Authz Server, this is useful when a rule doesn’t want to make a decision and delegate to the next rule:
[...]
authorizations:
# allow the request when the header value matches
- expression: >
variables.allowed
? envoy.Allowed().Response()
: null
# else deny the request
- expression: >
envoy.Denied(403).Response()
[...]
Simple rule
The following request will return 403
:
$ kubectl exec -n my-app deploy/curl -- curl -s -w "\nhttp_code=%{http_code}" httpbin:8000/get
The following request will return 200
:
$ kubectl exec -n my-app deploy/curl -- curl -s -w "\nhttp_code=%{http_code}" httpbin:8000/get -H "x-force-authorized: true"
Advanced manipulations
Now the more advanced use case, apply the second policy:
$ kubectl apply -f - <<EOF
apiVersion: envoy.kyverno.io/v1alpha1
kind: AuthorizationPolicy
metadata:
name: demo-policy.example.com
spec:
variables:
- name: force_authorized
expression: object.attributes.request.http.headers[?"x-force-authorized"].orValue("") in ["enabled", "true"]
- name: force_unauthenticated
expression: object.attributes.request.http.headers[?"x-force-unauthenticated"].orValue("") in ["enabled", "true"]
- name: metadata
expression: '{"my-new-metadata": "my-new-value"}'
authorizations:
# if force_unauthenticated -> 401
- expression: >
variables.force_unauthenticated
? envoy
.Denied(401)
.WithBody("Authentication Failed")
.Response()
: null
# if force_authorized -> 200
- expression: >
variables.force_authorized
? envoy
.Allowed()
.WithHeader("x-validated-by", "my-security-checkpoint")
.WithoutHeader("x-force-authorized")
.WithResponseHeader("x-add-custom-response-header", "added")
.Response()
.WithMetadata(variables.metadata)
: null
# else -> 403
- expression: >
envoy
.Denied(403)
.WithBody("Unauthorized Request")
.Response()
EOF
In that policy, you can see:
- If the request has the
x-force-unauthenticated: true
header (orx-force-unauthenticated: enabled
), we will return401
with the “Authentication Failed” body - Else, if the request has the
x-force-authorized: true
header (orx-force-authorized: enabled
), we will return200
and manipulate request headers, response headers and inject dynamic metadata - In all other cases, we will return
403
with the “Unauthorized Request” body
The corresponding CheckResponse will be returned to the Envoy proxy from the Kyverno Authz Server. Envoy will use those values to modify the request and response accordingly.
Change returned body
Let’s test the new capabilities:
$ kubectl exec -n my-app deploy/curl -- curl -s -w "\nhttp_code=%{http_code}" httpbin:8000/get
Now we can change the response body.
With 403
the body will be changed to “Unauthorized Request”, running the previous command, you should receive:
Unauthorized Request
http_code=403
Change returned body and status code
Running the request with the header x-force-unauthenticated: true
:
$ kubectl exec -n my-app deploy/curl -- curl -s -w "\nhttp_code=%{http_code}" httpbin:8000/get -H "x-force-unauthenticated: true"
This time you should receive the body “Authentication Failed” and error 401
:
Authentication Failed
http_code=401
Adding headers to request
Running a valid request:
$ kubectl exec -n my-app deploy/curl -- curl -s -w "\nhttp_code=%{http_code}" httpbin:8000/get -H "x-force-authorized: true"
You should receive the echo body with the new header x-validated-by: my-security-checkpoint
and the header x-force-authorized
removed:
[...]
"X-Validated-By": [
"my-security-checkpoint"
]
[...]
http_code=200
Adding headers to response
Running the same request but showing only the header:
$ kubectl exec -n my-app deploy/curl -- curl -s -I -w "\nhttp_code=%{http_code}" httpbin:8000/get -H "x-force-authorized: true"
You will find the response header added during the Authz check x-add-custom-response-header: added
:
HTTP/1.1 200 OK
[...]
x-add-custom-response-header: added
[...]
http_code=200
Sharing data between filters
Finally, you can pass data to the following Envoy filters using dynamic_metadata
.
This is useful when you want to pass data to another ext_authz
filter in the chain or you want to print it in the application logs.
To do so, review the access log format you set earlier:
[...]
accessLogFormat: |
[KYVERNO DEMO] my-new-dynamic-metadata: "%DYNAMIC_METADATA(envoy.filters.http.ext_authz)%"
[...]
DYNAMIC_METADATA
is a reserved keyword to access the metadata object. The rest is the name of the filter that you want to access.
In our case, the name envoy.filters.http.ext_authz
is created automatically by Istio. You can verify this by dumping the Envoy configuration:
$ istioctl pc all deploy/httpbin -n my-app -oyaml | grep envoy.filters.http.ext_authz
You will see the configurations for the filter.
Let’s test the dynamic metadata. In the advance rule, we are creating a new metadata entry: {"my-new-metadata": "my-new-value"}
.
Run the request and check the logs of the application:
$ kubectl exec -n my-app deploy/curl -- curl -s -I httpbin:8000/get -H "x-force-authorized: true"
$ kubectl logs -n my-app deploy/httpbin -c istio-proxy --tail 1
You will see in the output the new attributes configured by the Kyverno policy:
[...]
[KYVERNO DEMO] my-new-dynamic-metadata: '{"my-new-metadata":"my-new-value","ext_authz_duration":5}'
[...]
Conclusion
In this guide, we have shown how to integrate Istio and the Kyverno Authz Server to enforce policies for a simple microservices application. We also showed how to use policies to modify the request and response attributes.
This is the foundational example for building a platform-wide policy system that can be used by all application teams.