From 59c2ff1911c1882a178fbd031df64329254d1c74 Mon Sep 17 00:00:00 2001 From: Jerome Petazzoni Date: Sat, 1 Jun 2019 17:58:15 -0500 Subject: [PATCH] Add chapter about OpenID Connect tokens Includes a simplified demo using Google OAuth Playground, as well as numerous examples aiming at piercing the veil to explain JWT, JWS, and associated protocols and algos. --- slides/k8s/openid-connect.md | 377 +++++++++++++++++++++++++++++++++++ slides/kube-fullday.yml | 1 + slides/kube-selfpaced.yml | 1 + slides/kube-twodays.yml | 1 + 4 files changed, 380 insertions(+) create mode 100644 slides/k8s/openid-connect.md diff --git a/slides/k8s/openid-connect.md b/slides/k8s/openid-connect.md new file mode 100644 index 00000000..e1f4786e --- /dev/null +++ b/slides/k8s/openid-connect.md @@ -0,0 +1,377 @@ +# OpenID Connect + +- The Kubernetes API server can perform authentication with OpenID connect + +- This requires an *OpenID provider* + + (external authorization server using the OAuth 2.0 protocol) + +- We can use a third-party provider (e.g. Google) or run our own (e.g. Dex) + +- We are going to give an overview of the protocol + +- We will show it in action (in a simplified scenario) + +--- + +## Workflow overview + +- We want to access our resources (a Kubernetes cluster) + +- We authenticate with the OpenID provider + + - we can do this directly (e.g. by going to https://accounts.google.com) + + - or maybe a kubectl plugin can open a browser page on our behalf + +- After authenticating us, the OpenID provider gives us: + + - an *id token* (a short-lived signed JSON Web Token, see next slide) + + - a *refresh token* (to renew the previous one when needed) + +- We can now issue requests to the Kubernetes API with the *id token* + +- The API server will verify that token's content to authenticate us + +--- + +## JSON Web Tokens + +- A JSON Web Token (JWT) has three parts: + + - a header specifying algorithms and token type + + - a payload (indicating who issued the token, for whom, which purposes...) + + - a signature generated by the issuer (the issuer = the OpenID provider) + +- Anyone can verify a JWT without contacting the issuer + + (except to obtain the issuer's public key) + +- Pro tip: we can inspect a JWT with https://jwt.io/ + +--- + +## How the Kubernetes API uses JWT + +- Server side + + - enable OIDC authentication + + - indicate which issuer (provider) should be allowed + + - indicate which audience (or "client id") should be allowed + + - if necessary, map or prefix user and group names + +- Client side + + - obtain JWT as described earlier + + - pass JWT as authentication token + + - renew JWT when needed (using the refresh token) + +--- + +## Demo time! + +- We will use [Google Accounts](https://accounts.google.com) as our OpenID provider + +- We will use the [Google OAuth Playground](https://developers.google.com/oauthplayground) as the "audience" or "client id" + +- We will obtain a JWT through Google Accounts and the OAuth Playground + +- We will enable OIDC in the Kubernetes API server + +- We will use the JWT to authenticate + +.footnote[If you can't or won't use a Google Account, you can try to adapt to another provider.] + +--- + +## Checking the API server logs + +- The API server logs will be particularly useful in this section + + (they will indicate e.g. why a specific token is rejected) + +- Let's keep an eye on the API server output! + +.exercise[ + +- Tail the logs of the API server: + ```bash + kubectl logs kube-apiserver-node1 --follow --namespace=kube-system + ``` + +] + +--- + +## Authenticate with the OpenID provider + +- We will use the Google OAuth Playground for convenience + +- In a real scenario, we would need our own OAuth client instead of the playground + + (even if we were still using Google as the OpenID provider) + +.exercise[ + +- Open the Google OAuth Playground: + ``` + https://developers.google.com/oauthplayground/ + ``` + +- Enter our own custom scope in the text field: + ``` + https://www.googleapis.com/auth/userinfo.email + ``` + +- Click on "Authorize APIs" and allow the playground to access our email address + +] + +--- + +## Obtain our JSON Web Token + +- The previous step gave us an "authorization code" + +- We will use it to obtain tokens + +.exercise[ + +- Click on "Exchange authorization code for tokens" + +] + +- The JWT is the very long `id_token` that shows up on the right hand side + + (it is a base64-encoded JSON object, and should therefore start with `eyJ`) + +--- + +## Using our JSON Web Token + +- We need to create a context (in kubeconfig) for our token + + (if we just add the token or use `kubectl --token`, our certificate will still be used) + +.exercise[ + +- Create a new authentication section in kubeconfig: + ```bash + kubectl config set-credentials myjwt --token=eyJ... + ``` + +- Try to use it: + ```bash + kubectl --user=myjwt get nodes + ``` + +] + +We should get an `Unauthorized` response, since we haven't enabled OpenID Connect in the API server yet. We should also see `invalid bearer token` in the API server log output. + +--- + +## Enabling OpenID Connect + +- We need to add a few flags to the API server configuration + +- These two are mandatory: + + `--oidc-issuer-url` → URL of the OpenID provider + + `--oidc-client-id` → app requesting the authentication +
(in our case, that's the ID for the Google OAuth Playground) + +- This one is optional: + + `--oidc-username-claim` → which field should be used as user name +
(we will use the user's email address instead of an opaque ID) + +- See the [API server documentation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuring-the-api-server +) for more details about all available flags + +--- + +## Updating the API server configuration + +- The instructions below will work for clusters deployed with kubeadm + + (or where the control plane is deployed in static pods) + +- If your cluster is different, you will need to adapt them + +.exercise[ + +- Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` + +- Add the following lines to the list of command-line flags: + ```yaml + - --oidc-issuer-url=https://accounts.google.com + - --oidc-client-id=407408718192.apps.googleusercontent.com + - --oidc-username-claim=email + ``` +] + +--- + +## Restarting the API server + +- The kubelet monitors the files in `/etc/kubernetes/manifests` + +- When we save the pod manifest, kubelet will restart the corresponding pod + + (using the updated command line flags) + +.exercise[ + +- After making the changes described on the previous slide, save the file + +- Issue a simple command (like `kubectl version`) until the API server is back up + + (it might take between a few seconds and one minute for the API server to restart) + +- Restart the `kubectl logs` command to view the logs of the API server + +] + +--- + +## Using our JSON Web Token + +- Now that the API server is set up to recognize our token, try again! + +.exercise[ + +- Try an API command with our token: + ```bash + kubectl --user=myjwt get nodes + kubectl --user=myjwt get pods + ``` + +] + +We should see a message like: +``` +Error from server (Forbidden): nodes is forbidden: User "jean.doe@gmail.com" +cannot list resource "nodes" in API group "" at the cluster scope +``` + +→ We were successfully *authenticated*, but not *authorized*. + +--- + +## Authorizing our user + +- As an extra step, let's grant read access to our user + +- We will use the pre-defined ClusterRole `view` + +.exercise[ + +- Create a ClusterRoleBinding allowing us read access to the cluster: + ```bash + kubectl create clusterrolebinding i-can-view \ + --user=`jean.doe@gmail.com` --clusterrole=view + ``` + +- Confirm that we can now list pods with our token: + ```bash + kubectl --user=myjwt get pods + ``` + +] + +--- + +## From demo to production + +.warning[This was a very simplified demo! In a real deployment...] + +- We wouldn't use the Google OAuth Playground + +- We *probably* wouldn't even use Google at all + + (it doesn't seem to provide a way to include groups!) + +- Some popular alternatives: + + - [Dex](https://github.com/dexidp/dex), + [Keycloak](https://www.keycloak.org/) + (self-hosted) + + - [Okta](https://developer.okta.com/docs/how-to/creating-token-with-groups-claim/#step-five-decode-the-jwt-to-verify) + (SaaS) + +- We would use a helper (like the [kubelogin](https://github.com/int128/kubelogin) plugin) to automatically obtain tokens + +--- + +class: extra-details + +## Service Account tokens + +- The tokens used by Service Accounts are JWT tokens as well + +- They are signed and verified using a special service account key pair + +.exercise[ + +- Extract the token of a service account in the current namespace: + ```bash + kubectl get secrets -o jsonpath={..token} | base64 -d + ``` + +- Copy-paste the token to a verification service like https://jwt.io + +- Notice that it says "Invalid Signature" + +] + +--- + +class: extra-details + +## Verifying Service Account tokens + +- JSON Web Tokens embed the URL of the "issuer" (=OpenID provider) + +- The issuer provides its public key through a well-known discovery endpoint + + (similar to https://accounts.google.com/.well-known/openid-configuration) + +- There is no such endpoint for the Service Account key pair + +- But we can provide the public key ourselves for verification + +--- + +class: extra-details + +## Verifying a Service Account token + +- On clusters provisioned with kubeadm, the Service Account key pair is: + + `/etc/kubernetes/pki/sa.key` (used by the controller manager to generate tokens) + + `/etc/kubernetes/pki/sa.pub` (used by the API server to validate the same tokens) + +.exercise[ + +- Display the public key used to sign Service Account tokens: + ```bash + sudo cat /etc/kubernetes/pki/sa.pub + ``` + +- Copy-paste the key in the "verify signature" area on https://jwt.io + +- It should now say "Signature Verified" + +] diff --git a/slides/kube-fullday.yml b/slides/kube-fullday.yml index f5462d85..1ce65170 100644 --- a/slides/kube-fullday.yml +++ b/slides/kube-fullday.yml @@ -58,6 +58,7 @@ chapters: #- k8s/netpol.md #- k8s/authn-authz.md #- k8s/csr-api.md + #- k8s/openid-connect.md #- k8s/podsecuritypolicy.md #- k8s/ingress.md #- k8s/gitworkflows.md diff --git a/slides/kube-selfpaced.yml b/slides/kube-selfpaced.yml index 59eb8377..25cc5fbc 100644 --- a/slides/kube-selfpaced.yml +++ b/slides/kube-selfpaced.yml @@ -58,6 +58,7 @@ chapters: - - k8s/netpol.md - k8s/authn-authz.md - k8s/csr-api.md + - k8s/openid-connect.md - k8s/podsecuritypolicy.md - - k8s/ingress.md - k8s/gitworkflows.md diff --git a/slides/kube-twodays.yml b/slides/kube-twodays.yml index e338b627..243da6b5 100644 --- a/slides/kube-twodays.yml +++ b/slides/kube-twodays.yml @@ -58,6 +58,7 @@ chapters: #- k8s/netpol.md - k8s/authn-authz.md - k8s/csr-api.md + - k8s/openid-connect.md - k8s/podsecuritypolicy.md - - k8s/ingress.md #- k8s/gitworkflows.md