Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/dvob/k8s-s2s-auth

Kubernetes Service-to-Service Authentication using Service Accounts
https://github.com/dvob/k8s-s2s-auth

authentication golang jwt keycloak kubernetes oidc pod service-to-service serviceaccount tokenreview vault

Last synced: about 2 months ago
JSON representation

Kubernetes Service-to-Service Authentication using Service Accounts

Awesome Lists containing this project

README

        

# Kubernetes Service Accounts
Service accounts are well known in Kubernetes to access the Kubernets API from within the cluster. This is often used for infrastructure components like operators and controllers. But we can also use service accounts to implement authentication in our own applications.

This README tries to give an overview on how service accounts work and and shows a couple of variants how you can use them for authentication. Further this repository contains an example Go service which shows how to implement the authentication in an application.

If you have questions, feedback or if you want to share your expirience using these features feel free to start a [discussion](https://github.com/dvob/k8s-s2s-auth/discussions).

* [Tutorial](#tutorial)
* [Scenario](#scenario)
* [Setup Cluster](#setup-cluster)
* [minikube](#minikube)
* [Service 1 (Client)](#service-1-client)
* [Service 2 (Server)](#service-2-server)
* [TokenReview](#tokenreview)
* [JWT](#jwt)
* [Disadvantages](#disadvantages)
* [TokenRequestProjection](#tokenrequestprojection)
* [ServiceAccountIssuerDiscovery](#serviceaccountissuerdiscovery)
* [Appendix](#appendix)
* [Links](#links)
* [Mentioned Kubernetes Features](#mentioned-kubernetes-features)

# Tutorial
## Scenario
In our tutorial we look at a simple scenario with to services:
* Service 1 (Client)
* Service 2 (Server)

Service1 wants to call Service2. Service2 shall only respond to authenticated requests.

## Setup Cluster
Setup a test cluster with kind, minikube or another tool of your choice. At a later point we want to explore the TokenRequestProjection feature, thats why we have set the following options on the API server:
* `--service-account-issuer=yourIssuer`
* `--service-account-signing-key-file=pathToServiceAccountKey`

For this tutorial I used Kubernetes 1.20.2 and an earlier version also ran on 1.19.4 but for this you have to set additional options (see in Git history).

### minikube
```
minikube start --kubernetes-version v1.20.2 \
--extra-config=apiserver.service-account-issuer=https://kubernetes.default.svc \
--extra-config apiserver.service-account-signing-key-file=/var/lib/minikube/certs/sa.key
```

## Service 1 (Client)
First we want to deploy Service 1 and see how Service 1 can get a service account token which it later needs to authenticate itself to Service 2:
```
kubectl create ns mytest

# set context to the new namespace
kubectl config set-context $( kubectl config current-context ) --namespace mytest
```

If we create a new namespace the service account controller creates the default service account for us. Further the token controller creates a secret which contains the service account token.
We verify that the default service account and its token got created and extract the token from the secret:
```
kubectl get serviceaccount default

token_name=$( kubectl get serviceaccounts default --template '{{ ( index .secrets 0 ).name }}' )

kubectl get secret $token_name

token=$( kubectl get secret $token_name --template '{{ .data.token }}' | base64 -d )
echo $token
```

The service account token is a JWT so the claims in the payload can be inspected as follows:
```
echo $token | cut -d . -f 2 | base64 -d | jq
```
```
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "mytest
"kubernetes.io/serviceaccount/secret.name": "default-token-bwsb6",
"kubernetes.io/serviceaccount/service-account.name": "default",
"kubernetes.io/serviceaccount/service-account.uid": "9c2dc042-3543-4cc6-a458-542c0f56a324",
"sub": "system:serviceaccount:mytest:default"
}
```

If we create a pod without specifiyng anything concering servcie accounts the service account admission controller sets certain defaults for us:
* sets the service account itself
* adds a volume with the token to to the pod
* add volume mount for each container which makes the token available under `/var/run/secrets/kubernetes.io/serviceaccount/token`

Lets verify this by creating a pod which prints out the token:
```
kubectl create -f - <...NiIsImtpZKRavXR4H3UQ
User-Agent: Go-http-client/1.1
```

Service 2 now shall authenticate the requests so it somehow has to validate the token from the HTTP header. There are multiple ways to do this:
* Token Review API
* Verify Signature of the token (JWT)

### TokenReview
The API server has the public key to verify the tokens. External parties can send tokens to the token review API to verify if they are valid. That's what we try out next. For this we send a `TokenReview` with a token (we use `$token` which we've extracted before) to the API server. Since this action is not available as part of `kubectl` we use curl.

For that we first have to extract the pathes to the credentials from the kube config:
```
URL=$( kubectl config view --raw -o json | jq -r '.clusters[] | select( .name == "minikube") | .cluster.server' )
CA=$( kubectl config view --raw -o json | jq -r '.clusters[] | select( .name == "minikube") | .cluster["certificate-authority"]' )
CERT=$( kubectl config view --raw -o json | jq -r '.users[] | select( .name == "minikube") | .user["client-certificate"]' )
KEY=$( kubectl config view --raw -o json | jq -r '.users[] | select( .name == "minikube") | .user["client-key"]' )
```

Then we can send the token (`$token`) as part of the `TokenReview` to the API server:
```
curl --cacert "$CA" --cert "$CERT" --key "$KEY" --request POST --header "Content-Type: application/json" "$URL/apis/authentication.k8s.io/v1/tokenreviews" --data @- < sa.pub
```

Verify the signature of the service account token we have extracted before:
```
openssl dgst -sha256 -verify sa.pub -signature <( echo -ne $token | awk -F. '{ printf("%s", $3) }' | tr '\-_' '+/' | base64 -d ) <( echo -ne $token | awk -F. '{ printf("%s.%s", $1, $2) }' )
```
The decoding of the signature shows an error `base64: invalid input` because in the JWT signature the padding (`=`) is removed and `base64` does not like that. Nevertheless the verification should still work and show `Verfied OK`. We also have to convert the signature which is base64url encoded signature to a base64 so that base64 can decode it.

Now we can start Service 2 with the `--mode` option set to `jwt-pubkey` to see the authentication with the public key in action. We do this just locally that we don't have to create a configmap for `sa.pub`.
```
docker run -it -p 8080:8080 --rm -v $(pwd)/sa.pub:/sa.pub dvob/k8s-s2s-auth server --mode jwt-pubkey --pub-key /sa.pub
```

Test it with curl:
```
curl -H "Authorization: Bearer $token" http://localhost:8080
```

### Disadvantages
We have now seen how we can use service accounts to implement authentication. But the shown methods have some drawbacks:

TokenReview:
* Needs additional request to the API server
* The application has to talk to the Kubernetes API
* Token never expires unless you delete/recreate the service account

JWT:
* Tokens never expire (no `exp` field in JWT)
* Tokens have no audience (no `aud` field in JWT)
* Copying the service account public key (`sa.pub`) from the API server to all services which have to do authentication is not optimal

### TokenRequestProjection
The TokenRequestProjection feature enables the injection of service account tokens into a Pod through a projected volume.

In contrast to the service account tokens we've used before, these tokens have the following benefits:
* Tokens expire (`exp` claim in JWT is set)
* Tokens are bound to an audience (`aud` claim in JWT)
* Tokens are never stored as secret but directly injeted into the pod from `kubelet`
* Tokens are bound to a pod. When the pod gets deleted the token review API no longer treats the token as valid

Create a pod with a projected service account volume:
```
kubectl create -f - <