Sicurezza e Controllo di Accesso

Modello

Una tipica rcichiesta API dal client kubectl allo API Server di Kubernetes passa possibilmente attraveso alcune fasi di sicurezza:

Auth01

Le fasi sono:

  • Autenticazione
  • Autorizzazione RBAC
  • Controllo di Accesso

RBAC è il modello di autorizzazjone usato da Kubernetes: Role Based Access Control.

Questo modello implementa il Principio del Minimo Privilegio: tutte le azioni sono proibite a meno che esista una regola che le consenta.

Esempio: L'utente pippo vuole creare un Deployment app1 nel namespace util.

  • il modulo di autenticazione determina se pippo è un utente reale o un impostore
  • il modulo di autorizzazione determina se pippo ha il permesso di creare un Deloyment nel namespace util
  • il controllo di accesso applica le policy di creazione di Deployment

Autenticazione

Livello indicato anche come AuthN (auth-enne).

Le richieste API includono credenziali e il modulo le verifica. Se la verifica fallisce, viene ritornato un errore HTTP 401.

Il modulo non è parte di Kubernetes, che non si occupa di gestione account. Questa è fornita dall'esterno, possibilmente con un plugin, p.es. il Cloud Provider fornisce il plugin e le credenziali di autenticazione.

Le credenziali di autenticazione sono mantenute nel file di kubeconfig, che nel caso di Linux è $HOME/.kube/config.

less ~/.kube/config
...
users:
- name: kind-kind
  user:
    client-certificate-data: ...
    client-key-data: ...
...

Si può vedere la configurazione più concisamente col comando:

kubectl config view

Nel nostro caso è un Certificato Client, supportato da ogni versione di Kubernetes.

Questo certificato è stato prodotto da Kind in fase di installazione del cluster. Alternativamente si possono usare strumenti esterni per generarlo, e inserirlo nel file di configurazione.

Si può ispezionare il certificato degli utenti installando l'utility jq:

sudo apt install jq

E dando il comando:

kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"$(kubectl config current-context)\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text

Autorizzazione

Abbreviato in AuthZ (auth-zi).

Si occupa di tre aspetti:

  1. users
  2. actions
  3. resuorces

Descrive quali users possono compire quali actions su che tipo di resources.

Un esempio può essere dato dalla seguente tabella:

User (subject)ActionResource
BaocreatePods
KalilalistDeployments
JoshdeleteServiceAccounts

RBAC si basa su due concetti:

  • Role - definisce un insieme di permessi
  • RoleBinding - concede i permessi a utenti

Role

Un esempio di Manifest che definisce un Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: shield
  name: read-deployments
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "watch", "list"]

Notare la particolare apiVersion.

Notare le direttive:

  • namespace - si applica a questo spazio nomi
  • apiGroups - indica la componente gruppo del campo apiVersion
    • per esempio sappiamo che i Deployments hanno apiVersion apps/v1
    • se apiGroups è omesso o se apiGroups: [""], allora è lo apiGroup core, p.es. Services che ha solo apiVersion: v1
  • resources - quali oggetti Kubernetes sono influiti
  • verbs - quali comandi di kubectl si possono usare

Per avere una lista dei verbi disponibili per ogni risorsa:

kubectl api-resources --sort-by name -o wide

Nei campi apiGroups, resources e verbs si può mettere un asterisco (*) che indica tutti i valori.

Per esempio:

...
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
...

RoleBinding

Un Role deve essere collegato ad un utente con un RoleBinding.

Esempio:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-deployments
  namespace: shield
subjects:
- kind: User
  name: sky
# This is the authenticated user
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: read-deployments
# This is the Role to bind to the user
  apiGroup: rbac.authorization.k8s.io

L'utente autenticato "sky" può compiere un comando come: kubectl get deployments -n shield.

Con adeguata progettazione dei Role e RoleBinding si ottiene che certi utenti autenticati possano compiere solo certe azioni solo su certi oggetti di un determinato spazio nomi.

Roles01

Cluster Level

Vi sono in realtà 4 oggetti RBAC:

  • Roles
  • ClusterRoles
  • RoleBindings
  • ClusterRoleBindings

Roles e RoleBindings sono specifici di namespace.

ClusterRoles e ClusterRoleBindings si applicano all'intero cluster.

ClusterRole

Esempio:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: read-deployments
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "watch", "list"]

NOTA

Questo è solo un esempio. In generale non è una buona idea modificare i ClusterRole o ClusterRoleBinding se non si procede com molta cautela

Una serie di ClusterRole sono definiti nel cluster. Per vederli:

kubectl get clusterrole

Molti sono creati all'atto dell'installazione del cluster. Alcuni sono stati aggiunti da Manifests.

Per vedere i dettagli di un ClusterRole, p.es.:

