Powyżej swojego ‘capabilitisu’ nie podskoczysz - czyli o uprawnieniach kontenerów oraz podejściu do ich inwestygacji

OpenShift

Powyżej swojego ‘capabilitisu’ nie podskoczysz – czyli o uprawnieniach kontenerów oraz podejściu do ich inwestygacji

15/06/2020
Podziel się

Temat bezpieczeństwa kontenerów oraz całych platform kontenerowych to z pewnością zagadnienie bardzo szerokie. Dzisiaj spróbujemy analitycznie podejść do problemu uzyskania wiedzy o tym, gdzie tak naprawdę znajduje się nasz kontener? Postaramy się również prześledzić co może ten kontener i przede wszystkim, jak takie informacje uzyskiwać.

Ogólnie rzecz biorąc, skupiać będziemy się na mechanizmach niezależnych od samej platformy kontenerowej, a więc możliwych do prześledzenia i co ważniejsze zaaplikowania dla konkretnego kontenera niezależnie czy został on powołany do życia na platformie Red Hat OpenShift, czy np. Docker Enterprise (w poniższym tekście konkretnie będziemy używać platformy OpenShift).

Zainteresowany czytelnik poniższe przykłady może także w pewien zbliżony sposób obserwować, chociażby na własnym komputerze z zainstalowanym jedynie runtime’em kontenerowym. W tym przypadku jednak pierwsze zagadnienia szczególnie nie mają większego sensu, bo od początku wiemy, gdzie znajduje się nasz kontener. Zabawa zaczyna dopiero nabierać sensu, gdy na całej platformie złożonej z kilku lub nawet kilkudziesięciu węzłów mamy potrzebę dowiedzieć się, gdzie faktycznie znajduje się np. podmontowane z hosta zasoby dyskowe, z których dany kontener korzysta.

Jako nasze miejsce pracy weźmy najnowszą wersję platformy kontenerowej Red Hat OpenShift 4.4 o poniższej konfiguracji (3 węzły master oraz 3 nody compute):

[root@helper ocp44]# oc get nodes
NAME                	STATUS  ROLES       	AGE 	VERSION
master0.ocp4.internal   Ready	master,worker   2d19h   v1.17.1
master1.ocp4.internal   Ready	master,worker   2d19h   v1.17.1
master2.ocp4.internal   Ready	master,worker   2d19h   v1.17.1
worker0.ocp4.internal   Ready	worker      	2d18h   v1.17.1
worker1.ocp4.internal   Ready	worker      	2d18h   v1.17.1
worker2.ocp4.internal   Ready	worker      	2d18h   v1.17.1

Zaraz po utworzeniu nowego projektu o nazwie incepcja jesteśmy witani sugestią o możliwości stworzenia testowej aplikacji:

[root@helper ocp44]# oc new-project incepcja
Now using project "incepcja" on server "https://api.ocp4.internal:6443".

You can add applications to this project with the 'new-app' command. For example, try:

    oc new-app django-psql-example

to build a new example application in Python. Or use kubectl to deploy a simple Kubernetes application:

    kubectl create deployment hello-node --image=gcr.io/hello-minikube-zero-install/hello-node

Skorzystajmy z niej i stwórzmy sugerowaną testową aplikację:

[root@helper ocp44]#  oc new-app django-psql-example
--> Deploying template "openshift/django-psql-example" to project incepcja

 	Django + PostgreSQL (Ephemeral)
 	---------
 	An example Django application with a PostgreSQL database. For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.
	 
 	WARNING: Any data stored will be lost upon pod destruction. Only use this template for testing.

 	The following service(s) have been created in your project: django-psql-example, postgresql.
	 
 	For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.

 	* With parameters:
    	* Name=django-psql-example
    	* Namespace=openshift
    	* Version of Python Image=3.6
    	* Version of PostgreSQL Image=10
    	* Memory Limit=512Mi
    	* Memory Limit (PostgreSQL)=512Mi
    	* Git Repository URL=https://github.com/sclorg/django-ex.git
    	* Git Reference=
    	* Context Directory=
    	* Application Hostname=
    	* GitHub Webhook Secret=MqNj1NcmIe4Wp1wMNHTRgeDcnKdjLMGIeiYgEMly # generated
    	* Database Service Name=postgresql
    	* Database Engine=postgresql
    	* Database Name=default
    	* Database Username=django
    	* Database User Password=0qVHHjI70oyiliIP # generated
    	* Application Configuration File Path=
    	* Django Secret Key=JyZrFq6wItwtj3CxlUfjLi3vv2L5tagsRPVVXmKnE09q14yeHn # generated
    	* Custom PyPi Index URL=

