Strace
sert à intercepter et loguer les appels systèmes (kill, reboot...) fait par un processus. Strace
est souvent installé par défaut.
On peut l'utiliser avec la commande ls
pour voir quels sont les appels systèmes effectués :
Antoine@cks-worker:~$ strace ls
execve("/bin/ls", ["ls"], 0x7fffc3e224b0 /* 21 vars */) = 0
brk(NULL) = 0x56393ce80000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=24326, ...}) = 0
mmap(NULL, 24326, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fb50f0b1000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20b\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=154832, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb50f0af000
...
Chaque appel système a son utilité. Ceci peut-être retrouvé directement dans le man
(https://man7.org/linux/man-pages/man2/syscalls.2.html).
/proc
contient les informations des processus. Les fichiers/dossiers dans /proc
ne persistent pas, ils sont temporaires au processus.
Voyons les appels systèmes d'un processus (ETCD) :
root@cks-master:~$ ps aux | grep etcd
root 2782 2.1 1.3 11214524 53760 ? Ssl 19:23 0:13 etcd --advertise-client-urls=https://10.156.0.2:2379 --
...
root@cks-master:~$ strace -p 2782 -f -cw
strace: Process 2782 attached with 9 threads
...
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
77.34 22.169472 19211 1154 250 futex
20.45 5.861951 4323 1356 epoll_pwait
1.96 0.562066 411 1368 nanosleep
0.12 0.032994 1222 27 fdatasync
0.07 0.019600 88 222 write
0.03 0.007361 44 168 79 read
0.02 0.005707 5707 1 restart_syscall
0.00 0.001184 31 38 pwrite64
0.00 0.000672 61 11 tgkill
0.00 0.000541 49 11 lseek
0.00 0.000476 34 14 10 epoll_ctl
0.00 0.000388 35 11 getpid
0.00 0.000373 34 11 1 rt_sigreturn
0.00 0.000286 57 5 openat
0.00 0.000276 25 11 setsockopt
0.00 0.000210 30 7 close
0.00 0.000201 50 4 2 accept4
0.00 0.000180 30 6 getdents64
0.00 0.000106 21 5 getrandom
0.00 0.000047 24 2 getsockname
0.00 0.000035 18 2 fstat
0.00 0.000022 22 1 sched_yield
------ ----------- ----------- --------- --------- ----------------
100.00 28.664148 4435 342 total
-cw
nous permet d'avoir une sortie sous forme de tableau.
-f
permet de suivre en temps réel.
-p
permet de préciser un pid.
On peut retrouver le pid 2782
dans /proc
:
root@cks-master:/proc/2782$ ls
arch_status cmdline exe loginuid mountstats oom_score_adj sched stack timers
attr comm fd map_files net pagemap schedstat stat timerslack_ns
autogroup coredump_filter fdinfo maps ns patch_state sessionid statm uid_map
auxv cpuset gid_map mem numa_maps personality setgroups status wchan
cgroup cwd io mountinfo oom_adj projid_map smaps syscall
clear_refs environ limits mounts oom_score root smaps_rollup task
root@cks-master:/proc/2782$ ls -lh exe
lrwxrwxrwx 1 root root 0 Jul 6 19:24 exe -> /usr/local/bin/etcd
A partir de là nous pouvons trouver des informations compromettantes (valeurs des secrets si ETCD n'est pas chiffré par ex).
Pour faire la pratique suivante, ETCD ne doit pas chiffrer les flux.
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: apache
name: apache
spec:
containers:
- image: httpd
name: apache
resources: {}
env:
- name: SECRET
value: "mysecretword"
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
On va maintenant essayer de retrouver cette variable via /proc
:
root@cks-worker:/home/Antoine$ ps aux | grep httpd
root 5749 0.0 0.1 5932 4564 ? Ss 19:44 0:00 httpd -DFOREGROUND
www-data 5777 0.0 0.0 1210608 3380 ? Sl 19:44 0:00 httpd -DFOREGROUND
www-data 5778 0.0 0.0 1210608 3380 ? Sl 19:44 0:00 httpd -DFOREGROUND
www-data 5779 0.0 0.0 1210608 3380 ? Sl 19:44 0:00 httpd -DFOREGROUND
root 6035 0.0 0.0 14856 1108 pts/0 R+ 19:49 0:00 grep --color=auto httpd
root@cks-worker:/home/Antoine$ pstree -p
├─containerd-shim(5655)─┬─httpd(5749)─┬─httpd(5777)─┬─{httpd}(5783)
│ │ │ ├─{httpd}(5784)
│ │ │ ├─{httpd}(5785)
│ │ │ ├─{httpd}(5786)
│ │ │ ├─{httpd}(5787)
root@cks-worker:/proc/5749$ ls
arch_status cgroup coredump_filter exe io maps mountstats oom_adj patch_state sched smaps statm timers
attr clear_refs cpuset fd limits mem net oom_score personality schedstat smaps_rollup status timerslack_ns
autogroup cmdline cwd fdinfo loginuid mountinfo ns oom_score_adj projid_map sessionid stack syscall uid_map
auxv comm environ gid_map map_files mounts numa_maps pagemap root setgroups stat task wchan
root@cks-worker:/proc/5749$ ls -lh exe
lrwxrwxrwx 1 root root 0 Jul 6 19:44 exe -> /usr/local/apache2/bin/httpd
root@cks-worker:/proc/5749$ cat environ
HTTPD_VERSION=2.4.54KUBERNETES_SERVICE_PORT=443KUBERNETES_PORT=tcp://10.96.0.1:443HOSTNAME=apacheHOME=/rootHTTPD_PATCHES=KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binKUBERNETES_PORT_443_TCP_PORT=443HTTPD_SHA256=eb397feeefccaf254f8d45de3768d9d68e8e73851c49afd5b7176d1ecf80c340KUBERNETES_PORT_443_TCP_PROTO=tcpSECRET=mysecretwordHTTPD_PREFIX=/usr/local/apache2KUBERNETES_SERVICE_PORT_HTTPS=443KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443KUBERNETES_SERVICE_HOST=10.96.0.1PWD=/usr/local/apache2
On peut voir que l'on a accès aux variables d'env du container httpd. On voit donc notre variable SECRET=mysecretpassword
Falco
est un outil de sécurité CNCF. Il permet de faire du tracing sur le kernel, de détecter des comportements non souhaités et pouvoir réagir à une violation de sécurité.
Installation :
curl -s https://falco.org/repo/falcosecurity-3672BA8F.asc | apt-key add -
echo "deb https://download.falco.org/packages/deb stable main" | tee -a /etc/apt/sources.list.d/falcosecurity.list
apt-get update -y
apt-get -y install linux-headers-$(uname -r)
apt-get install -y falco
systemctl start falco
systemctl enable falco
root@cks-worker:~$ tail -f /var/log/syslog | grep falco
Jul 6 19:59:24 cks-worker kernel: [441215.350605] falco: loading out-of-tree module taints kernel.
Jul 6 19:59:24 cks-worker kernel: [441215.351070] falco: module verification failed: signature and/or required key missing - tainting kernel
Jul 6 19:59:24 cks-worker kernel: [441215.351640] falco: driver loading, falco 39ae7d40496793cf3d3e7890c9bbdc202263836b
Jul 6 19:59:24 cks-worker falco: Falco version 0.32.0 (driver version 39ae7d40496793cf3d3e7890c9bbdc202263836b)
Jul 6 19:59:24 cks-worker falco: Falco initialized with configuration file /etc/falco/falco.yaml
Jul 6 19:59:24 cks-worker falco[14766]: Wed Jul 6 19:59:24 2022: Falco version 0.32.0 (driver version 39ae7d40496793cf3d3e7890c9bbdc202263836b)
Jul 6 19:59:24 cks-worker falco[14766]: Wed Jul 6 19:59:24 2022: Falco initialized with configuration file /etc/falco/falco.yaml
Jul 6 19:59:24 cks-worker falco[14766]: Wed Jul 6 19:59:24 2022: Loading rules from file /etc/falco/falco_rules.yaml:
Jul 6 19:59:24 cks-worker falco: Loading rules from file /etc/falco/falco_rules.yaml:
Jul 6 19:59:24 cks-worker falco: Loading rules from file /etc/falco/falco_rules.local.yaml:
...
Nous allons utiliser falco afin de trouver des processus malicieux dans les containers :
tail -f /var/log/syslog | grep falco
root@cks-master:~$ k exec apache -ti -- bash
root@apache:/usr/local/apache2#
root@apache:/etc# echo user >> /etc/passwd
root@apache:/etc# apt-get update
root@cks-worker:~$ tail -f /var/log/syslog | grep falco
Jul 6 20:02:44 cks-worker falco: 20:02:44.843418038: Notice A shell was spawned in a container with an attached terminal (user=<NA> user_loginuid=-1 apache (id=9f21e9975f90) shell=bash parent=runc cmdline=bash terminal=34816 container_id=9f21e9975f90 image=docker.io/library/httpd)
Jul 6 20:05:21 cks-worker falco: 20:05:21.684258527: Error File below /etc opened for writing (user=<NA> user_loginuid=-1 command=bash parent=<NA> pcmdline=<NA> file=/etc/passwd program=bash gparent=<NA> ggparent=<NA> gggparent=<NA> container_id=9f21e9975f90 image=docker.io/library/httpd)
Jul 6 20:06:26 cks-worker falco: 20:06:26.652272740: Error Package management process launched in container (user=<NA> user_loginuid=-1 command=apt update container_id=9f21e9975f90 container_name=apache image=docker.io/library/httpd:latest)
On voit que chaque commande a entrainé des erreurs.
Par défaut nous avons déjà des règles qui nous permettent de remonter ces erreurs.
Ces règles se trouvent dans /etc/falco/falco_rules.yaml
et k8s_audit_rules.yaml
et se présentent de la manière suivante :
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec point into a container with an attached terminal.
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
and not user_expected_terminal_shell_in_container_conditions
output: >
A shell was spawned in a container with an attached terminal (user=%user.name user_loginuid=%user.loginuid %container.info
shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id image=%container.image.repository)
priority: NOTICE
tags: [container, shell, mitre_execution]
Afin de modifier une règle existante, on va copier la règle que nous voulons modifier et on va la mettre dans le fichier falco_rules.local.yaml
.
Il est possible de nous demander de modifier l'output ainsi que le niveau de criticité d'une règle pendant le CKS. La doc de Falco est autorisée, voir ici : https://falco.org/docs/rules/supported-fields/
Un container immutable est un container qui ne pourra pas être modifié pendant son fonctionnement.
Il existe différent moyen de renforcer la sécurité d'un container :
/bin
Nous allons maintenant créer une StartupProbe
afin d'enlever touch
du container :
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: immutable
name: immutable
spec:
containers:
- image: httpd
name: immutable
resources: {}
startupProbe:
exec:
command:
- rm
- /bin/touch
initialDelaySeconds: 1
periodSeconds: 5
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
En essayant de créer un fichier dans le container :
root@cks-master:~# k exec -it immutable -- bash
root@immutable:/usr/local/apache2# touch test
bash: touch: command not found
Nous allons maintenant rendre ce container read_only avec un accès en écriture sur un dossier (/usr/local/apache2/logs) grace à emptyDir
:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: immutable
name: immutable
spec:
containers:
- image: httpd
name: immutable
resources: {}
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /usr/local/apache2/logs
name: apache-logs
volumes:
- name: apache-logs
emptyDir: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
En créant cet emptyDir
on permet à l'application d'écrire dans ce dossier.
Chaque requête vers l'API (création de pod, secrets...) Kubernetes peut être logué vers les logs d'audit.
Chaque requête peut être traitée différemment :
RequestReceived
: L'étape pour les événements générés dès que le gestionnaire d'audit reçoit la demande, et avant qu'elle ne soit déléguée en bas de la chaîne des gestionnaires.ResponseStarted
: Une fois que les en-têtes de réponse sont envoyés, mais avant que le corps de la réponse ne soit envoyé. Cette étape n'est générée que pour les demandes de longue durée (par exemple, commande watch).ResponseComplete
: Le corps de la réponse (body) a été complété et aucun autre octet ne sera envoyé.Panic
: Événements générés lorsqu'une panic
se produit.On peut mettre en place des règle d'audit :
None
: ne pas enregistrer les événements qui correspondent à cette règle.Métadonnées
: enregistre les métadonnées de la demande (utilisateur demandeur, horodatage, ressource, verbe, etc.) mais pas le corps de la demande ou de la réponse.Request
: enregistrer les métadonnées de l'événement et le corps de la demande mais pas le corps de la réponse. Ceci ne s'applique pas aux demandes de non-ressources.RequestResponse
métadonnées de l'événement de journal, corps de la demande et de la réponse. Ceci ne s'applique pas aux demandes de non-ressources.Nous allons maintenant activer l'auditing pour l'API Kubernetes :
root@cks-master:/home/antoine$ cd /etc/kubernetes/
root@cks-master:/etc/kubernetes$ mkdir audit
policy.yaml
:apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
kube-apiserver.yaml
en y ajoutant : - --audit-policy-file=/etc/kubernetes/audit/policy.yaml # add
- --audit-log-path=/etc/kubernetes/audit/logs/audit.log # add
- --audit-log-maxsize=500 # add
- --audit-log-maxbackup=5 # add
volumeMounts:
- mountPath: /etc/kubernetes/audit # add
name: audit # add
volumes:
- hostPath: # add
path: /etc/kubernetes/audit # add
type: DirectoryOrCreate # add
name: audit # add
Créer le dossier logs
et le fichier audit.log
Tout est fonctionnel :
root@cks-master:/etc/kubernetes/audit/logs$ tail -2 audit.log
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"062246bf-f504-430b-ae93-445485025845","stage":"RequestReceived","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["10.156.0.2"],"userAgent":"kube-probe/1.23","requestReceivedTimestamp":"2022-07-07T15:10:08.795829Z","stageTimestamp":"2022-07-07T15:10:08.795829Z"}
{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Metadata","auditID":"062246bf-f504-430b-ae93-445485025845","stage":"ResponseComplete","requestURI":"/readyz","verb":"get","user":{"username":"system:anonymous","groups":["system:unauthenticated"]},"sourceIPs":["10.156.0.2"],"userAgent":"kube-probe/1.23","responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2022-07-07T15:10:08.795829Z","stageTimestamp":"2022-07-07T15:10:08.797677Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"system:public-info-viewer\" of ClusterRole \"system:public-info-viewer\" to Group \"system:unauthenticated\""}}
On va maintenant créer un Secret
et voir le log d'audit que cela a généré :
root@cks-master:/etc/kubernetes/audit/logs$ k create secret generic super-secret --from-literal=password=1234
secret/super-secret created
root@cks-master:/etc/kubernetes/audit/logs$ cat audit.log | grep super-secret | jq
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "Metadata",
"auditID": "f4665eea-19d1-438a-9d67-c9ad9542b6bd",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/default/secrets?fieldManager=kubectl-create",
"verb": "create",
"user": {
"username": "kubernetes-admin",
"groups": [
"system:masters",
"system:authenticated"
]
},
"sourceIPs": [
"10.156.0.2"
],
"userAgent": "kubectl/v1.23.4 (linux/amd64) kubernetes/e6c093d",
"objectRef": {
"resource": "secrets",
"namespace": "default",
"name": "super-secret",
"apiVersion": "v1"
},
"responseStatus": {
"metadata": {},
"code": 201
},
"requestReceivedTimestamp": "2022-07-07T15:12:06.478667Z",
"stageTimestamp": "2022-07-07T15:12:06.485364Z",
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": ""
}
}
Actuellement la règle d'audit ne filtre pas, la règle audite tout au level metadata
nous allons modifier la règle pour qu'elle soit moins verbeuse :
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived" # On ne veut rien de ce niveau
rules:
# log no "read" actions
- level: None
verbs: ["get", "watch", "list"]
# log nothing regarding events
- level: None
resources:
- group: "" # core
resources: ["events"]
# log nothing coming from some groups
- level: None
userGroups: ["system:nodes"]
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# for everything else log metadata
- level: Metadata