diff --git a/README.md b/README.md index b772acfd..eb048637 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,11 @@ The [architecture](docs/architecture.md) section of the docs covers the details 2. Have your upstream API [ready](docs/architecture.md#protecting-upstream-apis-with-envoy-and-authorino) to be protected 3. [Write](docs/architecture.md#the-authorino-service-custom-resource-definition-crd) and apply a `config.authorino.3scale.net`/`Service` custom resource declaring the desired state of the protection of your API -## Sample use cases +## Examples and Tutorials -The [Examples](examples) page lists several use cases and demonstrates how to implement those as Authorino custom resources. +The [Examples](examples) page lists several use cases and demonstrates how to implement those as Authorino custom resources. Each example use case presents a feature of Authorino and is independent from the other. + +The Authorino [Tutorials](docs/tutorials.md) provide guided examples for deploying and protecting an API with Authorino and the Envoy proxy, where each tutorial combines multiple features of Authorino into one cohesive use case, resembling real life use cases. ## Terminology diff --git a/docs/deploy.md b/docs/deploy.md index a4b81715..41de8b6f 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -47,7 +47,7 @@ Option A is meant for trying out Authorino locally. It gives you a bundle of a [ Included resources:
- **Talker API**
- Just a simple rack application that echoes back in a JSON whatever is gets in the request. You can control the response by passing the custom HTTP headers X-Echo-Status and X-Echo-Message (both optional). + Just a simple rack application that echoes back in a JSON whatever it gets in the request. You can control the response by passing the custom HTTP headers X-Echo-Status and X-Echo-Message (both optional). - **Authorino**
The Cloud-native AuthN/AuthZ enforcer that looks for `config.authorino.3scale.net/Service` custom resources in the Kubernetes server to add protection to your APIs. - **Envoy proxy**
diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 00000000..98244d51 --- /dev/null +++ b/docs/tutorials.md @@ -0,0 +1,27 @@ +# Authorino Tutorials + +### [Showcase](tutorials/showcase) +8 consecutive sample use cases to protect an API with Authorino and the Envoy proxy, deployed on a local Kubernetes cluster. + +**Authorino features covered in the tutorial:** +- Kubernetes auth +- API key auth +- OpenID Connect JWT validation +- User-Managed Access (UMA)-based attribute data +- JSON authorization policies +- Open Policy Agent (OPA) inline Rego policies + +**Requirements:** +- Docker +- Authorino repo cloned locally + +### [OpenShift demo](tutorials/openshift-demo) +A simple and straightforward demo of Authorino deployed to an OpenShift cluster running in the cloud. + +**Authorino features covered in the tutorial:** +- Kubernetes auth +- API key auth +- JSON authorization policies + +**Requirements:** +- An OpenShift cluster running diff --git a/docs/tutorials/openshift-demo/.gitignore b/docs/tutorials/openshift-demo/.gitignore new file mode 100644 index 00000000..1e82fc7d --- /dev/null +++ b/docs/tutorials/openshift-demo/.gitignore @@ -0,0 +1 @@ +*.yaml diff --git a/docs/tutorials/openshift-demo/README.md b/docs/tutorials/openshift-demo/README.md new file mode 100644 index 00000000..aa7595d2 --- /dev/null +++ b/docs/tutorials/openshift-demo/README.md @@ -0,0 +1,349 @@ +# Tutorial: Authorino on OpenShift + +- [Intro](#intro) +- [Stack](#stack) +- [Deploy](#deploy) +- [Protect the API](#protect-the-api) + - [Protect the API with Kubernetes auth tokens](#protect-the-api-with-kubernetes-auth-tokens) + - [Protect the API with API Key](#protect-the-api-with-api-key) + - [Restrict access to endpoints of the API with JSON authorization policies](#restrict-access-to-endpoints-of-the-api-with-json-authorization-policies) +- [Cleanup](#cleanup) +- [Extras](#extras) + - [Review a Kubernetes token](#review-a-kubernetes-token) + +## Intro + +This tutorial will walk you through the steps of deploying Authorino and the Envoy proxy to a running instance of an OpenShft server, and use it to protect a sample API with 2 authentication methods (Kubernetes tokens and API keys) and authorization policies that restrict access to certain endpoints of the API according to the identity source. + +The following Authorino features are covered in this tutorial: +- Kubernetes auth +- API key auth +- JSON authorization policies + +## Stack + +The following applications compose the stack for this tutorial: + +- **Talker API**
+ Just a simple rack application that echoes back in a JSON whatever it gets in the request. +- **Envoy proxy**
+ Serving the Talker API, configured with the ext_authz http filter pointing to Authorino. +- **Authorino**
+ The AuthN/AuthZ enforcer that will watch and apply Authorino `Service` custom resources in the Kubernetes/OpenShift server. + +## Deploy + +Follow the instructions below to deploy the stack of resources and applications to a running instance of an OpenShift server. + +> **NOTE:** Except for a few OpenShift-specific parts, the rest of the tutorial should work without issues on bare Kubernetes. Examples of OpenShift-specific parts and how to adapt them for bare Kubernetes: +> - Use of `oc` in some commands → replace accordingly with `kubectl` +> - Use of OpenShift `Route` resource to expose the Envoy service → replace accondingly with an `Ingress` resource + +#### Instructions + +1. Obtain an access token to the target OpenShift cluster: + + You can obtain an access token to an OpenShift cluster via OpenShift web console or, if you are logged in with the CLI, by reading the token from the `.kube/conf` file: + + ```sh + $ yq r ~/.kube/config "users(name==$(yq r ~/.kube/config 'current-context' | awk -F "/" '{ print $3"/"$2 }')).user.token" + ``` + +2. Set the envs replacing the values accordingly: + + ```sh + $ export OPENSHIFT_TOKEN=my-openshift-access-token \ + AUTHORINO_NAMESPACE=authorino-demo \ + AUTHORINO_IMAGE=quay.io/3scale/authorino:20210415 \ + TALKER_API_HOST=talker-api.apps.my-openshift-server + ``` + +3. Create the OpenShift project: + + ```sh + $ oc new-project $AUTHORINO_NAMESPACE + ``` + +4. Download the required resources: + + ```sh + $ git clone https://gist.github.com/4a86682282994ac5f9bb1f246f19df39.git authorino-openshift && cd authorino-openshift + ``` + +5. Patch the resources replacing the parameters to your env values: + + ```sh + $ sed -i -e "s/\${AUTHORINO_NAMESPACE}/$AUTHORINO_NAMESPACE/g;s/\${AUTHORINO_IMAGE}/$(print $AUTHORINO_IMAGE | sed -e 's/\//\\\//g')/g;s/\${TALKER_API_HOST}/$(print $TALKER_API_HOST | sed -e 's/\./\\\./g')/g" *.yaml + ``` + +6. Deploy Authorino: + + ```sh + $ kubectl apply -f authorino.yaml + customresourcedefinition.apiextensions.k8s.io/services.config.authorino.3scale.net created + role.rbac.authorization.k8s.io/authorino-leader-election-role created + clusterrole.rbac.authorization.k8s.io/authorino-manager-role created + clusterrole.rbac.authorization.k8s.io/authorino-metrics-reader created + clusterrole.rbac.authorization.k8s.io/authorino-proxy-role created + rolebinding.rbac.authorization.k8s.io/authorino-leader-election-rolebinding created + clusterrolebinding.rbac.authorization.k8s.io/authorino-manager-rolebinding created + clusterrolebinding.rbac.authorization.k8s.io/authorino-proxy-rolebinding created + service/authorino-authorization created + service/authorino-controller-manager-metrics-service created + deployment.apps/authorino-controller-manager created + ``` + +7. Deploy the Talker API: + + ```sh + $ kubectl apply -f talker-api-deploy.yaml + deployment.apps/talker-api created + service/talker-api created + ``` + +8. Deploy the Envoy proxy: + + ```sh + $ kubectl apply -f envoy-deploy.yaml + deployment.apps/envoy created + service/envoy created + route.route.openshift.io/talker-api created + configmap/envoy created + ``` + +## Protect the API + +### Protect the API with Kubernetes auth tokens + +Apply the CR: + +```yaml +# talker-api-protection-1.yaml +apiVersion: config.authorino.3scale.net/v1beta1 +kind: Service +metadata: + name: talker-api-protection +spec: + hosts: + - talker-api.apps.my-openshift-server + identity: + - name: dev-eng-ocp45-users + kubernetes: + audiences: + - https://kubernetes.default.svc # default audience of K8s tokens - change accordingly for less permissive scope +``` + +```sh +$ kubectl apply -f talker-api-protection-1.yaml +route.route.openshift.io/talker-api created +``` + +Send requests to the API: + +```sh +$ curl -k -H "Authorization: Bearer $OPENSHIFT_TOKEN" https://$TALKER_API_HOST/hello +200 OK + +$ curl -k -H "Authorization: Bearer nonono" https://$TALKER_API_HOST/hello +401 Unauthorized +``` + +### Protect the API with API Key + +Apply the CR: + +```yaml +# talker-api-protection-2.yaml +apiVersion: config.authorino.3scale.net/v1beta1 +kind: Service +metadata: + name: talker-api-protection +spec: + hosts: + - talker-api.apps.my-openshift-server + identity: + - name: dev-eng-ocp45-users + kubernetes: {…} + - name: external-access + apiKey: + labelSelectors: + authorino.3scale.net/managed-by: authorino + scope: talker-api + credentials: + in: authorization_header + keySelector: APIKEY + +``` + +```sh +$ kubectl apply -f talker-api-protection-2.yaml +service.config.authorino.3scale.net/talker-api-protection configured +``` + +Create an API key: + +```sh +$ kubectl apply -f - < + Just a simple rack application that echoes back in a JSON whatever it gets in the request. +- **Envoy proxy**
+ Serving the Talker API, configured with the ext_authz http filter pointing to Authorino. +- **Authorino**
+ The AuthN/AuthZ enforcer that will watch and apply Authorino `Service` custom resources in the Kubernetes/OpenShift server. +- **Keycloak**
+ To issue OIDC access tokens and to provide adhoc resource data for the authorization payload. The server is bundled with the following preloaded settings and realm resources: + - Admin console: http://localhost:8080/auth/admin (admin/p) + - Preloaded realm: **kuadrant** + - Preloaded clients: + - **demo**: to which API consumers delegate access and therefore the one which access tokens are issued to + - **authorino**: used by Authorino to fetch additional user info with client_credentials grant type + - **talker-api**: used by Authorino to fetch UMA-protected resource data associated with the Talker API + - Preloaded resources: + - `/hello` + - `/greetings/1` (owned by user john) + - `/greetings/2` (owned by user jane) + - `/goodbye` + - Realm roles: + - member (default to all users) + - admin + - Preloaded users: + - john/p (member) + - jane/p (admin) + - peter/p (member; email not verified) +- **Dex**
+ IdP to issue OIDC access tokens for the webapp. + - Preloaded client: **talker-web**: to get access to the Talker API via webapp + - Preloaded user: marta@localhost/p +- **Talker Web**
+ Webapp to consume resources of the Talker API from a web browser + - URL: http://talker-api-authorino.127.0.0.1.nip.io:8000/web + +## Clone the repo + +```sh +$ git clone git@github.com:3scale-labs/authorino.git && cd authorino +``` + +## Setup the trial local environment + +Launch the Kubernetes cluster on a Dokcer with [Kind](https://kind.sigs.k8s.io), build the latest Authrorino image from source and deploy the main applications of the stack. This step may take up to a few minutes for the cluster and all the deployments to be ready. + +```sh +$ DEPLOY_IDPS=1 make local-setup +``` + +Forward requests from the local host machine to pods running inside the cluster (API, Keycloak server, and Dex server): + +```sh +$ kubectl -n authorino port-forward deployment/envoy 8000:8000 & +$ kubectl -n authorino port-forward deployment/keycloak 8080:8080 & +$ kubectl -n authorino port-forward deployment/dex 5556:5556 & +``` + +Add the `keycloak` host name to your DNS resolution chain, so token requests initiated outside the cluster can have the issuer validated by Keycloak: + +```sh +$ echo '127.0.0.1 keycloak'>>/etc/hosts +``` + +## Use-case #1: Kubernetes authentication + +In this base use case, we want to give access to our protected API (the “Talker API”) to known users of the same Kubernetes server where both the API and Authorino are running. This is a good use for Authorino's **Kubernetes authentication** feature. + +The protection to the API defines an identity group 'same-k8s-server-users', whose users will have full access to the API. + +Apply the CR: + +```sh +$ kubectl -n authorino apply -f docs/tutorials/showcase/showcase-api-protection-1.yaml +``` + +Create a Service Account with permission to issue Kubernetes tokens: + +```sh +$ kubectl -n authorino apply -f - < **NOTE:** The token issued will be immediately valid and will expire after 10 minutes. + +```sh +$ export KUBERNETES_API=$(kubectl cluster-info | head -n 1 | awk '{print $7}' | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g") +$ export TOKEN_ISSUER_TOKEN=$(kubectl -n authorino get secret/$(kubectl -n authorino get sa/sa-token-issuer -o json | jq -r '.secrets[0].name') -o json | jq -r '.data.token' | base64 -d) + +$ export ACCESS_TOKEN=$(curl -k -X "POST" "$KUBERNETES_API/api/v1/namespaces/authorino/serviceaccounts/api-consumer/token" \ + -H "Authorization: Bearer $TOKEN_ISSUER_TOKEN" \ + -H 'Content-Type: application/json; charset=utf-8' \ + -d $'{ "apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest", "spec": { "audiences": ["talker"], "expirationSeconds": 600 } }' | jq -r '.status.token') +``` + +Send requests to the API: + +```sh +$ curl -H "Authorization: Bearer $ACCESS_TOKEN" http://talker-api-authorino.127.0.0.1.nip.io:8000/hello +200 OK +``` + +## Use-case #2: API key authentication + +In this step, we want to expand access to the Talker API to some friends who are not users of the Kubernetes cluster. Therefore, we will extend the definition of protection of the API with a second identity source, based on Authorino's **API key authentication** feature. + +Apply the CR: + +```sh +$ kubectl -n authorino apply -f docs/tutorials/showcase/showcase-api-protection-2.yaml +``` + +Create a secret holding an API key to access the API, and with labels matching the label selectors specified in the CR for the identity source 'friends': + +```sh +$ kubectl -n authorino apply -f - <' http://talker-api-authorino.127.0.0.1.nip.io:8000/hello -# curl -H 'Authorization: Bearer ' http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings -# curl -H 'Authorization: Bearer ' -x POST http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings -# curl -H 'Authorization: Bearer ' http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/1 -# curl -H 'Authorization: Bearer ' -X PUT http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/1 -# curl -H 'Authorization: Bearer ' -X DELETE http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/1 -# curl -H 'Authorization: Bearer ' http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/2 -# curl -H 'Authorization: Bearer ' -X PUT http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/2 -# curl -H 'Authorization: Bearer ' -X DELETE http://talker-api-authorino.127.0.0.1.nip.io:8000/greetings/2 -# curl -H 'Authorization: Bearer ' http://talker-api-authorino.127.0.0.1.nip.io:8000/goodbye -# -# 8) Consume the API from the web application [Use-case #8] -# - Users of this identity group ("legacy-iam") have full access to the API -# -# Access the webapp in the browser at http://talker-api-authorino.127.0.0.1.nip.io:8000/web -# -# Authenticate with the following credentials whenever requested (Dex login page): -# username: marta@localhost -# password: password - -apiVersion: config.authorino.3scale.net/v1beta1 -kind: Service -metadata: - name: talker-api-protection -spec: - hosts: - - talker-api-authorino.127.0.0.1.nip.io:8000 - identity: - # use case #1 - - name: same-k8s-server-users - kubernetes: - audiences: - - talker - - # use case #2 - - name: friends - apiKey: - labelSelectors: - authorino.3scale.net/managed-by: authorino - custom-label: friends - - # use case #4 - - name: beta-testers - apiKey: - labelSelectors: - authorino.3scale.net/managed-by: authorino - group: beta-testers - - # use case #5 - - name: keycloak-users - oidc: - endpoint: http://keycloak:8080/auth/realms/kuadrant - - # use case #8 - - name: legacy-iam - oidc: - endpoint: http://dex:5556 - credentials: - in: cookie - keySelector: ACCESS-TOKEN - - metadata: - # use case #7 - - name: resource-data - uma: - endpoint: http://keycloak:8080/auth/realms/kuadrant - credentialsRef: - name: uma-credentials-secret - - authorization: - # use case #3 - - name: friends-cannot-delete - json: - conditions: - - selector: auth.identity.metadata.labels.custom-label - operator: eq - value: friends - rules: - - selector: context.request.http.method - operator: neq - value: DELETE - - # use case #4 - - name: short-lived-api-keys-for-beta-testers - opa: - inlineRego: | - identityMetadata = object.get(input.auth.identity, "metadata", {}) - group = object.get(object.get(identityMetadata, "labels", {}), "group", "") - - allow { - creationTimestampStr := identityMetadata.creationTimestamp - creationTimestamp := time.parse_rfc3339_ns(creationTimestampStr) - durationNs := time.now_ns() - creationTimestamp - durationDays := (durationNs/1000000000)/86400 - - durationDays <= 5 - group == "beta-testers" - } - - allow { - group != "beta-testers" - } - - # use case #6 - - name: only-admins-say-hello - json: - conditions: - - selector: auth.identity.iss - operator: eq - value: http://keycloak:8080/auth/realms/kuadrant - - selector: context.request.http.path - operator: eq - value: /hello - - rules: - - selector: auth.identity.realm_access.roles - operator: incl - value: admin - - # use case #7 - - name: owned-resources - opa: - inlineRego: | - issuer = object.get(input.auth.identity, "iss", "") - http_request = input.context.request.http - request_path = split(trim_left(http_request.path, "/"), "/") - - keycloak { issuer == "http://keycloak:8080/auth/realms/kuadrant" } - not_keycloak { issuer != "http://keycloak:8080/auth/realms/kuadrant" } - - put { http_request.method == "PUT" } - not_put { http_request.method != "PUT" } - - owned_resource { some id; request_path = ["greetings", id] } - not_owned_resource { request_path = ["greetings"] } - not_owned_resource { request_path = ["hello"] } - not_owned_resource { request_path = ["goodbye"] } - - put_owned_resource { put; owned_resource } - - identity_owns_the_resource { - resource := object.get(input.auth.metadata, "resource-data", [])[0] - resource.owner.id == input.auth.identity.sub - } - - allow { not_keycloak } - allow { keycloak; not_owned_resource } - allow { keycloak; owned_resource; not_put } - allow { keycloak; put_owned_resource; identity_owns_the_resource } ---- -apiVersion: v1 -kind: Secret -metadata: - name: uma-credentials-secret -stringData: - clientID: talker-api - clientSecret: 523b92b6-625d-4e1e-a313-77e7a8ae4e88 -type: Opaque diff --git a/examples/README.md b/examples/README.md index df84cad9..79dce243 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,10 @@ # Authorino examples -## Setup the environment +The examples provided in this page show several use cases and how to implement them as Authorino custom resources. Each example presents a feature of Authorino and is independent from the other. + +For applications of Authorino into more complex combined real life-like guided examples, see Authorino [Tutorials](/docs/tutorials.md). + +## Setting up the environment setup for the examples The simplest way to try the examples in this page is by launching a local test Kubernetes environment included in the Authorino examples.