--> Creating resources ...
	secret "django-psql-example" created
	service "django-psql-example" created
	route.route.openshift.io "django-psql-example" created
	imagestream.image.openshift.io "django-psql-example" created
	buildconfig.build.openshift.io "django-psql-example" created
	deploymentconfig.apps.openshift.io "django-psql-example" created
	service "postgresql" created
	deploymentconfig.apps.openshift.io "postgresql" created
--> Success
	Access your application via route 'django-psql-example-incepcja.apps.ocp4.internal'
	Build scheduled, use 'oc logs -f bc/django-psql-example' to track its progress.
	Run 'oc status' to view your app.

Po krótkiej chwili widzimy, że pody są już dostępne:

[root@helper ocp44]# oc get pods
NAME                  READY   STATUS  	  RESTARTS   AGE
postgresql-1-deploy   0/1     Completed   0          2m
postgresql-1-gkc8z    1/1     Running 	  0          2m

Teraz zastanówmy się chwilę. Jak sprawdzić dokładną lokalizacje naszego kontenera, a także zinwestygować gdzie działają jego faktyczne procesy? Jak one wyglądają? Do tego celu na początku pomocny będzie przełącznik -o do powyższego polecenia:

[root@helper ocp44]# oc get pods -o wide
NAME                READY STATUS     RESTARTS  AGE  IP          NODE                  NOMINATED NODE READINESS GATES
postgresql-1-deploy 0/1   Completed  0         75m  10.254.4.7  worker1.ocp4.internal <none>         <none>
postgresql-1-gkc8z  1/1   Running    0         75m  10.254.5.6  worker0.ocp4.internal <none>         <none>

Na jego podstawie widzimy, że kontener właściwy postgresql-1-gkc8z został rozłożony na węźle worker0.ocp4.internal. Skorzystamy jeszcze z opisu (‘describe’ tego kontenera). Najbardziej w tym momencie interesuje nas sekcja Containers:

[root@helper ocp44]# oc describe pod postgresql-1-gkc8z
Name:     	postgresql-1-gkc8z
Namespace:	incepcja
Priority: 	0
Node:     	worker0.ocp4.internal/192.168.80.88
Start Time:   Mon, 01 Jun 2020 12:45:46 +0200
Labels:   	deployment=postgresql-1
…
Containers:
  postgresql:
	Container ID:   cri-o://56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa
…

Mając już Container ID, możemy teraz zalogować się na węzeł worker0.ocp4.internal. Tam, wyszukując specyficznie procesów (pid):

[root@worker0 ~]# runc state 56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa |grep pid
  "pid": 2840455,

Sprawdźmy zatem wszystkie namespace’y związane z tym procesem:

[root@worker0 ~]# lsns -p 2840455
NS         TYPE    NPROCS  PID     USER       COMMAND
4026531835 cgroup  248     1       root       /usr/lib/systemd/systemd --switched-root --system --deserialize 16
4026531837 user    247     1       root       /usr/lib/systemd/systemd --switched-root --system --deserialize 16
4026532325 uts     11      2840039 root       /usr/bin/pod
4026532326 ipc     11      2840039 root       /usr/bin/pod
4026532329 net     11      2840039 root       /usr/bin/pod
4026532485 mnt     10      2840455 1000580000 postgres
4026532486 pid     10      2840455 1000580000 postgres

Wejdźmy teraz do tego namespace’u:

[root@worker0 ~]# nsenter -t 2840455 -p -r ps -ef
UID      	PID	PPID  C  STIME  TTY     TIME      CMD
1000580+   	1   	0     0  10:45  ?    	00:00:00  postgres
1000580+  	61   	1     0  10:45  ?    	00:00:00  postgres: logger process   
1000580+  	63   	1     0  10:45  ?    	00:00:00  postgres: checkpointer process   
1000580+  	64   	1     0  10:45  ?    	00:00:00  postgres: writer process   
1000580+  	65   	1     0  10:45  ?    	00:00:00  postgres: wal writer process   
1000580+  	66   	1     0  10:45  ?    	00:00:00  postgres: autovacuum launcher process   
1000580+  	67   	1     0  10:45  ?    	00:00:00  postgres: stats collector process   
1000580+  	68   	1     0  10:45  ?    	00:00:00  postgres: bgworker: logical replication launcher   
1000580+ 	873   	0     0  10:53  pts/0	00:00:00  /bin/sh
1000580+	1488    0     0  10:58  pts/1	00:00:00  /bin/sh
root   	        14649   0     0  12:41  pts/0   00:00:00  ps -ef

