Het is al weer even terug dat ik zelf serviceaccounts aanmaakte. Destijds ging het op basis van secrets. Dit is inmiddels deprecated en terecht! Het was plat, simpel maar ook redelijk statisch en stiekem wat onveilig. Tegenwoordig is het best practice om moderne web authenticatie mechanismes te gebruiken en daar zitten wat voordelen aan. In dit artikel neem ik je mee bij weer een mooi voorbeeld van hoe Kubernetes de afgelopen jaren flinke stappen heeft gezet qua volwassenheid op dit vlak.

wat zijn serviceaccounts ook al weer?

Service accounts zijn accounts waarmee systemen en processen die in het Kubernetes cluster actief zijn gebruik kunnen maken van de Kubernetes API. Denk daarbij bijvoorbeeld aan Cert-Manager. Cert-manager is software die TLS certificaten management kan beheren. Als ik een nieuw certificaat wil, kan ik op basis van CRD’s tegen de Kubernetes API zeggen wat Cert-Manager moet configureren. Die Cert-manager gebruikt een service account om te praten met de Kubernetes API om te zien wat voor taken er uitgevoerd moeten worden. Een ander voorbeeld waarbij een service account gebruikt wordt, is een operator operator die via de Kubernetes API kan zien dat er iets geconfigureerd moet worden. Denk bijvoorbeeld aan de Cilium BGP configuratie die eerder op deze site in een artikel besproken is.

Deze service accounts worden voorzien van roles/clusterroles en RoleBindings/ClusterRoleBindings. Op die manier kan je op basis van role based access control (RBAC) alleen de Kubernetes API rechten toekennen die nodig zijn. Wel zo veilig. Een voorbeeld van de relatie tussen een serviceaccount, de role en de rolebinding zie je hieronder:

ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    kubernetes.io/enforce-mountable-secrets: "true"
  name: geurt
  namespace: satest

Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: satest
  name: pod-reader
rules:
- apiGroups: [""] # "" indicates the core API group
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

Rolebinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: satest
subjects:
- kind: ServiceAccount
  name: geurt
  namespace: satest
roleRef:
  kind: Role #this must be Role or ClusterRole
  name: pod-reader # this must match the name of the Role or ClusterRole you wish to bind to
  apiGroup: rbac.authorization.k8s.io

Wat je hierboven ziet is een definitie van een ServiceAccount die de rechten heeft om te kijken welke pods er zijn in de namespace “satest”. Deze rechten zijn in een Role vormgegeven. De RoleBinding zorgt er voor dat de dat het ServiceAccount en de rechten binnen de Role aan elkaar gekoppeld worden.

Hoe ging het “vroeger”

In de wereld van Kubernetes voelt het woord “vroeger” nog altijd een beetje vreemd sinds het nog een relatief jonge tak van sport is. Toch is er in de afgelopen jaren veel veranderd. Vroeger werd de daadwerkelijke toegang geregeld door een secret aan te maken in de namespace en die aan het ServiceAccount te koppelen. Dit was makkelijk maar ook statisch en het periodiek roteren van de secrets was een dingetje.

Hoe gaat het nu?

tegenwoordig is het best practice om voor service accounts geen statische secrets meer te gebruiken. Nu, wordt er gebruik gemaakt van JSON Web tokens. In het voorbeeld hierboven van ServiceAccount geurt, genereer ik een token:

kubectl -n satest create token geurt

Het resultaat is een base64 encoded brei aan karakters die in het beeld verschijnt. Sites als token.dev maken het mogelijk snel om te zien wat er allemaal schuil gaat achter die karakters. Besef je wel dat er ook private informatie in staat. Voor dit voorbeeld maakt dat niet uit:

JSON webtokens werken echt anders en voor de korte geldigheid is ook wat bedacht. Software die in Kubernetes draait en via een serviceaccount informatie met de Kubernetes API uitwisselt, kan automatisch via de Kubelet een JWT token, Kubernetes-API adres en bijbehorende ca.crt gemount krijgen in de pod. Dat is zogezegd een flinke verbetering.

Hoe zat het ook al weer met X509 certificaten en toegang voor personen?

