È un po’ che non scrivo su questo blog ormai legacy… sarebbe meglio chiamarlo gitopstribe?! 😀

Direi di riprendere con un post recap che faccia da diario durante l’installazione di una piattaforma di laboratorio basata su Kubernetes.

La base di partenza è sempre il mio solito server Kimsufi (Server KS-9 – Intel W3520 – 16GB DDR3 ECC 1333 MHz – 240GB SSD) con Proxmox Virtual Environment 7.1-11.

Tramite Cloud Init ho configurato velocemente 3 nodi con sotto Rocky Linux 8.

Una volta preparate le VMs ho clonato il mitico KubeSpray e preparato un inventory Ansible ad-hoc in questo modo:

cp -rfp inventory/sample inventory/kimsufi

cat inventory/kimsufi/inventory.ini

[all]
k8s1 ip=10.10.10.4 etcd_member_name=etcd1
k8s2 ip=10.10.10.5 etcd_member_name=etcd2
k8s3 ip=10.10.10.6 etcd_member_name=etcd3

[kube_control_plane]
k8s1
k8s2
k8s3

[etcd]
k8s1
k8s2
k8s3

[kube_node]
k8s1
k8s2
k8s3

[calico_rr]

[k8s_cluster:children]
kube_control_plane
kube_node
calico_rr

Nell’inventory ho degli indirizzi privati e siccome dalla mia workstation non vedo direttamente la 10.10.10.0/24 in Proxmox ho configurato così il mio .ssh/config… ovvero faccio jump attraverso il nodo fisico ns304365.ip-94-23-211.eu.

Host k8s1
  HostName 10.10.10.4
  User rocky
  ProxyCommand ssh -W %h:%p -lroot kimsufi

Host k8s2
  HostName 10.10.10.5
  User rocky
  ProxyCommand ssh -W %h:%p -lroot kimsufi

Host k8s3
  HostName 10.10.10.6
  User rocky
  ProxyCommand ssh -W %h:%p -lroot kimsufi

Host kimsufi
  HostName ns304365.ip-94-23-211.eu
  User root
  Port 2222
  ServerAliveInterval 20

Ecco come lanciare KubeSpray per l’installazione:

ansible-playbook -i inventory/kimsufi/inventory.ini --become --become-user=root cluster.yml

Se il provisioning via Ansible è andato a buon fine l’output è più o meno questo:

PLAY RECAP **************************************************************************************************************************************************************************************************************************
k8s1                       : ok=766  changed=96   unreachable=0    failed=0    skipped=1319 rescued=0    ignored=9
k8s2                       : ok=670  changed=84   unreachable=0    failed=0    skipped=1147 rescued=0    ignored=4
k8s3                       : ok=684  changed=140  unreachable=0    failed=0    skipped=1149 rescued=0    ignored=4
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Una volta installato k8s ci si può connettere con kubectl.. ormai nulla di troppo sciamanico 😀

luckysideburn:kubespray$ ssh k8s1

Last login: Wed Jun  8 14:33:15 2022 from 10.10.10.1
[rocky@k8s1 ~]$ sudo  su - root
Last login: Wed Jun  8 14:33:29 UTC 2022 on pts/0

[root@k8s1 ~]# kubectl get namespaces
NAME              STATUS   AGE
default           Active   18m
kube-node-lease   Active   18m
kube-public       Active   18m
kube-system       Active   18m

Siccome sono povero rendo i nodi “master” anche “worker” in modo da poter agire sia da control-plane che da esecutori di pod applicativi:

kubectl label node k8s1 node-role.kubernetes.io/worker=worker
kubectl label node k8s2 node-role.kubernetes.io/worker=worker
kubectl label node k8s3 node-role.kubernetes.io/worker=worker

… mi rendo conto che la cpu è insufficiente per girare pod applicativi… non mi resta che spengere massivamente il cluster, aumentare le CPU e farlo ripartire.. Uso ansible…

ansible all -m shell -a "sudo shutdown -h now" -i inventory/kimsufi/inventory.ini

Helm

Prima di tutto mi serve Helm per installare in futuro un po’ di robette utili prese dal landscape del CNCF.

Come riportato qui procedo con i seguenti step:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

Ci sono molti modi per installare Helm. Uso il metodo “script” per pigrizia. Basta un copia e incolla ed è fatta. Certo, lanciare script remoti alla cieca su un server è pericoloso, ma ci fidiamo della community 😀

[root@k8s1 ~]# ./get_helm.sh
Downloading https://get.helm.sh/helm-v3.9.0-linux-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
helm installed into /usr/local/bin/helm

Ingress Controller

Mi serve un ingress controller… Vado su quello che conosco meglio, ovvero Nginx! Come riportato qui procedo con un installazione standard via Helm.

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

Vessel Operator

Uso Vessel, un progetto tutto Sourcesense.

È un tool open-source che sviluppiamo e usiamo per controllare se sui nostri cluster Kube è tutto ok a livello di:

  1. Best practice (tipo: ho settato Limits e Requests?).
  2. CVE delle immagini.

Ecco come installarlo (trovate la guida sul repo ufficiale di Vessel Operator)


yum install make -y

# Clono il repo del Vessel Operator
git clone https://github.com/sourcesense/vessel-operator

cd vessel-operator/

# Controllo che tutto sia running
kubectl -n vessel-operator-system get ingress,pods,services

