"Role-Based Access Control" (RBAC) permet de réguler les accès aux ressources basé sur des rôles utilisateurs (peut lire, peut écrire...) au sein d'une organisation.
Le RBAC est intégré par défaut dans un cluster Kubernetes. Il est activé grace à kube-apiserver
et son paramètre --authorization-mode
par défaut à AlwaysAllow
.
Dans un cluster K8S, le RBAC peut servir :
Il est important de connaitre la différence entre une ressource Namespaced
et une ressource Non Namespaced
:
Antoine@cks-master:~$ k api-resources --namespaced | wc -l
32
Antoine@cks-master:~$ k api-resources --namespaced=false | wc -l
26
Toutes les ressources namespaced
sont les ressources qui contiennent une notion de namespace (configmaps, deployments, jobs...). Les non namespaced
sont des ressources qui ne contiennent pas de notion de namespace (storageclasses, ingressclasses, clusterroles...)
Voici comment est appliqué un RBAC :
Le Role s'applique dans un namespace.
Le ClusterRole s'applique au cluster entier (non namespaced).
Le RoleBinding/ClusterRoleBinding permettent d'appliquer le Role/ClusterRole à un utilisateur, nom de service...
Exemple de Role :
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: test
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
Ici, nous créons un role qui permet de get
, watch
, list
la ressource pods
dans le namespace test
. L'utilisateur assigné à ce rôle ne pourra voir les pods que dans le namespace test
. Pour les autres namespaces, il faudra avoir un autre rôle.
Voici un exemple de ClusterRole :
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
# Pas de "namespace" car la ressource ClusterRoles est "not namespaced"
name: secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
Ici, nous créons un role qui permet de get
, watch
, list
la ressource secrets
dans tout le cluster.
Attention, si l'on crée un nouveau namespace, le ClusterRole sera aussi appliqué à ce nouveau namespace !
Nous allons maintenant appliquer le Role à l'utilisateur "joe" :
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: test
subjects:
# On peut spécifier plusieurs "subjects" (Liste en yaml)
- kind: User
name: joe # "name" est sensible à la casse
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role # Doit être Role ou ClusterRole
name: pod-reader # Doit matcher avec le nom du Role ou ClusterRole auquel on veut le lier
apiGroup: rbac.authorization.k8s.io
Toujours tester sa règle RBAC. Pour se faire il faut utiliser auth can-i
(après avoir crée le Role et le RoleBinding).
Antoine@cks-master:~$ k -n test auth can-i get pod --as joe
yes
Antoine@cks-master:~$ k -n test auth can-i get secrets --as joe
no
Un compte de service est une ressource Kubernetes. Elle est aussi utilisée par les ressources de K8S (les pods pour accéder à l'API de kube). Le compte de service est géré par l'API de Kubernetes.
Un utilisateur lui ne possède pas de ressource User
dans le cluster. Un utilisateur géré via un certificat et une clé.
Le certificat client doit être signé par le cluster. Le nom de l'utilisateur est noté dans le CN
(Common Name) du certificat.
Pour créer un certificat, nous allons devoir :
$ openssl genrsa -out joe.key 2048
$ openssl req -new -key joe.key -out joe.csr # Mettre le Common Name = joe
$ cat joe.csr | base64 -w 0 # Encode en base64 sur 1 seule ligne
Nous allons maintenant créer le Certificate Signing Requests
:
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: myuser
spec:
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTU... # Remplacer par le "cat" fait ci-dessus
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # 1 jour
usages:
- client auth
Puis appliquer la ressource. Quand on regarde la ressource csr on voit que notre certificat est en attente :
Antoine@cks-master:~$ k get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
joe 2m4s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Pending
Il faut ensuite valider le certificat :
$ k certificate approve joe
certificatesigningrequest.certificates.k8s.io/joe approved
$ k get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
joe 3m37s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Approved,Issued
On va maintenant récupérer le certificat :
Antoine@cks-master:~$ k get csr joe -o yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
...
spec:
expirationSeconds: 86400
groups:
- system:masters
- system:authenticated
request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KT...
signerName: kubernetes.io/kube-apiserver-client
usages:
- client auth
username: kubernetes-admin
status:
certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPRENDQWlD... # Copier cette ligne
conditions:
- lastTransitionTime: "2022-05-14T22:01:18Z"
lastUpdateTime: "2022-05-14T22:01:18Z"
message: This CSR was approved by kubectl certificate approve.
reason: KubectlApprove
status: "True"
type: Approved
Récupérer status.certificate puis le mettre dans un fichier après l'avoir décodé :
$ echo "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURPRENDQWlDZ0F3SUJBZ0lR..." | base64 -d > joe.crt
On va maintenant utiliser ce certificat :
$ k config view
apiVersion: v1
...
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes # On utilise actuellement l'utilisateur kubernetes-admin
...
On va ajouter un utilisateur et un contexte à notre configuration :
$ k config set-credentials joe --client-key=joe.key --client-certificate=joe.crt # On ajoute un utilisateur à la config
User "joe" set.
# On peut aussi cacher le certificat ("REDACTED") dans le get config avec le paramètre --embed-certs
$ k config set-credentials joe --client-key=joe.key --client-certificate=joe.crt --embed-certs
$ k config set-context joe --cluster=kubernetes --user=joe # On ajoute un contexte à notre config
On regarde les changements faits sur notre config :
$ k config view
# ou
$ k config view --raw # Pour voir le certificat "REDACTED"
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://10.156.0.2:6443
name: kubernetes
contexts:
- context: # Nouveau
cluster: kubernetes # Nouveau
user: joe # Nouveau
name: joe # Nouveau
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: joe # Nouveau
user: # Nouveau
client-certificate: /home/Antoine/joe.crt # Nouveau
client-key: /home/Antoine/joe.key # Nouveau
- name: kubernetes-admin
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
On va maintenant utiliser notre nouvel utilisateur joe :
$ k config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
joe kubernetes joe
* kubernetes-admin@kubernetes kubernetes kubernetes-admin
$ k config use-context joe
On essaie de get les pods :
Antoine@cks-master:~$ k get pod
Error from server (Forbidden): pods is forbidden: User "joe" cannot list resource "pods" in API group "" in the namespace "default"
Antoine@cks-master:~$ k get pod -n test
No resources found in test namespace.
Nous allons maintenant créer et utiliser un compte de service :
$ k create sa accessor
serviceaccount/accessor created
$ k get sa,secrets
NAME SECRETS AGE
serviceaccount/accessor 1 9s
serviceaccount/default 1 57d
NAME TYPE DATA AGE
secret/accessor-token-pxl6x kubernetes.io/service-account-token 3 9s
secret/default-token-6gtjb kubernetes.io/service-account-token 3 57d
Un ServiceAccount
ainsi que son Secret
ont été crée. Dans le secret nous retrouverons le token afin de s'authentifier à l'API Kubernetes.
On va maintenant lié un pod à ce ServiceAccount
:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: accessor
name: accessor
spec:
serviceAccountName: accessor # S'il n'existe pas, sortira en erreur
containers:
- image: nginx
name: accessor
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
Une fois appliquée, nous allons nous rendre dans le pod et voir le token du ServiceAccount
:
$ k exec -it accessor -- bash
root@accessor:/$ mount | grep serv
tmpfs on /run/secrets/kubernetes.io/serviceaccount type tmpfs (ro,relatime,size=3920272k)
root@accessor:/$ ls /run/secrets/kubernetes.io/serviceaccount
ca.crt namespace token
Comme dit plus haut, le secret du ServiceAccount
(qui est le fichier token
dans notre pod) va nous permettre de communiquer avec l'API de K8S :
root@accessor:~$ curl https://kubernetes -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
root@accessor:~# curl https://kubernetes -k -H "Authorization: Bearer <token>"
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:serviceaccount:default:accessor\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
En passant le token
on voit que Kubernetes arrive bien à reconnaitre le ServiceAccount
accessor, cependant nous n'avons pas les droits pour le moment.
Est-ce que mon pod a réellement besoin de discuter avec l'API Kubernetes ? La plupart du temps... NON et dans ce cas là il est préférable de ne pas monter de ServiceAccount
dans le pod.
On peut désactiver le montage auto directement depuis le ServiceAccount
ou directement dans la déclaration du Pod
:
apiVersion: v1
kind: ServiceAccount
metadata:
name: accessor
automountServiceAccountToken: false
# OU
apiVersion: v1
kind: Pod
metadata:
name: accessor
spec:
serviceAccountName: accessor
automountServiceAccountToken: false
Une fois modifié, on peut remplacer le pod afin de ne pas monter le Secret
du ServiceAccount
accessor dans le pod :
Antoine@cks-master:~$ k -f accessor.yaml replace --force
pod "accessor" deleted
pod/accessor replaced
Antoine@cks-master:~$ k exec -it accessor -- bash
root@accessor:/$ mount | grep ser
# Rien ne ressort
Par défaut, tous les pods utilisents le ServiceAccount
par défaut. Celui-ci n'a aucun droit, donc tout va bien. Cependant, si on lui donne plus de droits, tous les pods auront les droits.
Il faut donc créer un ServiceAccount
spécifique pour chaque application qui en a besoin et lui donner les bons droits ! (aucun droit par défaut)
$ k auth can-i delete secrets --as system:serviceaccount:default:accessor
no
On va maintenant assorcier le ClusterRole
edit qui existe par défaut à notre ServiceAccount
:
$ k create clusterrolebinding accessor --clusterrole edit --serviceaccount default:accessor
clusterrolebinding.rbac.authorization.k8s.io/accessor created
$ k auth can-i delete secrets --as system:serviceaccount:default:accessor
yes
Afin de sécuriser au mieux son cluster K8S, il est important de restreindre l'accès :
NodeRestriction
)Pour envoyer des requêtes à l'API nous allons devoir récupérer le CA/crt/Key :
$ k config view --raw
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: blabla==
server: https://10.156.0.2:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: blabla==
client-key-data: blabla=
$ echo <certificate-authority-data> | base64 -d > ca
$ echo <client-certificate-data> | base64 -d > crt
$ echo <certificate-authority-data> | base64 -d > key
$ curl https://10.156.0.2:6443 --cacert ca --cert crt --key key
{
"paths": [
"/.well-known/openid-configuration",
"/api",
"/api/v1",
"/apis",
"/apis/",
"/apis/admissionregistration.k8s.io",
"/apis/admissionregistration.k8s.io/v1",
"/apis/apiextensions.k8s.io",
...
Pour se faire nous allons devoir passer un paramètre du manifest kube-apiserver.yaml
à false :
$ curl https://localhost:6443 -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
# On va modifier le paramètre
$ sudo vim /etc/kubernetes/manifests/kube-apiserver.yaml
# Ajouter dans "command" :
- --anonymous-auth=false
# Puis réessayer :
$ curl https://localhost:6443 -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
Au moment ou j'écris cet article, il existe de nombreux problème à passer anonymous-auth
à false (liveness probe qui ne fonctionne plus, le noeud ne peut plus être rejoint...). Il est donc bon de connaitre ce paramètre mais actuellement problématique.
Non recommandé, l'API peut être exposé sur internet, pour se faire :
$ k edit service kubernetes
service/kubernetes edited
# Remplacer ClusterIP par NodePort
$ k get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes NodePort 10.96.0.1 <none> 443:31171/TCP 57d
L'API est maintenant exposé sur l'adresse publique, port 31171.
Pour y accéder depuis son ordinateur, copier la config :
$ k config view --raw
# Copier / Coller dans un fichier en local
# Modifier l'adresse IP et le port avec les informations de votre cluster
$ k --kubeconfig conf get pod
Unable to connect to the server: x509: certificate is valid for 10.96.0.1, 10.156.0.2, not 35.246.210.238
$ openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text
...
X509v3 Subject Alternative Name:
DNS:cks-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:10.96.0.1, IP Address:10.156.0.2
...
Le certificat est valide pour 2 IP adresses ainsi que des DNS différents. Ajoutons le DNS kubernetes
à notre fichier hosts (machine personnel) :
$ sudo vim /etc/hosts
# Ajouter
35.246.210.238 kubernetes
Modifier le fichier config pour aller vers le DNS kubernetes
et non l'IP puis réessayer :
$ k --kubeconfig conf get pod
No resources found in default namespace.
Tout est fonctionnel !
Eviter au maximum cela, remettre le service en ClusterIP
Le NodeRestriction
est au niveau de l'AdmissionControl
. Il permet de limiter la modification des labels du noeud par le kubelet
. Le kubelet ne pourra modifier que certains labels et uniquement les labels de son propre noeud.
Sur le master on vérifie que NodeRestriction
:
root@cks-master:~$ cat /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
...
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=NodeRestriction
Sur le worker :
# On va utiliser la configuration kubelet.conf comme KUBECONFIG :
root@cks-worker:~$ export KUBECONFIG=/etc/kubernetes/kubelet.conf
root@cks-worker:~$ kubectl get pod
No resources found in default namespace.
root@cks-worker:~$ kubectl label node cks-master test=yolo
Error from server (Forbidden): nodes "cks-master" is forbidden: node "cks-worker" is not allowed to modify node "cks-master"
root@cks-worker:~$ kubectl label node cks-worker test=yolo
node/cks-worker labeled
root@cks-worker:~$ kubectl label node cks-worker node-restriction.kubernetes.io/test=yolo
Error from server (Forbidden): nodes "cks-worker" is forbidden: is not allowed to modify labels: node-restriction.kubernetes.io/test
On peut donc voir que l'on peut ajouter un/plusieurs label(s) sur son propre noeud. Cependant on ne peut pas utiliser la clé node-restriction.kubernetes.io
qui est utilisable que sur le master. Cela permet donc d'avoir un label ultra sécurisé qui ne peut être remplacé par une worker.