From 3d139b53f83d44eab985e902fc8699f87a21413b Mon Sep 17 00:00:00 2001 From: Ruoyu Ying Date: Wed, 24 Jul 2024 10:52:13 +0800 Subject: [PATCH] gmc: add authN & authZ support on keycloak (#204) * gmc: add authN & authZ support on keycloak * add gateway, authN and authZ yaml for keycloak option * add documentation for keycloak option Signed-off-by: Ruoyu Ying * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Ruoyu Ying Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Iris --- .../config/authN-authZ/README.md | 90 +++++++++++++++++-- .../authN-authZ/chatQnA_authN_keycloak.yaml | 15 ++++ .../authN-authZ/chatQnA_authZ_keycloak.yaml | 23 +++++ .../authN-authZ/chatQnA_router_gateway.yaml | 34 +++++++ .../config/authN-authZ/keycloak_install.yaml | 66 ++++++++++++++ 5 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 microservices-connector/config/authN-authZ/chatQnA_authN_keycloak.yaml create mode 100644 microservices-connector/config/authN-authZ/chatQnA_authZ_keycloak.yaml create mode 100644 microservices-connector/config/authN-authZ/chatQnA_router_gateway.yaml create mode 100644 microservices-connector/config/authN-authZ/keycloak_install.yaml diff --git a/microservices-connector/config/authN-authZ/README.md b/microservices-connector/config/authN-authZ/README.md index cfbe0b1c..4212a950 100644 --- a/microservices-connector/config/authN-authZ/README.md +++ b/microservices-connector/config/authN-authZ/README.md @@ -10,11 +10,12 @@ Before composing an OPEA pipeline with authN & authZ using GMC, user need to ins **Deploy chatQnA GMC custom resource and enable Istio sidecar injection** ```sh +# make sure you are executing under the microservices-connector folder kubectl create ns chatqa kubectl apply -f $(pwd)/config/samples/chatQnA_xeon.yaml # patch the router deployment to enable istio sidecar injection -kubectl patch deployment -n chatqa router-service-deployment --patch '{ +kubectl patch deployment -n chatqa router-server --patch '{ "spec": { "template": { "metadata": { @@ -27,18 +28,18 @@ kubectl patch deployment -n chatqa router-service-deployment --patch '{ }' ``` -## Perform authentication and authorization via bearer JWT tokens +## Perform authentication and authorization via fake JWT tokens **Apply authentication and authorization policies to the pipeline endpoint based on raw JWT tokens** ```sh # apply the yaml to request authentication using JWT token -kubectl apply -f $(pwd)/config/authN-authZ/chatQnA_authZ_fakejwt.yaml +kubectl apply -f $(pwd)/config/authN-authZ/chatQnA_authZ_fakejwt.yaml -n chatqa # apply the yaml file to request that only JWT token with # issuer & sub == "testing@secure.istio.io" and groups belongs to group1 # can access the endpoint of chatQnA service -kubectl apply -f $(pwd)/config/authN-authZ/chatQnA_authN_fakejwt.yaml +kubectl apply -f $(pwd)/config/authN-authZ/chatQnA_authN_fakejwt.yaml -n chatqa ``` After applying these two yaml files, we have setup the policy that only user with a valid JWT token (with valid issuer and claims) could access the pipeline endpoint. @@ -75,9 +76,86 @@ export accessUrl=$(kubectl get gmc -n chatqa -o jsonpath="{.items[?(@.metadata.n kubectl exec -it -n chatqa $CLIENT_POD -- curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H 'Content-Type: application/json' -w " %{http_code}\n" # try with an invalid token. Shall get response: "RBAC: access denied 403" -kubectl exec -it -n chatqa $CLIENT_POD -- curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H 'Authorization: Bearer $TOKEN1' -H 'Content-Type: application/json' -w " %{http_code}\n" +kubectl exec -it -n chatqa $CLIENT_POD -- curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H "Authorization: Bearer $TOKEN1" -H 'Content-Type: application/json' -w " %{http_code}\n" # try with the valid token. Shall get the correct response -kubectl exec -it -n chatqa $CLIENT_POD -- curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H 'Authorization: Bearer $TOKEN2' -H 'Content-Type: application/json' +kubectl exec -it -n chatqa $CLIENT_POD -- curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H "Authorization: Bearer $TOKEN2" -H 'Content-Type: application/json' +``` + +## Perform authentication and authorization via JWT tokens generated by Keycloak + +Keycloak is an open source identity and access management project to add authentication to applications and secure services with minimum effort. Here we leverage Keycloak to generate a JWT token. + +In this sample, we are going to test with scenario that only privileged users can access our chatQnA service and ask questions. In this case, user `mary` who has the role `user` can access the chatQnA pipeline. And user `bob` with the role `viewer` will not be able to access the service. Of course, the other users without valid token cannot access the service. + +**Keycloak installation and configuration** + +Install Keycloak in the kubernetes cluster for user management. **Note:** Replace the admin password as your own in the keycloak_install.yaml file. + +```bash +cd $(pwd) +kubectl apply -f $(pwd)/config/authN-authZ/keycloak_install.yaml + +# get the ip and port to access keycloak. +export HOST_IP=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' | cut -d '/' -f3 | cut -d ':' -f1) +export KEYCLOAK_PORT=$(kubectl get svc keycloak -o jsonpath='{.spec.ports[0].nodePort}') +export KEYCLOAK_ADDR=${HOST_IP}:${KEYCLOAK_PORT} +``` + +**Note:** Double check if the host ip captured is the correct ip. + +Access the Keycloak admin console through the `KEYCLOAK_ADDR` to configure the users. Use the username and password specified in the yaml file to login. + +The user management is done via Keycloak and the configuration steps look like this: + +1. Create a new realm named `istio` within Keycloak. + +2. Create a new client called `istio` with default configurations. + +3. From the left pane select the Realm roles and create a new role name as `user` and another new role as `viewer`. + +4. Create a new user name as `mary` and another user as `bob`. Set passwords for both users (set 'Temporary' to 'Off'). Select Role mapping on the top, assign the `user` role to `mary` and assign the `viewer` role to `bob`. + +**Apply authentication and authorization policies to the pipeline endpoint based on Keycloak** + +Use the commands to apply the authentication and authorization rules. + +```bash +# export the router service through istio ingress gateway +kubectl apply -f $(pwd)/config/authN-authZ/chatQnA_router_gateway.yaml + +# use 'envsubst' to substitute envs in yaml. +# use 'sudo apt-get install gettext-base' to install envsubst if it does not exist on your machine +# apply the authentication and authorization rule +# these files will restrict user access with valid token (with valid issuer, username and sub) +envsubst < $(pwd)/config/authN-authZ/chatQnA_authN_keycloak.yaml | kubectl -n chatqa apply -f - +envsubst < $(pwd)/config/authN-authZ/chatQnA_authZ_keycloak.yaml | kubectl -n chatqa apply -f - +``` + +**Validate authentication and authorization with different JWT tokens** + +Fetch ID tokens for the two different users. **Note:** Remember to replace the `password` in the curl url and turn off the all the 'Required actions' under the 'Authentication' section. + +```bash +# get id token for mary. Please replace the password inside the url. +export TOKENA=$(curl -X POST 'http://${KEYCLOAK_ADDR}/realms/istio/protocol/openid-connect/token' -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'grant_type=password' --data-urlencode 'client_id=istio' --data-urlencode 'username=mary' -d 'password=${PASSWORD}' -d 'scope=openid' -d 'response_type=id_token' | jq -r .id_token) + +# get id token for bob. Please replace the password inside the url. +export TOKENB=$(curl -X POST 'http://${KEYCLOAK_ADDR}/realms/istio/protocol/openid-connect/token' -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode 'grant_type=password' --data-urlencode 'client_id=istio' --data-urlencode 'username=bob' -d 'password=${PASSWORD}' -d 'scope=openid' -d 'response_type=id_token' | jq -r .id_token) +``` + +Access the istio ingress gateway to reach the chatQnA service with different tokens. Follow the istio guide [here](https://istio.io/latest/docs/tasks/traffic-management/ingress/ingress-control/#determining-the-ingress-ip-and-ports) to determine the ingress IP and ports. + +```bash +# follow the guide above to fetch the $INGRESS_HOST and $INGRESS_PORT +export accessUrl="http://$INGRESS_HOST:$INGRESS_PORT" + +# try without token. Shall get response: "RBAC: access denied 403" +curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H 'Content-Type: application/json' -w " %{http_code}\n" + +# try with token of bob. Shall get response: "RBAC: access denied 403" +curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H "Authorization: Bearer $TOKENB" -H 'Content-Type: application/json' -w " %{http_code}\n" +# try with the valid token from mary. Shall get the correct response from LLM +curl -X POST $accessUrl -d '{"text":"What is the revenue of Nike in 2023?","parameters":{"max_new_tokens":17, "do_sample": true}}' -sS -H "Authorization: Bearer $TOKENA" -H 'Content-Type: application/json' ``` diff --git a/microservices-connector/config/authN-authZ/chatQnA_authN_keycloak.yaml b/microservices-connector/config/authN-authZ/chatQnA_authN_keycloak.yaml new file mode 100644 index 00000000..a0f741c6 --- /dev/null +++ b/microservices-connector/config/authN-authZ/chatQnA_authN_keycloak.yaml @@ -0,0 +1,15 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: security.istio.io/v1 +kind: RequestAuthentication +metadata: + name: jwt-keycloak +spec: + jwtRules: + - issuer: http://${KEYCLOAK_ADDR}/realms/istio + jwksUri: http://${KEYCLOAK_ADDR}/realms/istio/protocol/openid-connect/certs + outputPayloadToHeader: jwt-parsed + selector: + matchLabels: + app: router-service diff --git a/microservices-connector/config/authN-authZ/chatQnA_authZ_keycloak.yaml b/microservices-connector/config/authN-authZ/chatQnA_authZ_keycloak.yaml new file mode 100644 index 00000000..177d4b82 --- /dev/null +++ b/microservices-connector/config/authN-authZ/chatQnA_authZ_keycloak.yaml @@ -0,0 +1,23 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: router +spec: + action: ALLOW + rules: + - when: + - key: request.auth.claims[iss] + values: + - 'http://${KEYCLOAK_ADDR}/realms/istio' + - key: request.auth.claims[preferred_username] + values: + - 'mary' + - key: request.auth.claims[aud] + values: + - 'istio' + selector: + matchLabels: + app: router-service diff --git a/microservices-connector/config/authN-authZ/chatQnA_router_gateway.yaml b/microservices-connector/config/authN-authZ/chatQnA_router_gateway.yaml new file mode 100644 index 00000000..fbb8f490 --- /dev/null +++ b/microservices-connector/config/authN-authZ/chatQnA_router_gateway.yaml @@ -0,0 +1,34 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: networking.istio.io/v1 +kind: Gateway +metadata: + name: router-gateway +spec: + selector: + istio: ingressgateway + servers: + - hosts: + - '*' + port: + name: http + number: 80 + protocol: HTTP +--- +apiVersion: networking.istio.io/v1 +kind: VirtualService +metadata: + name: chatqna-router + namespace: chatqa +spec: + gateways: + - default/router-gateway + hosts: + - '*' + http: + - route: + - destination: + host: router-service + port: + number: 8080 diff --git a/microservices-connector/config/authN-authZ/keycloak_install.yaml b/microservices-connector/config/authN-authZ/keycloak_install.yaml new file mode 100644 index 00000000..5b8ee0f4 --- /dev/null +++ b/microservices-connector/config/authN-authZ/keycloak_install.yaml @@ -0,0 +1,66 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: Apache-2.0 + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak +spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - args: + - start-dev + env: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + - name: KC_PROXY + value: edge + image: quay.io/keycloak/keycloak:25.0.2 + imagePullPolicy: IfNotPresent + name: keycloak + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /realms/master + port: 8080 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always +--- +apiVersion: v1 +kind: Service +metadata: + name: keycloak +spec: + allocateLoadBalancerNodePorts: true + ports: + - name: http + nodePort: 31504 + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + app: keycloak + type: LoadBalancer