Runtime detection and observability
Tetragon: credential file reads, the secure way
Every attacker who lands in a container does the same three things, and "cat the service account token" is one of them. It takes a second, leaves no trace in the app logs, and turns one compromised pod into an API client.
The short answer
Apply a Tetragon TracingPolicy on the security_file_permission hook with Prefix matches for /etc/shadow, SSH keys and the service account token path, limited to container PID namespaces and rate-limited. Hooking the LSM function covers every read system call at once. Alert on reads by unexpected binaries.
On this page
What goes wrong
Credentials sit in files: password hashes in /etc/shadow, SSH keys in
~/.ssh/, and in Kubernetes, a service account token mounted into almost
every pod at /var/run/secrets/kubernetes.io/serviceaccount/token.
Reading one of these files is normal for a few programs and suspicious for everything else. A web server has no reason to read its own service account token; a shell that an attacker opened in that web server very much does.
Syscall-based monitoring misses reads that use read, pread, readv,
sendfile, mmap or io_uring paths the rules did not list. Monitoring at
the security hook in the kernel catches all of them in one place.
What the docs say
Instead of monitoring every system call, we opt to hook into the security_file_permission hook, which is a common execution point for all the above system calls.
Source: Tetragon docs, Filename access
Selectors enable per-hook in-kernel BPF filtering and actions.
Source: Tetragon docs, Selectors
Filtering in the kernel is what makes this affordable: events for files you
did not list never leave the kernel. The docs' example watches all of
/etc/, which is far too noisy for alerting. Narrow the prefixes to the
files that hold secrets.
The secure configuration
Container reads of credential files:
# Reads of files that hold credentials, from inside containers.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: credential-file-reads
spec:
kprobes:
- call: "security_file_permission"
syscall: false
return: true
args:
- index: 0
type: "file" # the path of the file
- index: 1
type: "int" # 0x04 MAY_READ, 0x02 MAY_WRITE
returnArg:
index: 0
type: "int"
selectors:
- matchNamespaces:
- namespace: Pid
operator: NotIn
values: ["host_ns"]
matchArgs:
- index: 0
operator: "Prefix"
values:
- "/etc/shadow"
- "/etc/gshadow"
- "/etc/sudoers"
- "/root/.ssh/"
- "/var/run/secrets/kubernetes.io/serviceaccount/"
- "/run/secrets/kubernetes.io/serviceaccount/"
matchActions:
- action: Post
rateLimit: "1m"Node-level secrets, read by anything other than the expected control-plane binaries:
# Node-level secrets, including host processes. Paths are those of kubeadm-style nodes;
# check where your distribution keeps them, and which binaries read them, with tetra getevents first.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: host-credential-reads
spec:
kprobes:
- call: "security_file_permission"
syscall: false
args:
- index: 0
type: "file"
- index: 1
type: "int"
selectors:
- matchArgs:
- index: 0
operator: "Prefix"
values:
- "/etc/kubernetes/pki/"
- "/var/lib/kubelet/pki/"
matchBinaries:
- operator: "NotIn"
values:
- "/usr/bin/kubelet" # kubeadm DEB/RPM path
- "/usr/local/bin/kube-apiserver"
- "/usr/local/bin/etcd"kubectl apply -f credential-file-reads.yaml -f host-credential-reads.yamlThe kubelet path is the one the kubeadm packages install. The Kubernetes docs list the package contents:
Installs the
/usr/bin/kubeletbinary.
Source: Kubernetes docs, Configuring each kubelet in your cluster using kubeadm
Other installs put it elsewhere, so read the real path on a node
(readlink /proc/$(pgrep -x kubelet)/exe) before you trust the list.
The node-level policy is only useful if its events leave the node. The
Tetragon chart's default export deny list drops every event from the host and
from kube-system, so host reads of the node PKI never reach the JSON file,
even though tetra getevents shows them. Narrow it to
{"health_check":true} (see
kernel module and BPF loads).
Alert rules in your pipeline, for example: any read of the service account
token by a binary that is not the app's main binary; any read of
/etc/shadow from a pod; any read under the node PKI paths from a binary
outside the allow list.
Prove it
A root alpine:3.22 pod in namespace creds, with the default token mount.
The reads:
$ kubectl -n creds exec app -- sh -c "cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -c1-20"
eyJhbGciOiJSUzI1NiIs
$ kubectl -n creds exec app -- sh -c "cat /etc/shadow | head -1"
root:*::0:::::What Tetragon recorded (tetra getevents -o compact --namespace creds):
process creds/app /bin/cat /var/run/secrets/kubernetes.io/serviceaccount/token
read creds/app /bin/cat /run/secrets/kubernetes.io/serviceaccount/..2026_09_25_00_33_17.379378043/token
exit creds/app /bin/cat /var/run/secrets/kubernetes.io/serviceaccount/token 0
process creds/app /bin/cat /etc/shadow
read creds/app /bin/cat /etc/shadow
exit creds/app /bin/cat /etc/shadow 0Look at the token line. The command asked for /var/run/..., and the kernel
reports /run/secrets/.../..2026_09_25_00_33_17.379378043/token, the real
file behind two symlinks. In this image /var/run is a link:
$ kubectl -n creds exec app -- ls -la /var/run
lrwxrwxrwx 1 root root 6 Sep 17 17:34 /var/run -> ../runOnly the /run/secrets/kubernetes.io/serviceaccount/ prefix matched. A
policy with the /var/run/... prefix alone would have stayed silent.
The rate limit is per thread. Two head commands in a row are two processes,
so both reads were reported.
On the control-plane node, reads of node keys by a shell:
read lab-control-plane /usr/bin/cat /etc/kubernetes/pki/ca.key
read lab-control-plane /usr/bin/head /var/lib/kubelet/pki/kubelet.keyA limit of this lab: kind runs every node as a container on one kernel. There,
Tetragon reported the key reads of the running kube-apiserver (host PID
11781, whose /proc/11781/exe is /usr/local/bin/kube-apiserver) with the
binary /usr/lib/systemd/systemd. The process started before Tetragon did. So
kind cannot prove the matchBinaries allow list. On real nodes, check what
tetra getevents shows for your control-plane binaries before you rely on
the list.
Mistakes people make
Watching read() instead of the security hook
Attackers have many ways to read a file. The security hook sees them all; a list of syscalls always misses one.
Prefixes that are too wide
/etc/ or / produces thousands of events per second on a busy node. List
the files that hold secrets, and add paths as you learn.
Not accounting for symlinks
The service account token is a symlink, through ..data, into a timestamped
directory, and /var/run is often a link to /run. Match on directory
prefixes for both spellings, not on the exact file name.
An allow list copied from another install
matchBinaries compares full paths. /usr/local/bin/kubelet does not match
a kubelet installed at /usr/bin/kubelet, and every kubelet read of its own
keys becomes an alert. Noise like that gets the policy turned off. Take the
paths from your own nodes.
Detection without prevention
The best result is a pod that has no token to read. Set
automountServiceAccountToken: false where the app does not call the API, and
keep this policy for the pods that must have one.
Checklist
- A policy on
security_file_permissioncovers/etc/shadow, SSH keys and the service account token path. - The policy is limited to container PID namespaces and rate-limited.
- Node PKI and kubelet credential paths are watched with an allow list of binaries taken from your own nodes.
- Alerts fire on reads by binaries that are not the workload's own.
- Pods that do not need the API have
automountServiceAccountToken: false. - The policy is reviewed when new secret paths appear (new operators, new mounts).
A credential read is the quietest step of an attack and often the most valuable. Make the kernel tell you when it happens.
H2-CTDE
Learn it on a live range
Kernel-level detection with eBPF, in Runtime Detection and Response: a real host in your browser, and every objective checked on the machine.
Start freeThe Secure Way
More on runtime detection and observability
Tetragon, alerting as code, multi-tenant logs and knowing when a sensor goes quiet.
All runtime detection and observability guides