X509 certificaten en Public Key Infrastructuren zijn veilig! maar het gedrag wat mensen er bij vertonen is dat vaak niet. Om die reden is het een prima oplossing ombijvoorbeeld de communicatie tussen de Kubelet met de Kubernetes API te beveiligen maar is het een slecht idee om het in je kubeconfig te gebruiken. Ik heb in het verleden meer dan eens meegemaakt dat die kubectl configfiles eventjes per mail doorgestuurd werden naar de gebruiker. Zo private is je private key dan niet meer natuurlijk. tegelijkertijd is voor veel mensen technisch te complex om zelf een private key, met Certificate signing request (CSR) met de juiste inhoud te maken. Daar komt bij dat het omslachtig is om zo’n CSR vervolgens te ondertekenen en het gegenereerde certificaat weer naar de klant toe te sturen. De kers op de spreekwoordelijke taart is dat deze klant uiteindelijk een werkende kubeconfig moet bouwen. X509 heeft nog altijd zijn plek maar dit is hem niet! Wat wel goed werkt is dat Kubernetes zo geconfigureerd wordt dat OIDC (OpenID Connect) ingezet wordt. De persoon die toegang wil tot de Kubernetes API krijgt dan net als hierboven beschreven een JWT token waaruit Kubernetes kan herleiden hoe lang bepaalde taken uitgevoerd mogen worden op de Kubernetes API.

Waar Service Accounts vooral gebruikt worden door software in pods, en dus de Kubelet deze JWT tokens kan plaatsen en verversen, ben je als persoon die toegang wil aangewezen op third party OIDC software. zoals Keycloak of Authentik.

Even kijken hoe het werkt?

Voor zover ik het kan inschatten ben jij geen Kubelet. Toch kan het handig zijn om even te experimenteren met die tokens voordat je iets bouwt wat in een pod draait en de tokens automatisch voorgeschoteld krijgt door de Kubelet.

Zie daarom het onderstaande voorbeeld dat verder gaat op het het voorbeeld eerder in dit verhaal waarin een ServiceAccount, een Role en een RoleBinding gemaakt wordt.

kubectl --kubeconfig geurt config set-cluster homelab --certificate-authority=/root/satest/ca.crt --server=https://192.168.88.21:6443
token=$(kubectl -n satest create token geurt)
kubectl --kubeconfig geurt config set-credentials geurt --token=$token
kubectl --kubeconfig geurt config set-context geurt --cluster=homelab --user=geurt --namespace=satest
kubectl --kubeconfig geurt config use-context geurt

Wat zien we in de bovenstaande commando’s:

  • Er wordt een kubectl configfile aangemaakt met de naam “geurt”
  • de ca.crt van de Kubernetes API en het Kubernetes API endpoint wordt in de configuratie toegevoegd.
  • Een JWT token wordt aangemaakt en toegevoegd aan de kubectl config.
  • Als context wordt het juiste Kubernetes cluster “homelab” en de juiste namespace “geurt” in de kubectl ingesteld.

Wat we zien in het bovenstaande screenshot:

  • User “system:serviceaccount:satest:geurt” is het gebruikte serviceaccount in de namespace “satest”
  • –kubeconfig geurt geeft de kubectl configfile aan die in dit voorbeeld gebruikt wordt.
  • Buiten de satest namespace zijn er geen rechten. Get pods -A geeft dus forbidden.
  • get pods -n satest werkt wel. De lijst met pods is alleen leeg omdat er geen pods actief zijn in de satest namespace.

Tot slot

Zelf “spelen” met JWT tokens is leuk maar Je ziet dat het zonder OIDC eigenlijk alleen goed tot zijn recht komt bij systemen waar de tokens automatisch ververst worden. De Kubelet doet daarin goed werk voor de serviceaccounts die in Pods gebruikt worden. Als menselijke gebruiker is het leuk om even te zien hoe het werkt maar het is niet werkbaar om dit elke keer te moeten doen (als je wat anders te doen hebt).

De overstap naar JWT tokens is wederom een mooi voorbeeld van hoe Kubernetes de afgelopen jaren flinke stappen heeft gezet qua volwassenheid. Als je continu met het “nu” bezig bent vergeet je die vooruitgang af en toe. Nu ik weer studeer voor mijn Kubernetes certificeringen, zie je ook hier de grote verschillen met hoe het ooit was.

Over de auteur

Categorieën: serviceaccountstokens

0 reacties

Geef een reactie

Avatar plaatshouder

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *