Déploiement zéro confiance avec Kubernetes
L'utilisation de logiciels OpenSource écrits par des inconnus peut parfois être un peu effrayante. Encore plus lorsque je les déploie dans un environnement de production dans mon entreprise. Dans mon cas, j'ai créé un tout nouveau cluster Kubernetes pour héberger certains services privés sur mon réseau local et je voulais être certain qu'ils ne font rien de malveillant sur mon réseau.
Isoler les applications et les services
La première chose à faire est d'isoler chacun des services dans un namespace Kubernetes. Il s'agit d'une ressource centrale de Kubernetes qui permet d'isoler des groupes de ressources au sein d'un même cluster. Il est alors possible d'avoir un contrôle plus fin sur les accès, les permissions, le réseau sur les ressources.
Un Namespace peut être créé assez facilement avec la commande suivante :
Ensuite, je peux lister les Namespace avec :
Résultat :NAME STATUS AGE
default Active 4h52m
kube-system Active 4h52m
kube-public Active 4h52m
kube-node-lease Active 4h52m
tiwabbit-prod Active 2s
Ignorez les Namespaces préfixés par kube-
qui sont dédiés pour le control plane de Kubernetes
Je peux maintenant créer des ressources dans mon Namespace. Commençons par un Secret par exemple :
kubectl \
--namespace tiwabbit-prod \
create secret generic \
db-credentials \
--from-literal=username=tiwabbit \
--from-literal=password=mysecurepassword
Si je liste tous les Secrets de mon cluster :
Résultat :NAMESPACE NAME TYPE DATA AGE
[...]
default default-token-m59jl kubernetes.io/service-account-token 3 4h58m
tiwabbit-prod default-token-8zkr2 kubernetes.io/service-account-token 3 3m15s
tiwabbit-prod db-credentials Opaque 2 49s
En théorie, seuls les Pods exécutés dans mon Namespace (tiwabbit-prod
) peuvent monter ce secret et le lire.
Sécurisation RBAC
Par défaut, tous les Pods sans configuration spécifique utilisent le ServiceAccount default
du namespace dans lequel ils s'exécutent.
Ce dernier n'a aucun droit sur l'API Kubernetes ce qui est une bonne chose et ne devrait pas être modifié.
Cependant, dans certains scénarios, mon application peut devoir appeller l'API Kubernetes. Par exemple, s'il doit créer un batch à l'aide d'un Job Kubernetes. Prenons ce dernier exemple pour créer un ServiceAccount avec ces autorisations et l'attribuer à mon application Pod.
Je dois d'abord créer un nouveau ServiceAccount :
Ensuite, j'ai besoin d'un Role qui implémente le niveau d'autorisation dont mon Pod a besoin. voici le manifeste :
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-application-batch-creator
namespace: tiwabbit-prod
rules:
- apiGroups: []
resources: [ pods, pods/status, pods/log ]
verbs: [ get, list, watch ]
- apiGroups: [ batch ]
resources: [ jobs ]
verbs: [ create, get, list, watch, patch, update, delete ]
Enfin, je dois attribuer le Role à mon ServiceAccount en utilisant un RoleBinding avec la définition de manifeste suivante :
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-application-permissions
namespace: tiwabbit-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: my-application-batch-creator
subjects:
- kind: ServiceAccount
name: my-application
namespace: tiwabbit-prod
Créons maintenant un Pod avec le ServiceAccount et examinons les actions autorisées par le rôle. Voici un Déploiement manifeste :
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-application
namespace: tiwabbit-prod
spec:
replicas: 1
selector:
matchLabels:
app: my-application
template:
metadata:
labels:
app: my-application
spec:
serviceAccountName: my-application
containers:
- command:
- sleep
- 1d
image: alpine:latest
name: alpine
Entrer dans le Pod et suivre la documentation Kubernetes pour installer kubectl
.
Vérifions si je peux lister Pods en interrogeant l'API Kubernetes :
Résultat :Ensuite, je devrais essayer de créer une exécution par batch avec un Job :
Liste de tous les Jobs dans le Namespace :
Résultat :Lister les Pods dans le Namespace :
Résultat :NAME READY STATUS RESTARTS AGE
my-application-df5b5cb75-8twc5 1/1 Running 0 11m
my-application-batch-f2pf6 1/1 Running 0 3s
Mon Job est terminé et je peux le supprimer avec :
Conclusions
Si le processus de mon Pod n'a pas besoin de communiquer avec l'API Kubernetes (pour créer, interroger, supprimer des ressources), utilisez le default
ServiceAccount qui ne donne aucune autorisation à mon Pod. Dans d'autres cas, utilisez un ServiceAccount personnalisé pour chacune de mes applications et plusieurs Role et RoleBinding pour chacun de mes cas d'utilisation.
Empêcher l'utilisation de l'utilisateur root ou l'escalade de privilége
Kubernetes autorise par défaut plusieurs options de sécurité pouvant être appliquées aux pods et aux conteneurs sous-jacents. La plupart d'entre eux peuvent être configurés avec le bloc SecurityContext comme suit :
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext: {}
containers:
- name: my-secure-container
image: nginx:latest
securityContext: {}
- name: my-second-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext: {}
Évidemment, de telles options peuvent être ajoutées à un StatefulSet ou Deployment.
Exécuter le pod en tant que non root
La configuration la plus simple pour sécuriser le Pod consiste à remplacer l'UID et le GID de l'utilisateur exécutant le processus principal. De cette façon, quel que soit l'utilisateur par défaut dans l'image, Kubernetes le contournera. Un UID et un GID supérieurs ou égaux à 1000
doivent être utilisés.
Trois paramètres peuvent être utilisés :
runAsUser
: Permet de changer l'UID du processus par défautrunAsGroup
: Permet de changer le GID du processus par défautfsGroup
: si spécifié, l'utilisateur sera également dans ce groupe et tous les fichiers et répertoires créés prendront ce GID comme propriétaire.- Ce dernier paramètre peut augmenter le temps de montage d'un volume externe donné car Kubernetes garantit que les fichiers appartiennent au groupe défini par
fsGroup
. Résultant en unchmod -R
de tout le système de fichiers. fsGroupChangePolicy
peut être utilisé pour modifier ce comportement avec ces valeurs :OnRootMismatch
: vérifie uniquement le répertoire racine et modifiez tous les systèmes de fichiers si le groupe ne correspond pasAlways
: c'est dans le nom de l'option
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 2000
fsGroupChangePolicy: OnRootMismatch
containers:
- name: my-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext:
runAsUser: 2000
runAsGroup: 2000
L'application de l'exécution en tant que non root
peut être obtenue avec le paramètre runAsNonRoot
:
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext:
runAsNonRoot: true
containers:
- name: my-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext: {}
Utiliser le système de fichiers en lecture seule
Empêcher toute opération d'écriture/mise à jour/suppression on le système de fichiers racine peut empêcher un processus malveillant (ou une application piratée) de profiter de l'environnement du conteneur et de le modifier.
L'option readOnlyRootFilesystem
permet de réaliser cela :
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext: {}
containers:
- name: my-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext:
readOnlyRootFilesystem: true
Si l'application doit écrire des données temporaires ou d'application pendant l'exécution, je peux toujours créer un volume EmptyDir et le monter à l'intérieur du conteneur :
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext: {}
containers:
- name: my-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /data
name: my-data
volumes:
- name: my-data
emptyDir: {}
Empêcher l'escalade privilégiée
Le noyau Linux expose la fonction de bas niveau pour qu'un processus à modifier soit actuel UID ou GID. Par exemple, en utilisant les fonctions privatives [setuid][linux-setuid-wikipedia] ou setgid.
L'utilisation de l'option allowPrivilegeEscalation
peut empêcher que cela ne se produise.
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext: {}
containers:
- name: my-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext:
allowPrivilegeEscalation: false
Suppression de toutes les capabilities
En ce qui concerne l'escalade privilégiée, Linux donne Capabilities au processus leur permettant de faire des appels système hautement privilégiés. Comme l'écoute sur des ports réseau end dessous de 1024
.
Nous ne voulons donner à notre processus que les capacités nécessaires à son exécution. Dans le cas de l'exemple on peut avoir :
apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext: {}
containers:
- name: my-second-secure-container
image: busybox:latest
command: ["sleep", "infinity"]
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
Renforcement de la communication réseau
Dans le monde des pods, par défaut, tous les pods de chaque espace de noms peuvent communiquer entre eux. Si vous avez plusieurs applications ou même plusieurs clients sur le même cluster Kubernetes, vous ne devez pas les autoriser à communiquer (du moins pas par défaut).
C'est pourquoi, des NetworkPolicies doivent être créés avec deny all
par défaut. Ouvrez ensuite des connexions point à point si les services/clients doivent communiquer entre eux.
La configuration par défaut de deny all
ressemblera à ceci :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
Ensuite, si vous avez une application tierce qui a besoin d'accéder à vos pods d'application :
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: my-namespace
spec:
podSelector:
matchLabels:
application: my-application
component: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
project: my-friend-namespace
- podSelector:
matchLabels:
application: his-application
ports:
- protocol: TCP
port: 8080