diff --git a/demo/istio/README.md b/demo/istio/README.md index 351a397..e1f2165 100644 --- a/demo/istio/README.md +++ b/demo/istio/README.md @@ -22,129 +22,89 @@ The [bootstrap.sh](bootstrap.sh) script contains everything needed to create a l # create a local cluster and install istio ./bootstrap.sh ``` +### Install kyverno-envoy-sidecar-injector admission controller -### Sample application +First, we need to install the kyverno-envoy-sidecar-injector admission controller to inject kyverno-envoy-plugin sidecar into the sample-application pods(upstream pod which we need to authorization). -Manifests for the sample application are available in [sample-application.yaml](manifests/sample-application.yaml). +Flow this [README](./../../sidecar-injector/README.md) for installation of kyverno-envoy-sidecar-injector admission controller -```console -# deploy sample application -kubectl apply -f ./manifests/sample-application.yaml -``` +### Sample application -### Calling the sample application +Manifests for the sample application are available in [test-application.yaml](manifests/test-application.yaml). The sample app provides information about books in a collection and exposes APIs to get, create and delete Book resources. -We are going to call the sample application using a pod in the cluster. +But first we need to apply [kyverno policy configmap](manifests/policy-config.yaml) this policy will be passed to kyverno-envoy-sidecar: ```console -kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - echo.demo.svc.cluster.local:8080/foo - -{ - "path": "/foo", - "headers": { - "host": "echo.demo.svc.cluster.local:8080", - "user-agent": "Wget", - "x-forwarded-proto": "http", - "x-request-id": "1badcd84-75eb-4911-9835-b3588e3c5eee", - "x-b3-traceid": "904f847c3db71758fa4076e48440800a", - "x-b3-spanid": "fa4076e48440800a", - "x-b3-sampled": "0" - }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "echo.demo.svc.cluster.local", - "ip": "::ffff:127.0.0.6", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [ - "svc", - "demo", - "echo" - ], - "xhr": false, - "os": { - "hostname": "echo-6847f9f85-wbgbx" - }, - "connection": {} -} +kubectl apply -f ./manifests/namespace.yaml ``` -### Authorization policy +```console +kubectl apply -f ./manifests/policy-config.yaml +``` -Now we can deploy an istio `AuthorizationPolicy`: +```console +# deploy sample application +kubectl apply -f ./manifests/test-application.yaml +``` +Check that their should be three containers should be running in the pod. ```console -# deploy authorisation policy -kubectl apply -f - < 8080/TCP 4h5m -The provider will be registered later in the istio config map. +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/echo 1/1 1 1 3h59m -### Calling the sample application again +NAME DESIRED CURRENT READY AGE +replicaset.apps/echo-55c77757f4 1 1 1 3h59m -Calling the sample application again at the `/foo` path will return `403 Forbidden`. +``` -```console -kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - echo.demo.svc.cluster.local:8080/foo +### Calling the sample application -wget: server returned error: HTTP/1.1 403 Forbidden +We are going to call the sample application using a pod in the cluster. + +```console +kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - testapp.demo.svc.cluster.local:8080/book +``` +output +``` +[{"id":"1","bookname":"Harry Potter","author":"J.K. Rowling"},{"id":"2","bookname":"Animal Farm","author":"George Orwell"}] +pod "test" deleted ``` -Note that calling another path (like `/bar`) succeeds as it's not part of the policy. +### ServiceEntry -```console -kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --output-document - echo.demo.svc.cluster.local:8080/bar +ServiceEntry to registor the kyverno-envoy-plugin sidecar as external authorizer. -{ - "path": "/bar", - "headers": { - "host": "echo.demo.svc.cluster.local:8080", - "user-agent": "Wget", - "x-forwarded-proto": "http", - "x-request-id": "ca22cf4c-fd28-4dff-94a1-bc0611d710a4", - "x-b3-traceid": "202ef8abae854851c12c033ff52252e4", - "x-b3-spanid": "c12c033ff52252e4", - "x-b3-sampled": "0" - }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "echo.demo.svc.cluster.local", - "ip": "::ffff:127.0.0.6", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [ - "svc", - "demo", - "echo" - ], - "xhr": false, - "os": { - "hostname": "echo-6847f9f85-wbgbx" - }, - "connection": {} -} +```console +kubectl apply -f ./manifests/service-entry.yaml +``` +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: ServiceEntry +metadata: + name: kyverno-ext-authz-grpc-local +spec: + hosts: + - "kyverno-ext-authz-grpc.local" + # The service name to be used in the extension provider in the mesh config. + endpoints: + - address: "127.0.0.1" + ports: + - name: grpc + number: 9000 + # The port number to be used in the extension provider in the mesh config. + protocol: GRPC + resolution: STATIC ``` - ### Register authorization provider Edit the mesh configmap to register authorization provider with the following command: @@ -161,112 +121,161 @@ In the editor, add the extension provider definitions to the mesh configmap. extensionProviders: - name: "kyverno-ext-authz-grpc" envoyExtAuthzGrpc: - service: "ext-authz.demo.svc.cluster.local" + service: "kyverno-ext-authz-grpc.local" port: "9000" - - name: "kyverno-ext-authz-http" - envoyExtAuthzHttp: - service: "ext-authz.demo.svc.cluster.local" - port: "8000" - includeRequestHeadersInCheck: ["x-ext-authz"] ``` -### Authorization service +### Authorization policy -The following command will deploy the sample external authorizer which allows requests with the header `x-ext-authz: allow`: +Now we can deploy an istio `AuthorizationPolicy`: +AuthorizationPolicy to tell Istio to use kyverno-envoy-plugin as the Authz Server ```console -kubectl apply -n demo -f https://raw.githubusercontent.com/istio/istio/release-1.20/samples/extauthz/ext-authz.yaml +kubectl apply -f ./manifests/authorizationpolicy.yaml +``` +```yaml +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: kyverno-ext-authz-grpc + namespace: demo +spec: + action: CUSTOM + provider: + # The provider name must match the extension provider defined in the mesh config. + name: kyverno-ext-authz-grpc + rules: + # The rules specify when to trigger the external authorizer. + - to: + - operation: + notPaths: ["/healthz"] + # Allowed all path except /healthz ``` -Verify the sample external authorizer is up and running: -```console -kubectl logs "$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})" -n demo -c ext-authz +This policy configures an external service for authorization. Note that the service is not specified directly in the policy but using a `provider.name` field. -2024/03/12 11:46:42 Starting gRPC server at [::]:9000 -2024/03/12 11:46:42 Starting HTTP server at [::]:8000 +### Verify the authorization -``` +For convenience, we’ll want to store Alice’s and Bob’s tokens in environment variables. Here bob is assigned the admin role and alice is assigned the guest role. +```bash +export ALICE_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk" +export BOB_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6ImFkbWluIiwic3ViIjoiWVd4cFkyVT0ifQ.veMeVDYlulTdieeX-jxFZ_tCmqQ_K8rwx2OktUHv5Z0" +``` -### Calling the sample application again +In this policy , we are using the Kyverno JSON API to define a Validating which we have already applied to the cluster. +The policy checks the conditions of the incoming request and denies the request if the user is a guest and the request method is POST at the /book path. -Calling the sample application again at the `/foo` path with with header `x-ext-authz: allow` will succeed. +```yaml +apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + name: checkrequest + spec: + rules: + - name: deny-guest-request-at-post + assert: + any: + - message: "POST method calls at path /book are not allowed to guests users" + check: + request: + http: + method: POST + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): admin + path: /book + - message: "GET method call is allowed to both guest and admin users" + check: + request: + http: + method: GET + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): admin + path: /book + - message: "GET method call is allowed to both guest and admin users" + check: + request: + http: + method: GET + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): guest + path: /book + +``` -```console -kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="x-ext-authz: allow" --output-document - echo.demo.svc.cluster.local:8080/foo +Here is the Comman format of CheckRequest payload, Envoy transmits a [CheckRequest](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkrequest) in Protobuf format to an external authorization service (which is our kyverno-envoy-plugin) for making access control decisions. This payload is then converted into a JSON format (inside kyverno-envoy-plugin) and evaluated against the defined policy within the Kyverno JSON engine. +```json { - "path": "/foo", - "headers": { - "host": "echo.demo.svc.cluster.local:8080", - "user-agent": "Wget", - "x-ext-authz": "allow", - "x-forwarded-proto": "http", - "x-request-id": "2ef1a0ce-6948-413e-a9a9-91c5b9242b5c", - "x-ext-authz-check-result": "allowed", - "x-ext-authz-check-received": "source:{address:{socket_address:{address:\"10.244.1.7\" port_value:52396}}} destination:{address:{socket_address:{address:\"10.244.1.3\" port_value:8080}}} request:{time:{seconds:1710245883 nanos:556386000} http:{id:\"15150282829336904450\" method:\"GET\" headers:{key:\":authority\" value:\"echo.demo.svc.cluster.local:8080\"} headers:{key:\":method\" value:\"GET\"} headers:{key:\":path\" value:\"/foo\"} headers:{key:\":scheme\" value:\"http\"} headers:{key:\"user-agent\" value:\"Wget\"} headers:{key:\"x-ext-authz\" value:\"allow\"} headers:{key:\"x-forwarded-proto\" value:\"http\"} headers:{key:\"x-request-id\" value:\"2ef1a0ce-6948-413e-a9a9-91c5b9242b5c\"} path:\"/foo\" host:\"echo.demo.svc.cluster.local:8080\" scheme:\"http\" protocol:\"HTTP/1.1\"}} metadata_context:{}", - "x-ext-authz-additional-header-override": "grpc-additional-header-override-value", - "x-b3-traceid": "ddc174607e9d88bf1830b48578b53e79", - "x-b3-spanid": "1830b48578b53e79", - "x-b3-sampled": "0" + "source": { + "address": { + "socketAddress": { + "address": "10.244.1.10", + "portValue": 59252 + } + } + }, + "destination": { + "address": { + "socketAddress": { + "address": "10.244.1.4", + "portValue": 8080 + } + } }, - "method": "GET", - "body": "", - "fresh": false, - "hostname": "echo.demo.svc.cluster.local", - "ip": "::ffff:127.0.0.6", - "ips": [], - "protocol": "http", - "query": {}, - "subdomains": [ - "svc", - "demo", - "echo" - ], - "xhr": false, - "os": { - "hostname": "echo-6847f9f85-fg9pd" + "request": { + "time": "2024-04-09T07:42:29.634453Z", + "http": { + "id": "14694995155993896575", + "method": "GET", + "headers": { + ":authority": "testapp.demo.svc.cluster.local:8080", + ":method": "GET", + ":path": "/book", + ":scheme": "http", + "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNDEwODE1MzksIm5iZiI6MTUxNDg1MTEzOSwicm9sZSI6Imd1ZXN0Iiwic3ViIjoiWVd4cFkyVT0ifQ.ja1bgvIt47393ba_WbSBm35NrUhdxM4mOVQN8iXz8lk", + "user-agent": "Wget", + "x-forwarded-proto": "http", + "x-request-id": "27cd2724-e0f4-4a69-a1b1-9a94edfa31bb" + }, + "path": "/book", + "host": "echo.demo.svc.cluster.local:8080", + "scheme": "http", + "protocol": "HTTP/1.1" + } }, - "connection": {} -}pod "test" deleted + "metadataContext": {}, + "routeMetadataContext": {} +} ``` -Calling the sample application again at the `/foo` path with with header `x-ext-authz: deny` will be denied. - -```console - -kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="x-ext-authz: deny" --output-document - ec -ho.demo.svc.cluster.local:8080/foo - -wget: server returned error: HTTP/1.1 403 Forbidden -pod "test" deleted -pod default/test terminated (Error) +Check for `Alice` which can get book but cannot create book. +```bash +kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="authorization: Bearer "$ALICE_TOKEN"" --output-document - testapp.demo.svc.cluster.local:8080/book ``` -Check the log of the sample ext_authz server to confirm it was called twice. The first one was allowed and the second one was denied: - -```console -kubectl logs "$(kubectl get pod -l app=ext-authz -n demo -o jsonpath={.items..metadata.name})" -n demo -c ext-authz -f - - -2024/03/12 11:55:26 Starting HTTP server at [::]:8000 -2024/03/12 11:55:26 Starting gRPC server at [::]:9000 -2024/03/12 12:18:03 [gRPCv3][allowed]: echo.demo.svc.cluster.local:8080/foo, attributes: source:{address:{socket_address:{address:"10.244.1.7" port_value:52396}}} destination:{address:{socket_address:{address:"10.244.1.3" port_value:8080}}} request:{time:{seconds:1710245883 nanos:556386000} http:{id:"15150282829336904450" method:"GET" headers:{key:":authority" value:"echo.demo.svc.cluster.local:8080"} headers:{key:":method" value:"GET"} headers:{key:":path" value:"/foo"} headers:{key:":scheme" value:"http"} headers:{key:"user-agent" value:"Wget"} headers:{key:"x-ext-authz" value:"allow"} headers:{key:"x-forwarded-proto" value:"http"} headers:{key:"x-request-id" value:"2ef1a0ce-6948-413e-a9a9-91c5b9242b5c"} path:"/foo" host:"echo.demo.svc.cluster.local:8080" scheme:"http" protocol:"HTTP/1.1"}} metadata_context:{} -2024/03/12 12:18:37 [gRPCv3][denied]: echo.demo.svc.cluster.local:8080/foo, attributes: source:{address:{socket_address:{address:"10.244.1.8" port_value:45762}}} destination:{address:{socket_address:{address:"10.244.1.3" port_value:8080}}} request:{time:{seconds:1710245917 nanos:57648000} http:{id:"2185755048778078711" method:"GET" headers:{key:":authority" value:"echo.demo.svc.cluster.local:8080"} headers:{key:":method" value:"GET"} headers:{key:":path" value:"/foo"} headers:{key:":scheme" value:"http"} headers:{key:"user-agent" value:"Wget"} headers:{key:"x-ext-authz" value:"deny"} headers:{key:"x-forwarded-proto" value:"http"} headers:{key:"x-request-id" value:"007781a3-519e-400f-8562-cabf75e989c1"} path:"/foo" host:"echo.demo.svc.cluster.local:8080" scheme:"http" protocol:"HTTP/1.1"}} metadata_context:{} - +```bash +kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="authorization: Bearer "$ALICE_TOKEN"" --post-data='{"bookname":"Harry Potter", "author":"J.K. Rowling"}' --output-document - testapp.demo.svc.cluster.local:8080/book ``` +Check the `Bob` which can get book also create the book -## Architecture - -The below architecture illustrates a scenario where no service mesh or Envoy-like components have been pre-installed or already installed. - -![Architecture](architecture1.png) +```bash +kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="authorization: Bearer "$BOB_TOKEN"" --output-document - testapp.demo.svc.cluster.local:8080/book +``` -The below architecture illustrates a scenario where a service mesh or Envoy-like components have been pre-installed or already installed. -![Architecture](architecture2.png) +```bash +kubectl run test -it --rm --restart=Never --image=busybox -- wget -q --header="authorization: Bearer "$BOB_TOKEN"" --post-data='{"bookname":"Harry Potter", "author":"J.K. Rowling"}' --output-document - testapp.demo.svc.cluster.local:8080/book +``` -## Requirements +Check on logs +```bash +kubectl logs "$(kubectl get pod -l app=testapp -n demo -o jsonpath={.items..metadata.name})" -n demo -c ext-authz -f -- Istio Authorizationpolicy manifest to add "extension provider " concept in MeshConfig to specify Where/how to talk to envoy ext-authz service -- +``` \ No newline at end of file diff --git a/demo/istio/architecture-istio.png b/demo/istio/architecture-istio.png new file mode 100644 index 0000000..ae3f8cd Binary files /dev/null and b/demo/istio/architecture-istio.png differ diff --git a/demo/istio/architecture1.png b/demo/istio/architecture1.png deleted file mode 100644 index d3fa104..0000000 Binary files a/demo/istio/architecture1.png and /dev/null differ diff --git a/demo/istio/architecture2.png b/demo/istio/architecture2.png deleted file mode 100644 index 8b0c52a..0000000 Binary files a/demo/istio/architecture2.png and /dev/null differ diff --git a/demo/istio/manifests/authorizationpolicy.yaml b/demo/istio/manifests/authorizationpolicy.yaml new file mode 100644 index 0000000..f739158 --- /dev/null +++ b/demo/istio/manifests/authorizationpolicy.yaml @@ -0,0 +1,16 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: kyverno-ext-authz-grpc + namespace: demo +spec: + action: CUSTOM + provider: + # The provider name must match the extension provider defined in the mesh config. + name: kyverno-ext-authz-grpc + rules: + # The rules specify when to trigger the external authorizer. + - to: + - operation: + notPaths: ["/healthz"] + # Allowed all path except /healthz \ No newline at end of file diff --git a/demo/istio/manifests/ext-authz.yaml b/demo/istio/manifests/ext-authz.yaml deleted file mode 100644 index 0a6a512..0000000 --- a/demo/istio/manifests/ext-authz.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: ext-authz - labels: - app: ext-authz - namespace: demo -spec: - ports: - - name: http - port: 8000 - targetPort: 8000 - - name: grpc - port: 9000 - targetPort: 9000 - selector: - app: ext-authz ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ext-authz - namespace: demo -spec: - replicas: 1 - selector: - matchLabels: - app: ext-authz - template: - metadata: - labels: - app: ext-authz - spec: - containers: - - image: ko.local/github.com/kyverno/kyverno-envoy-plugin:7bd39c9d958eb408a86cee2d97241895522b317f - imagePullPolicy: IfNotPresent - name: ext-authz - ports: - - containerPort: 8000 - - containerPort: 9000 \ No newline at end of file diff --git a/demo/istio/manifests/namespace.yaml b/demo/istio/manifests/namespace.yaml new file mode 100644 index 0000000..48e061b --- /dev/null +++ b/demo/istio/manifests/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: demo + labels: + istio-injection: enabled \ No newline at end of file diff --git a/demo/istio/manifests/policy-config.yaml b/demo/istio/manifests/policy-config.yaml new file mode 100644 index 0000000..f31d03e --- /dev/null +++ b/demo/istio/manifests/policy-config.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: policy-files + namespace: demo +data: + policy.yaml: | + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + name: checkrequest + spec: + rules: + - name: deny-guest-request-at-post + assert: + any: + - message: "POST method calls at path /book are not allowed to guests users" + check: + request: + http: + method: POST + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): admin + path: /book + - message: "GET method call is allowed to both guest and admin users" + check: + request: + http: + method: GET + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): admin + path: /book + - message: "GET method call is allowed to both guest and admin users" + check: + request: + http: + method: GET + headers: + authorization: + (split(@, ' ')[1]): + (jwt_decode(@ , 'secret').payload.role): guest + path: /book + \ No newline at end of file diff --git a/demo/istio/manifests/service-entry.yaml b/demo/istio/manifests/service-entry.yaml new file mode 100644 index 0000000..0ef89a6 --- /dev/null +++ b/demo/istio/manifests/service-entry.yaml @@ -0,0 +1,17 @@ +# ServiceEntry to register the Kyverno-Envoy sidecars as external authorizers. +apiVersion: networking.istio.io/v1beta1 +kind: ServiceEntry +metadata: + name: kyverno-ext-authz-grpc-local +spec: + hosts: + - "kyverno-ext-authz-grpc.local" + # The service name to be used in the extension provider in the mesh config. + endpoints: + - address: "127.0.0.1" + ports: + - name: grpc + number: 9000 + # The port number to be used in the extension provider in the mesh config. + protocol: GRPC + resolution: STATIC \ No newline at end of file diff --git a/demo/istio/manifests/sample-application.yaml b/demo/istio/manifests/test-application.yaml similarity index 58% rename from demo/istio/manifests/sample-application.yaml rename to demo/istio/manifests/test-application.yaml index 13e2bf0..e92d7e7 100644 --- a/demo/istio/manifests/sample-application.yaml +++ b/demo/istio/manifests/test-application.yaml @@ -1,40 +1,34 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: demo - labels: - istio-injection: enabled ---- apiVersion: apps/v1 kind: Deployment metadata: - name: echo + name: testapp namespace: demo spec: replicas: 1 selector: matchLabels: - app: echo + app: testapp template: metadata: labels: - app: echo + kyverno-envoy-sidecar/injection: enabled + app: testapp spec: containers: - - name: echo - image: mendhak/http-https-echo + - name: testapp + image: sanskardevops/test-application:0.0.1 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: - name: echo + name: testapp namespace: demo spec: - type: ClusterIP + type: ClusterIP selector: - app: echo + app: testapp ports: - port: 8080 targetPort: 8080