pod/vessel-operator-controller-manager-55694999c-h7ptc   2/2     Running   0          98s

# Creo un ServiceAccount con diritti di admin e reperisco il suo token. Per comodità uso cluster-admin ma è possibile anche usare dei ruoli ad-hoc!

kubectl create serviceaccount k8sadmin -n kube-system
kubectl create clusterrolebinding k8sadmin --clusterrole=cluster-admin --serviceaccount=kube-system:k8sadmin
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | (grep k8sadmin || echo "$_") | awk '{print $1}') | grep token: | awk '{print $2}'

Come prima cosa voglio ispezionare il namespace più importante di tutti, ovvero kube-system e vedere come è messo. Creo quindi una custom resource dedicata:

apiVersion: cache.sourcesense/v1alpha1
kind: Vessel
metadata:
  name: vessel-sample
spec:
  vessel_nsname: 'kube-system'
  vessel_k8s_url: 'https://10.10.10.4:6443'
  vessel_k8s_token: 'token super secret'
  vessel_ingress_enable: true
  vessel_ingress_host: 'vessel.sourcesense.com'
  vessel_ingress_class: 'nginx'

La salvo in vessel-sample.yaml e applico con kubectl:

kubectl apply -f vessel-sample.yaml 

# Verifico corretta installazione di Vessel
[root@k8s1 vessel-operator]# kubectl get vessel
NAME            AGE
vessel-sample   6s

Ora ispezioniamo kube-system!

# Prendo IP del service di Vessel. C'è anche l'ingress ma lo chiamo dall'interno per cui va bene anche il service.

[root@k8s1 vessel-operator]# kubectl get svc -A  | grep vessel-service
kube-system              vessel-service                                       ClusterIP      10.233.40.132   <none>        80/TCP                       10m

A caccia di CVE Critical!

Di seguito interrogo Vessel per capire quali immagini hanno CVE a livello Critical o High. Vessel utilizza Trivy per trovare vulnerabilità nelle immagini in essere su un cluster k8s.

[root@k8s1 vessel-operator]#  curl -s http://10.233.40.132/query?issue=HIGH_CVE | jq
{
  "size": 20,
  "count": 43,
  "page": 1,
  "result": [
    {
      "id": 1,
      "name": "calico-kube-controllers",
      "namespace": "kube-system",
      "kind": "deployment",
      "issue": "HIGH_CVE",
      "issue_metadata": "{'id': 'CVE-2018-1098', 'severity': 'HIGH', 'package': 'go.etcd.io/etcd', 'name': 'etcd: Cross-site request forgery via crafted local POST forms', 'description': \"A cross-site request forgery flaw was found in etcd 3.3.1 and earlier. An attacker can set up a website that tries to send a POST request to the etcd server and modify a key. Adding a key is done with PUT so it is theoretically safe (can't PUT from an HTML form or such) but POST allows creating in-order keys that an attacker can send.\", 'link': 'https://avd.aquasec.com/nvd/cve-2018-1098'}",
      "tool": "vessel.tools.trivy",
      "current": true,
      "created_at": "2022-06-08 15:26"
    },
...
....
    {
      "id": 11,
      "name": "nodelocaldns",
      "namespace": "kube-system",
      "kind": "daemonset",
      "issue": "CRITICAL_CVE",
      "issue_metadata": "{'id': 'CVE-2022-23219', 'severity': 'CRITICAL', 'package': 'libc-bin', 'name': 'glibc: Stack-based buffer overflow in sunrpc clnt_create via a long pathname', 'description': 'The deprecated compatibility function clnt_create in the sunrpc module of the GNU C Library (aka glibc) through 2.34 copies its hostname argument on the stack without validating its length, which may result in a buffer overflow, potentially resulting in a denial of service or (if an application is not built with a stack protector enabled) arbitrary code execution.', 'link': 'https://avd.aquasec.com/nvd/cve-2022-23219'}",
      "tool": "vessel.tools.trivy",
      "current": true,
      "created_at": "2022-06-08 15:26"
    },
....
......

kube-system rispetta le best practice più comuni? 😀

Ovviamente questa è una provocazione, ma vediamo cosa ha tirato fuori vessel!

curl -s http://10.233.40.132/query?kind=deployment | jq

{
  "size": 20,
  "count": 6,
  "page": 1,
  "result": [  
    {
      "id": 48,
      "name": "dns-autoscaler",
      "namespace": "kube-system",
      "kind": "deployment",
      "issue": "missing_liveness_probe",
      "issue_metadata": null,
      "tool": "vessel.tools.linter",
      "current": true,
      "created_at": "2022-06-08 15:26"
    },
    {
      "id": 49,
      "name": "dns-autoscaler",
      "namespace": "kube-system",
      "kind": "deployment",
      "issue": "missing_limits",
      "issue_metadata": null,
      "tool": "vessel.tools.linter",
      "current": true,
      "created_at": "2022-06-08 15:26"
    }
  ]
}

Non ho trovato troppi problemi a parte i Limits e la LivenessProbe mancanti sul deployment del dns-autoscaler.

Conclusioni

Finalmente sono riuscito a prendere un paio d’ore per poter scrivere di nuovo un post su questo blog. Belle le call per carità, ma anche meno!

ToDo

  1. Continuare il setup di un equipaggiamento completo su questo cluster k8s installato con KubeSpray..
  2. Continuare gli sviluppi di Vessel e Vessel Operator.
  3. Fare meno call