kubectl describe clusterrole metallb-system:controller

ClusterRoleBinding

Vedere tutti i ClusterRoleBinding:

kubectl get vlusterrolebinding

Vedere i dettagli di uno, p.es.:

kubectl describe clusterrolebinding system:basic-user
Name:         system:basic-user
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  system:basic-user
Subjects:
  Kind   Name                  Namespace
  ----   ----                  ---------
  Group  system:authenticated  

Sequenza

Il cluster ispeziona il certificato dell'utente che accede, ricercando il campo "Subject:". Lo possiamo vedere da:

kubectl config view --raw -o json \
| jq ".users[] | select(.name==\"$(kubectl config current-context)\")" \
| jq -r '.user["client-certificate-data"]' \
| base64 -d | openssl x509 -text | grep "Subject:"
        Subject: O = system:masters, CN = kubernetes-admin

Ne estrae la proprietà O che ha valore system:masters. Questo è un gruppo predefinito che equivale agli amministratori globali di sistema.

Il ClusterRoleBinding corrispondente, predefinito, è cluster-admin. Lo si può vedere con:

kubectl describe clusterrolebinding cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
Role:
  Kind:  ClusterRole
  Name:  cluster-admin
Subjects:
  Kind   Name            Namespace
  ----   ----            ---------
  Group  system:masters

Il ClusterRole associato è cluster-admin. Possiamo ispezionarlo con:

kubectl describe clusterrole cluster-admin
Name:         cluster-admin
Labels:       kubernetes.io/bootstrapping=rbac-defaults
Annotations:  rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
  Resources  Non-Resource URLs  Resource Names  Verbs
  ---------  -----------------  --------------  -----
  *.*        []                 []              [*]
             [*]                []              [*]

Questo equivale in pratica a tutti i permessi su tutti gli spazi nomi.

Controllo Accesso

E' basato su una serie di Policy ed è implementato da degli Admission Controllers.

Vi sono due tipi di Admission Controllers:

  • validating - controllano la validità di una richiesta ma non possono cambiarla
  • mutating - possono modificare una richiesta

Gli Admission Controllers di tipo Mutating sono eseguiti per primi.

Un'implementazione di cluster ha un numero considerevole di Admission Controllers. Sono controllati da un numero considerevole di settaggi nei Manifest delle risorse gestite.

Per esempio, il settaggio .spec.containers.imagePullPolicy corrisponde al controller AlwaysPullImages.

La possibilità di loro gestione è determinata dall'implementazione del cluster, ed è di solito eseguita col comando kube-apiserver.

Nell'implementazione cluster Kind questo comando non è direttamente usabile ma è interno al pod kube-apiserver-kind-control-plane nello spazio nomi kube-system.

Per vedere i Controllers installati in Kind, il comando può essere:

kubectl exec kube-apiserver-kind-control-plane \
-n kube-system -- kube-apiserver --help | \
grep enable-admission-plugins | grep enabled | \
cut -d. -f2

Sfortunatamente il controllo fine può essere eseguito solo modificando il codice sorgente di Kind.


Esempi

Il Manifest di creazione del servizio di Metallb fornisce numerosi esempi d'uso di controllo di accesso.

Creazione di ServiceAccount

apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app: metallb
  name: controller
  namespace: metallb-system

Definizione di ClusterRole

Il ClusterRole è globale di cluster.

Una serie di regole, ciascuna contraddistinta da apiGroups/resources.

Quali sono i verbi, cioè le azioni, che chi possiede questo ClusterRole può compiere sugli apiGroups/resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app: metallb
  name: metallb-system:controller
rules:
- apiGroups:
  - ''
  resources:
  - services
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ''
  resources:
  - services/status
  verbs:
  - update
- apiGroups:
  - ''
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - policy
  resourceNames:
  - controller
  resources:
  - podsecuritypolicies
  verbs:
  - use

Definizione di Role

Locale di namespace. Serie di regole serie di regole, ciascuna contraddistinta da apiGroups/resources.

Quali sono i verbi, cioè le azioni, che chi possiede questo ClusterRole può compiere sugli apiGroups/resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app: metallb
  name: config-watcher
  namespace: metallb-system
rules:
- apiGroups:
  - ''
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch

Chi possiede il ruolo config-watcher, solo nel namespace metallb-system: su tutti i ConfigMaps può dare i comandi kubectl get, kubectl list e kubectl watch.

In realtà non sono comandi client di kubectl, sono verbi della API REST che vengono inviati allo API Server.

Definizione di RoleBinding

Associazione tra ServiceAccount e Role.

Questo RoleBinding si chiama config-watcher, che è anche il nome di un Role: non confondersi, sono due oggetti diversi.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app: metallb
  name: config-watcher
  namespace: metallb-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: config-watcher