Widzimy, że istotnie działające tam procesy związane są z bazą danych Postgres. Jak to wygląda od strony samego kontenera? Zobaczmy:

[root@helper ocp44]# oc rsh postgresql-1-gkc8z
sh-4.2$ ps -ef
UID        PID	  PPID   C  STIME  TTY    TIME      CMD
1000580+   1      0      0  10:45  ?      00:00:02  postgres
1000580+   61     1      0  10:45  ?      00:00:00  postgres: logger process   
1000580+   63     1      0  10:45  ?      00:00:00  postgres: checkpointer process   
1000580+   64     1      0  10:45  ?      00:00:00  postgres: writer process   
1000580+   65     1      0  10:45  ?      00:00:00  postgres: wal writer process   
1000580+   66     1      0  10:45  ?      00:00:00  postgres: autovacuum launcher process   
1000580+   67     1      0  10:45  ?      00:00:01  postgres: stats collector process   
1000580+   68     1      0  10:45  ?      00:00:00  postgres: bgworker: logical replication launcher   
1000580+   873    0      0  10:53  pts/0  00:00:00  /bin/sh
1000580+   1488   0      0  10:58  pts/1  00:00:00  /bin/sh
1000580+   56383  0      0  18:10  pts/2  00:00:00  /bin/sh
1000580+   56412  56383  0  18:10  pts/2  00:00:00  ps -ef

Widzimy zatem dokładnie ten sam output.

Przyjrzyjmy się  jeszcze przez raz konfiguracji kontenera. W tym momencie będzie nas najbardziej interesowała sekcja capabilities:

[root@helper ocp44]# oc get pod postgresql-1-gkc8z --output=yaml
apiVersion: v1
kind: Pod
metadata:
  annotations:
…
	securityContext:
  	capabilities:
    	drop:
    	- KILL
    	- MKNOD
    	- SETGID
    	- SETUID
  	runAsUser: 1000580000
…

Widzimy tutaj kilka capabilities kontenera, którego został on pozbawiony. Konkretnie kill, mknod, setgid, setuid. W tym momencie ponownie ‘wchodząc na kontener’ i listując wszystkie dostępne capabilities, powinniśmy potwierdzić fakt wzajemnego pokrywania się tych listingów:

sh-4.2$ capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot+i
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=1000580000(1000580000)
gid=0(root)
groups=1000580000(???)
sh-4.2$

Istotnie, żadne capabilities wymienione w sekcji drop pliku yaml nie są obecne na liście tych wymienionych jako bounding set.

Kolejnym poziomem separacji jest poziom Control Group (cgroup). Sprawdźmy je dla wcześniej znalezionego procesu:

[root@worker0 ~]# cat /proc/2840455/cgroup 
12:memory:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 11:net_cls,net_prio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 10:cpuset:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 9:freezer:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 8:rdma:/ 7:hugetlb:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 6:pids:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 5:devices:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 4:perf_event:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 3:cpu,cpuacct:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 2:blkio:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope 1:name=systemd:/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod05fba1f1_8651_4b0c_8e36_01bc23098955.slice/crio-56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa.scope

W OpenShift struktura cgroup znajduje się w drzewie /hubepods.slice/:

[root@worker0 ~]# cd /sys/fs/cgroup/  	 
[root@worker0 cgroup]# ls
blkio  cpu  cpu,cpuacct  cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_cls,net_prio  net_prio  perf_event  pids  rdma  systemd
[root@worker0 cgroup]# ls -la
total 0
drwxr-xr-x. 14 root root 360 May 29 16:19 .
drwxr-xr-x.  8 root root   0 May 29 16:19 ..
dr-xr-xr-x.  5 root root   0 May 29 16:19 blkio
lrwxrwxrwx.  1 root root  11 May 29 16:19 cpu -> cpu,cpuacct
dr-xr-xr-x.  5 root root   0 May 29 16:19 cpu,cpuacct
lrwxrwxrwx.  1 root root  11 May 29 16:19 cpuacct -> cpu,cpuacct
dr-xr-xr-x.  5 root root   0 May 29 16:19 cpuset
dr-xr-xr-x.  5 root root   0 May 29 16:19 devices
dr-xr-xr-x.  4 root root   0 May 29 16:19 freezer
dr-xr-xr-x.  4 root root   0 May 29 16:19 hugetlb
dr-xr-xr-x.  5 root root   0 May 29 16:19 memory
lrwxrwxrwx.  1 root root  16 May 29 16:19 net_cls -> net_cls,net_prio
dr-xr-xr-x.  4 root root   0 May 29 16:19 net_cls,net_prio
lrwxrwxrwx.  1 root root  16 May 29 16:19 net_prio -> net_cls,net_prio
dr-xr-xr-x.  4 root root   0 May 29 16:19 perf_event
dr-xr-xr-x.  5 root root   0 May 29 16:19 pids
dr-xr-xr-x.  2 root root   0 May 29 16:19 rdma
dr-xr-xr-x.  6 root root   0 May 29 16:19 systemd

