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.