subjects:
- kind: ServiceAccount
  name: controller
- kind: ServiceAccount
  name: speaker

DaemonSet

Finalmente il Daemonset contiene specifiche di chi può operare.

Contiene anche molti altri elementi interessanti, utili per imparare seguendo gli esempi.

Prima l'intera specifica:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: metallb
    component: speaker
  name: speaker
  namespace: metallb-system
spec:
  selector:
    matchLabels:
      app: metallb
      component: speaker
  template:
    metadata:
      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'
      labels:
        app: metallb
        component: speaker
    spec:
      containers:
      - args:
        - --port=7472
        - --config=config
        - --log-level=info
        env:
        - name: METALLB_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: METALLB_HOST
          valueFrom:
            fieldRef:
              fieldPath: status.hostIP
        - name: METALLB_ML_BIND_ADDR
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        # needed when another software is also using memberlist / port 7946
        # when changing this default you also need to update the container ports definition
        # and the PodSecurityPolicy hostPorts definition
        #- name: METALLB_ML_BIND_PORT
        #  value: "7946"
        - name: METALLB_ML_LABELS
          value: "app=metallb,component=speaker"
        - name: METALLB_ML_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: memberlist
              key: secretkey
        image: quay.io/metallb/speaker:v0.12.1
        name: speaker
        ports:
        - containerPort: 7472
          name: monitoring
        - containerPort: 7946
          name: memberlist-tcp
        - containerPort: 7946
          name: memberlist-udp
          protocol: UDP
        livenessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_RAW
            drop:
            - ALL
          readOnlyRootFilesystem: true
      hostNetwork: true
      nodeSelector:
        kubernetes.io/os: linux
      serviceAccountName: speaker
      terminationGracePeriodSeconds: 2

La linea:

      serviceAccountName: speaker

specifica che questo DaemonSet opera come account Speaker, definito sopra, che ha sicuramente un RoleBinding ad un Role, e quest'ultimo lista le azioni che può compiere sulle varie risorse.

Le linee:

      nodeSelector:
        kubernetes.io/os: linux

richiedono che il sistema operativo sottostante sia Linux.

Le linee:

      tolerations:
      - effect: NoSchedule
        key: node-role.kubernetes.io/master
        operator: Exists

richiedono che il pod del DaemonSet corrente non venga schedulato sul Master del cluster.

Il Security Context del contenitore del pod è particolarmente interessante:

        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            add:
            - NET_RAW
            drop:
            - ALL
          readOnlyRootFilesystem: true

L'account che opera i pod di questo DaemonSet, che è speaker:

  • non può dare comandi equivalenti a sudo che compiano un Privilege Escalation
  • se usa il filesystem di root (quello del container), è in sola lettura
  • tutti i capabilities sono tolti eccetto NET_RAW, evidentemente il programma inserito nell'immagine usa solo un socket 'Raw', come fa ad esempio ICMP.

Notare le linee:

        - name: METALLB_ML_BIND_ADDR
          valueFrom:
            fieldRef:
              fieldPath: status.podIP

Sono l'assegnazione di valore ad una variabile d'ambiente, prese dal campo status.podIP. Quando il pod è creato e allocato ad un nodo, il suo indirizzo IP è già stato assegnato.

L'interessante è che questo campo non esiste nel Manifest, che descrive il desired state, ma è parte del current state del pod appena creato.

Notare anche i probe Liveness e Readiness.

Deployment

Il Deployment è anch'esso interessante:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: metallb
    component: controller
  name: controller
  namespace: metallb-system
spec:
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: metallb
      component: controller
  template:
    metadata:
      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'
      labels:
        app: metallb
        component: controller
    spec:
      containers:
      - args:
        - --port=7472
        - --config=config
        - --log-level=info
        env:
        - name: METALLB_ML_SECRET_NAME
          value: memberlist
        - name: METALLB_DEPLOYMENT
          value: controller
        image: quay.io/metallb/controller:v0.12.1
        name: controller
        ports:
        - containerPort: 7472
          name: monitoring
        livenessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /metrics
            port: monitoring
          initialDelaySeconds: 10
          periodSeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - all
          readOnlyRootFilesystem: true
      nodeSelector:
        kubernetes.io/os: linux
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
      serviceAccountName: controller
      terminationGracePeriodSeconds: 0

Vi sono due securityContext, uno a livello container e uno a livello pod. Quest'ultimo è:

      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
      serviceAccountName: controller

L'utente controller, che gestisce questo deployment, se ha interazioni con il Linux sottostante, è mappato all'utente di sistema 65534, gruppo di sistema 65534.

Le annotazioni:

      annotations:
        prometheus.io/port: '7472'
        prometheus.io/scrape: 'true'

vengono usate dall'utility Prometheus se esistente.