Np. w przypadku ustawień dla procesora:

[root@worker0 cgroup]# cd cpuset/kubepods.slice/
[root@worker0 kubepods.slice]# ls -la
total 0
drwxr-xr-x.  4 root root 0 May 29 16:20 .
dr-xr-xr-x.  5 root root 0 May 29 16:19 ..
-rw-r--r--.  1 root root 0 May 29 16:20 cgroup.clone_children
-rw-r--r--.  1 root root 0 May 29 16:20 cgroup.procs
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.cpu_exclusive
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.cpus
-r--r--r--.  1 root root 0 May 29 16:20 cpuset.effective_cpus
-r--r--r--.  1 root root 0 May 29 16:20 cpuset.effective_mems
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.mem_exclusive
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.mem_hardwall
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.memory_migrate
-r--r--r--.  1 root root 0 May 29 16:20 cpuset.memory_pressure
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.memory_spread_page
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.memory_spread_slab
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.mems
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.sched_load_balance
-rw-r--r--.  1 root root 0 May 29 16:20 cpuset.sched_relax_domain_level
drwxr-xr-x.  2 root root 0 May 29 16:20 kubepods-besteffort.slice
drwxr-xr-x. 12 root root 0 May 29 16:27 kubepods-burstable.slice
-rw-r--r--.  1 root root 0 May 29 16:20 notify_on_release
-rw-r--r--.  1 root root 0 May 29 16:20 tasks
[root@worker0 kubepods.slice]#

Analogicznych sprawdzeń moglibyśmy tutaj dokonać np. dla pamięci lub przydzielanej przestrzeni dyskowej (a na maszynie workera, na którym działa dany pod zlokalizować konkretną ścieżkę, do której się odwołuje):

[root@worker0 kubepods.slice]# runc state 56b8b23c49907fb9e10e54fb64614d4e60ecfe84e4d6ec3d87421a21f98d3baa |grep rootfs
  "rootfs": "/var/lib/containers/storage/overlay/7290da8b7ec5f543280f4fc27865565646455a0749d02f5c91f4f068d266e7a6/merged",

Wykorzystując powyższą strukturę i układ OpenShift za pomocą swoich mechanizmów, narzuca limity na poziomie podów oraz kontenerów. Samo narzucanie tych limitów odbywa się przez mechanizmy limits i quota, które można oczywiście przez uprawnionych do tego użytkowników modyfikować. Domyślnie, to sam orkiestrator, czyli w naszym przypadku – Kubernetes, odpowiada za jak najbardziej optymalne rozmieszczenie podów pomiędzy węzłami, aby wspomniane parametry użycia pamięci, procesora itd. względem węzłów compute były jak najbardziej optymalne.

Jak widzimy, niezależnie od mechanizmów bezpieczeństwa wbudowanych w samą platformę oraz ułatwiających zarządzania przywilajami i użytkownikami w niej samej, do dyspozycji mamy także ogromne narzędzie do granulacji uprawnień wewnątrz kontenerów za pomocą mechanizmów container capabilities oraz seccomp.

Jak wspomnieliśmy na wstępie, mechanizmy możemy wręcz zaaplikować do kontenerów występujących poza samą platformą, stojących bezpośrednio na danym hoście.

Proszę zwrócić uwagę, że w powyższym opracowaniu, w ogóle nie skupiliśmy się na ograniczeniach, które mogą być kastomizowane za pomocą mechanizmów SELinux, którego omówienie, nawet w kontekście kontenerów zasługuje co najmniej na podobne, osobne opracowanie. Podobnie, nie wchodziliśmy w szczegóły specyficznych dla OpenShift reguł SCC.

Zobacz również

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

    Skontaktuj się z nami