Les politiques de réseau (NetworkPolicies) sont en quelques sortes le firewall de Kubernetes. Elles sont implémentées par le plugin réseau CNI (Calico, Weave...). Les règles sont placées au niveau d'un namespace et permettent de restreindre l'entrée et/ou la sortie pour un groupe de pods basé sur certaines règles et conditions.
Par défaut, il n'y a aucune restriction, les pods ne sont pas isolés (au niveau du réseau).
On peut donc par exemple autoriser les flux sortants du pod1 à aller vers le pod2. Mais refuser les flux entrant sur le pod1. Pod2 ne pourra donc pas communiquer avec pod1 mais pod1 pourra communiquer avec pod2.
On peut aussi par exemple autoriser les flux sortant du pod1 allant vers le namespace ns1
.
kind: NetworkPolicy
metadata:
name: example
namespace: default
spec:
podSelector:
matchLabels:
id: frontend # Appliqué au pods ayant le label id=frontend
policyTypes:
- Egress # Seulement le traffic sortant
egress:
- to:
- namespaceSelector:
matchLabels:
id: ns1
ports:
- protocol: TCP
port: 80 # Vers le namespace avec le label id=ns1 ET le port 80 uniquement
- to: # Ou
- podSelector:
matchLabels:
id: backend # Vers les pods avec le label id=backend dans le même namespace
Nous allons créer 2 pods nginx et les exposer entre eux :
Antoine@cks-master:~$ k run frontend --image=nginx
pod/frontend created
Antoine@cks-master:~$ k run backend --image=nginx
pod/backend created
Antoine@cks-master:~$ k expose pod {frontend,backend} --port 80
service/frontend exposed
service/backend exposed
Testons la connectivité :
Antoine@cks-master:~$ k exec frontend -- curl backend
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 615 100 615 0 0 150k 0 --:--:-- --:--:-- --:--:-- 150k
...
<h1>Welcome to nginx!</h1>
...
Identique depuis backend
.
Nous allons maintenant créer une NetworkPolicy pour refuser tous les flux par défaut :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: default
spec:
podSelector: {} # Sélectionne TOUS les pods
policyTypes: # Deny sur l'entrée ET la sortie
- Ingress
- Egress
Après application :
Antoine@cks-master:~$ k exec frontend -- curl backend
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- 0:00:19 --:--:-- 0
curl: (6) Could not resolve host: backend command terminated with exit code 6
De même pour backend.
Nous allons devoir faire 2 règles :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-egress
namespace: default
spec:
podSelector:
matchLabels:
run: frontend # Label par défaut avec un "k run frontend..."
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
run: backend
ports:
- protocol: TCP
port: 80
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-ingress
namespace: default
spec:
podSelector:
matchLabels:
run: backend # Label par défaut avec un "k run frontend..."
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
run: frontend
ports:
- protocol: TCP
port: 80
Mais suite à ces 2 règles, tout n'est pas parfait :
Antoine@cks-master:~$ k exec frontend -- curl backend
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0^C
La résolution DNS est bloquée. Il faut donc l'IP du pod :
Antoine@cks-master:~$ k get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
backend 1/1 Running 0 19m 10.44.0.2 cks-worker <none> <none>
frontend 1/1 Running 0 17m 10.44.0.1 cks-worker <none> <none>
Antoine@cks-master:~$ k exec frontend -- curl 10.44.0.2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 615 100 615 0 0 600k 0 --:--:-- --:--:-- --:--:-- 600k
...
<title>Welcome to nginx!</title>
Vous allez me dire "Oui mais c'est pas pratique du tout !!". Et bien c'est vrai. Donc on va refuser tout SAUF la résolution DNS sortante :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: default
spec:
podSelector: {} # Sélectionne TOUS les pods
policyTypes: # Deny sur l'entrée ET la sortie
- Ingress
- Egress
egress: # On autorise uniquement le port 53 en sortie
- to:
ports:
- port: 53
protocol: TCP
- port: 53
protocol: UDP
Maintenant ça fonctionne mieux :
Antoine@cks-master:~$ k exec frontend -- curl backend
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 615 100 615 0 0 200k 0 --:--:-- --:--:-- --:--:-- 200k
...
<title>Welcome to nginx!</title>
On va maintenant vérifier que le connexion backend ➔ frontend échoue :
Antoine@cks-master:~$ k exec backend -- curl frontend
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- 0:00:03 --:--:-- 0
Tout est bon
Nous allons lancer le dashboard kubernetes que nous pouvons retrouver sur le github suivant : https://github.com/kubernetes/dashboard
Antoine@cks-master:~$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.5.1/aio/deploy/recommended.yaml
namespace/kubernetes-dashboard created
serviceaccount/kubernetes-dashboard created
service/kubernetes-dashboard created
secret/kubernetes-dashboard-certs created
secret/kubernetes-dashboard-csrf created
secret/kubernetes-dashboard-key-holder created
configmap/kubernetes-dashboard-settings created
role.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrole.rbac.authorization.k8s.io/kubernetes-dashboard created
rolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
clusterrolebinding.rbac.authorization.k8s.io/kubernetes-dashboard created
deployment.apps/kubernetes-dashboard created
service/dashboard-metrics-scraper created
deployment.apps/dashboard-metrics-scraper created
Dans un premier temps le dashboard n'est pas exposé :
Antoine@cks-master:~$ k get pod,svc -n kubernetes-dashboard
NAME READY STATUS RESTARTS AGE
pod/dashboard-metrics-scraper-799d786dbf-w2kz7 1/1 Running 1 (6d21h ago) 6d21h
pod/kubernetes-dashboard-fb8648fd9-sk8p2 1/1 Running 1 (6d21h ago) 6d21h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/dashboard-metrics-scraper ClusterIP 10.96.45.30 <none> 8000/TCP 6d21h
service/kubernetes-dashboard ClusterIP 10.100.239.116 <none> 443/TCP 6d21h
Nous allons éditer le deployment :
k edit deploy -n kubernetes-dashboard kubernetes-dashboard
Nous allons chercher comment exposer le dashboard via la doc du Github.
- args:
- --auto-generate-certificates # Enlever
- --namespace=kubernetes-dashboard
- --insecure-port=9090 # Ajouter
image: kubernetesui/dashboard:v2.5.1
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
httpGet:
path: /
port: 9090 # Modifier le port
scheme: HTTP # Modifier le type (HTTPS de base)
On va maintenant exposer le dashboard via le service.
k edit svc -n kubernetes-dashboard kubernetes-dashboard
Puis remplacer ClusterIP
par NodePort
et modifier le port/targetPort par 9090
.
On peut maintenant accéder au dashboard. Cependant celui-ci est vide car nous n'avons pas les droits !
Nous voyons dans les notifications que le ServiceAccount par défaut a très peu de droits.
Antoine@cks-master:~$ k get -n kubernetes-dashboard sa
NAME SECRETS AGE
default 1 6d21h
kubernetes-dashboard 1 6d21h
Antoine@cks-master:~$ k get clusterroles | grep view
system:aggregate-to-view 2022-04-23T14:53:16Z
system:public-info-viewer 2022-04-23T14:53:16Z
view 2022-04-23T14:53:16Z
Nous avons bien un ServiceAccount pour le dashboard. En regardant les clusterroles
nous pouvons voir un rôle view
. Si nous regardons en détail ce clusterrole
:
Antoine@cks-master:~$ k describe clusterroles view
Name: view
Labels: kubernetes.io/bootstrapping=rbac-defaults
rbac.authorization.k8s.io/aggregate-to-edit=true
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
bindings [] [] [get list watch]
configmaps [] [] [get list watch]
endpoints [] [] [get list watch]
...
Ce rôle sert donc uniquement pour la lecture (comme son nom l'indique).
On va donc lier notre ServiceAccount
à ce ClusterRole
:
$ k create -n kubernetes-dashboard rolebinding insecure --serviceaccount kubernetes-dashboard:kubernetes-dashboard --clusterrole view -o yaml --dry-run=client
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
creationTimestamp: null
name: insecure
namespace: kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kubernetes-dashboard
On peut donc maintenant voir toutes les resources du namespace kubernetes-dashboard
.
Nous pouvons donner encore plus de droits en donnant accès au cluster :
k create -n kubernetes-dashboard clusterrolebinding insecure --serviceaccount kubernetes-dashboard:kubernetes-dashboard --clusterrole view
Il est intéressant pour le CKS d'avoir connaissance des arguements : https://github.com/kubernetes/dashboard/blob/master/docs/common/dashboard-arguments.md
Nous utiliserons l'Ingress Nginx.
L'objectif est de rediriger /service1
vers le pod1
et /service2
vers le pod2
.
Dans un premier temps, installons l'ingress nginx :
Antoine@cks-master:~$ kubectl apply -f https://raw.githubusercontent.com/killer-sh/cks-course-environment/master/course-content/cluster-setup/secure-ingress/nginx-ingress-controller.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
...
job.batch/ingress-nginx-admission-patch created
Nous allons maintenant configurer /service1
et /service2
:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /service1
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
- path: /service2
pathType: Prefix
backend:
service:
name: service2
port:
number: 80
On vérifie l'ingress :
Antoine@cks-master:~$ k get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
secure-ingress nginx * 10.156.0.3 80 47s
Puis créer 2 potes avec leur service :
Antoine@cks-master:~$ k run pod1 --image=nginx
pod/pod1 created
Antoine@cks-master:~$ k run pod2 --image=httpd
pod/pod2 created
Antoine@cks-master:~$ k expose pod pod1 --port 80 --name service1
service/service1 exposed
Antoine@cks-master:~$ k expose pod pod2 --port 80 --name service2
service/service2 exposed
On accède donc à nginx via /service1
et httpd /service2
.
Génération du certificat et de la clé :
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Puis créer le secret associé à la clé / au certificat :
Antoine@cks-master:~$ kubectl create secret tls secure-ingress --cert=cert.pem --key=key.pem -oyaml --dry-run=client
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZuVENDQTRXZ0F3SUJBZ0lVWFp5eUVlMnQ3cHhPZVdKOUx...
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2d...
kind: Secret
metadata:
creationTimestamp: null
name: secure-ingress
type: kubernetes.io/tls
Puis créer l'ingress secure :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
tls:
- hosts:
- antoine-chiris.com
secretName: secure-ingress
rules:
- host: antoine-chiris.com
http:
paths:
- path: /service1
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
- path: /service2
pathType: Prefix
backend:
service:
name: service2
port:
number: 80
On teste ensuite que tout fonctionne bien via un curl :
Antoine@cks-master:~$ curl https://antoine-chiris.com:31827/service2 -kv --resolve antoine-chiris.com:31827:34.141.123.16
* Added antoine-chiris.com:31827:34.141.123.16 to DNS cache
...
* Server certificate:
* subject: C=FR; ST=Some-State; O=Internet Widgits Pty Ltd; CN=antoine-chiris.com
* start date: May 3 20:18:51 2022 GMT
* expire date: May 3 20:18:51 2023 GMT
* issuer: C=FR; ST=Some-State; O=Internet Widgits Pty Ltd; CN=antoine-chiris.com
* SSL certificate verify result: self signed certificate (18), continuing anyway.
Les metadatas des noeuds peuvent contenir des données sensibles (mots de passes, clé kubelet...).
Il est donc recommandé de limiter l'accès à ses données. Il ne faut donner que le nécessaire.
Dans Kubernetes nous pouvons limiter l'accès via les NetworkPolicies
.
Sur GCP, nous pouvons accéder aux metadatas depuis les VMs. Par exemple :
Antoine@cks-master:~$ curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/" -H "Metadata-Flavor: Google"
0/
Antoine@cks-master:~$ curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/0/" -H "Metadata-Flavor: Google"
device-name
index
interface
mode
type
Antoine@cks-master:~$ curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/0/type" -H "Metadata-Flavor: Google"
PERSISTENT
On peut donc trouver des informations sensibles sur l'instance. C'est exactement pareil dans un container :
Antoine@cks-master:~$ k exec -it pod1 -- bash # Pod nginx
root@pod1:/\# curl "http://metadata.google.internal/computeMetadata/v1/instance/disks/0/" -H "Metadata-Flavor: Google"
device-name
index
interface
mode
type
On va maintenant limiter certains pods d'accéder à ces metadatas :
# Aucun pod ne peut accéder aux metadatas (IP 169.254.169.254)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cloud-metadata-deny
namespace: default
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # Autorise à sortir sur toutes les IPs
except:
- 169.254.169.254/32 # Sauf l'IP des metadatas Google
On va autoriser que certains pods :
# Autorise uniquement les pods avec le label role=metadata-accessor
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: cloud-metadata-allow
namespace: default
spec:
podSelector:
matchLabels:
role: metadata-accessor
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 169.254.169.254/32
Le Centre pour la sécurité internet est une entité à but non lucratif dont la mission consiste à "identifier, développer, valider, promouvoir et soutenir les solutions recommandées pour la cyberdéfense".
Ils disposent de recommandations pour de nombreuses technologies dont Kubernetes (https://www.cisecurity.org/benchmark/kubernetes).
Toutes les vérifications peuvent être automatisés : https://github.com/aquasecurity/kube-bench/blob/main/docs/running.md
Voici la commande à éxécuter sur le master/worker :
# Master
docker run --pid=host -v /etc:/etc:ro -v /var:/var:ro -t aquasec/kube-bench:latest run --targets=master --version 1.23
# Worker
docker run --pid=host -v /etc:/etc:ro -v /var:/var:ro -t aquasec/kube-bench:latest run --targets=node --version 1.23
Un compte rendu devrait s'afficher :
== Summary managedservices ==
0 checks PASS
0 checks FAIL
37 checks WARN
0 checks INFO
== Summary total ==
9 checks PASS
3 checks FAIL
65 checks WARN
0 checks INFO
Nous allons télécharger une version de kubernetes et vérifier le hash de celle-ci afin de confirmer son authenticité.
Nous allons récupérer le lien de téléchargement de la dernière version ici : https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.24.md
Dans "Server Binaries", nous allons récupérer le lien amd64 (ici)
puis faire un wget sur la machine :
wget https://dl.k8s.io/v1.24.0/kubernetes-server-linux-amd64.tar.gz
On va maintenant comparer le hash (sha512sum
) avec celui vu sur le github :
Antoine@cks-master:~$ sha512sum kubernetes-server-linux-amd64.tar.gz
43a3e68bed60252b588493d07ed85eaa35ff3fec7f9440096fe9af284925f040467d1b31a8948e3035e4738bb689ad6d6fb9208fe77c16b053874d020a3fabd3 kubernetes-server-linux-amd64.tar.gz
Le hash est le même ➔ RAS
Nous allons maintenant extraire le fichier compressé téléchargé au-dessus :
tar xzf kubernetes-server-linux-amd64.tar.gz
Puis nous allons comparer le sha512sum
de l'apiserver
:
Antoine@cks-master:~$ sha512sum kubernetes/server/bin/kube-apiserver
7045200a3dd3664d94c794466e40fe1983000ba2d5ab8146b797481a9b8158ee8e008c52cf4c98279a6404dbd029945e84764d09145669a58476b7edf5b0c690 kubernetes/server/bin/kube-apiserver
On va maintenant regarder si l'apiserver
que nous faisons tourner dans notre cluster est bien l'officiel :
root@cks-master:~$ crictl ps | grep api
c5ddd034a6088 62930710c9634 41 minutes ago Running kube-apiserver 5 a7f90abe93bf5
root@cks-master:~$ ps aux | grep kube-apiserver
root 3472 5.3 8.2 1110532 330624 ? Ssl 20:32 2:12 kube-apiserver --advertise-address=10.156.0.2 --allow-privileged=true --authorization-mode=Node,RBAC --client-ca-file=/etc/kubernetes/pki/ca.crt --enable-admission-plugins=NodeRestriction --enable-bootstrap-token-auth=true --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key --etcd-servers=https://127.0.0.1:2379 --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname --proxy-client-cert-file=/etc/kubernetes/pki/front-proxy-client.crt --proxy-client-key-file=/etc/kubernetes/pki/front-proxy-client.key --requestheader-allowed-names=front-proxy-client --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-group-headers=X-Remote-Group --requestheader-username-headers=X-Remote-User --secure-port=6443 --service-account-issuer=https://kubernetes.default.svc.cluster.local --service-account-key-file=/etc/kubernetes/pki/sa.pub --service-account-signing-key-file=/etc/kubernetes/pki/sa.key --service-cluster-ip-range=10.96.0.0/12 --tls-cert-file=/etc/kubernetes/pki/apiserver.crt --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
root 18545 0.0 0.0 14860 2572 pts/1 S+ 21:13 0:00 grep --color=auto kube-apiserver
root@cks-master:~$ ls /proc/3472/root/
bin boot dev etc go-runner home lib proc root run sbin sys tmp usr var
root@cks-master:~$ find /proc/3472/root/ -name kube-apiserver
/proc/3472/root/usr/local/bin/kube-apiserver
root@cks-master:~$ sha512sum /proc/3472/root/usr/local/bin/kube-apiserver
7045200a3dd3664d94c794466e40fe1983000ba2d5ab8146b797481a9b8158ee8e008c52cf4c98279a6404dbd029945e84764d09145669a58476b7edf5b0c690 /proc/3472/root/usr/local/bin/kube-apiserver
Le sha512sum
est le même. Nous faisons donc bien tourner une version officielle de l'apiserver.