WARNING: REPO HAS BEEN RENAMED
We introduce the “Bill of Behavior” (BoB): a vendor-supplied profile detailing known benign runtime behaviors for software, designed to be distributed directly within OCI artifacts. Generated using eBPF, a BoB codifies expected syscalls, file access patterns, network communications, and capabilities. This empowers two things:
- for the supply chain at deploy-time (possibly in a staging env): we can use a detailed and highly specific such profile to verify an installer at client side to exclude tampering (see the npm incident from sept 8)
- for continuous anomaly detection at runtime: allowing end-users to infer malicious activity, to shrink their false positive noise and to have a vendor-supplied behavioural baseline, the generalized and more lightweight profile is used.
We foresee a massive scale benefit for the end-user, who does not have in-depth knowledge of the software by shifting authoring and maintaining custom security policies to the vendor, who knows their own software, has the test cases and can judge what part of the policies should be generalized.
Imagine a software vendor (like a pharmaceutical company) distills all their knowledge of their own testing into a standard file and ship it with each update
. Just like a Container Beipackzettel
🌡️📦📃🩻
<img
That means the user receives a secure default runtime profile. They can customize it, or directly apply it for runtime detection. And which each update of the software, get an uptodate runtimeprofile
Trademark: Bill of Behavior is a registered trademark by Constanze Roedig, all rights reserved
- Introduction
- Bill of Behavior (BoB) Overview
- A generalized ApplicationProfile
- FAQ
- Example Comparison: Seccomp vs BoB
- BoB as a Transport and Enforcement Mechanism
- Origin Story
- Understanding the Use Cases
- Try it Out in a Live Lab
- Demo: Deploy an Application
- Generate Traffic of Benign Behaviour
- Create a Repeatable Positive Test
- Generate Runtime Attack
- Generate Supply Chain Attack (WIP)
- License
DISCLAIMER:
The scripts are currently for rapid prototyping to iterate on a design the community will accept.
The regex is not stable—do not rely on AI for these profiles. If you want to use them for your software:
Run the scripts, then use your eyes to fix what the regex messed up. Once the code is stable, (and if there is expressed interest/acceptance/funding etc) I ll create proper tooling
🚨 New Design Kubescape 4.0 will support user-defined-profiles, here an example using the kubescape CRDs 🚨
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ApplicationProfile
metadata:
name: bob-application123
namespace: {{ .Release.Namespace }}
spec:
architectures:
- amd64
containers:
- capabilities: # KNOWN CAPABILITIES
- DAC_OVERRIDE
- SETGID
- SETUID
endpoints: # KNOWN NETWORK
- direction: inbound
endpoint: :8080/ping.php
headers:
Host: # User accessible Overrides
- {{ include "mywebapp.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
internal: false
methods:
- GET
execs: #KNOWN EXEC
- args:
- /usr/bin/dirname
- /var/lock/apache2
path: /usr/bin/dirname
- args:
- /bin/sh
- -c
- ping -c 4 172.16.0.2
path: /bin/sh
imageID: ghcr.io/k8sstormcenter/webapp@sha256:e323014ec9befb76bc551f8cc3bf158120150e2e277bae11844c2da6c56c0a2b #IMAGE HASH
opens: #KNOWN FILE OPENS
- flags:
- O_CLOEXEC
- O_DIRECTORY
- O_NONBLOCK
- O_RDONLY
path: /etc/apache2/* #globs that generalize UUIDs or well-known FS structures
- flags:
- O_CLOEXEC
- O_RDONLY
path: /etc/group
- flags:
- O_CLOEXEC
- O_RDONLY
path: /etc/ld.so.cache
rulePolicies: # SPECIFIC EXCEPTION RULES
R0001:
processAllowed:
- ping
- sh
R0002: {}
...
syscalls: # KNOWN SYSCALLS
- accept4
- access
- arch_prctl
- getegid
- geteuid
...
Q: Isnt this the same as SELINUX/APPARMOR profiles?
A: Just like eBPF extends the Kernel, the above Profile are a superset of (Lists of recorded activity like incl FileAccess, Execs, ImageHashes, NetworkEndpoints, SystemCalls and Capabilities) and can work real-time with user-defined profiles, but it doesnt require loading anything into the LSM. LSMs have a totally different life-cycle and granularity than applications.
THE MOST IMPORTANT DIFFERENCE is UX, granularity and timeing and this enables transferring it between systems and making it transparent to users
Seccomp is a well-established sandboxing mechanism that filters which syscalls are allowed from an application to be made to the kernel. Kubernetes uses it since version 1.19
For the KV-database redis
in its most popular Helm-Chart, we traced out the superset
of benign behavior across many k8s-versions/distros. In K8s, there is a RuntimeDefault
seccomp profile that is shipped by default, the SBoB allowlists 99 syscalls, meaning the resulting difference will be detected as anomaly.
Generally speaking, a BoB profile will have a lower number of syscalls than a seccomp profile. There are many discussions on the internet on how seccomp is difficult across architecture
An SBoB is a generalized and customizable Application Profile that alerts on anything not allowlisted
(The following summaries are output by the github workflow script that summarizes what the profile allows if each of the workloads is annotated with said SBoB profile) WARNING: those scripts may not be fully stable.
using the bitnami chart: For a DB, the user will need to supply network-ranges that are allowed.
Component | Container | Type | Capabilities | Net | Opens | Execs | Syscalls |
---|---|---|---|---|---|---|---|
rs-bob-redis-master | redis | container | DAC_OVERRIDE DAC_READ_SEARCH NET_ADMIN |
0 | 17 | 5 | 99 |
Applications of type DB are security sensitive, as they often store juicy content. The most interesting thing to anomaly detect is which outbound network
connections are happening (exfiltration attempts).
If you are interested in using eBPF to monitor querys, see this course how our friends from pixie achieve such observability.
Component | Container | Type | Capabilities | Net | Opens | Execs | Syscalls |
---|---|---|---|---|---|---|---|
rs-tetragon-operator | tetragon-operator | container | NET_ADMIN | 10 | 8 | 1 | 92 |
rs-tetragon | export-stdout | container | none | 0 | 5 | 2 | 84 |
rs-tetragon | tetragon | container | BPF DAC_OVERRIDE DAC_READ_SEARCH NET_ADMIN PERFMON SYSLOG SYS_ADMIN SYS_PTRACE |
0 | 49 | 1 | 131 |
More elaborate Comparison of the shrinking attack surface if using NO| FULL | BAU profiles for CNCF Pixie
Profile | Capabilities | Network | Opens (#) | Execs (#) | Allowed Syscalls (#) |
---|---|---|---|---|---|
Kubernetes Default (v1.33) | unconfined | CNI | unconfined | unconfined | 363 |
FULL:catalogoperator | CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN, SETGID, SETPCAP, SETUID, SYS_ADMIN |
1 | 23 | 3 | 96 |
FULL:catalogsource | CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN, SETGID, SETPCAP, SETUID, SYS_ADMIN |
0 | 19 | 2 | 106 |
BAU :catalogsource | CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN, SETGID, SETPCAP, SETUID, SYS_ADMIN |
0 | 57 | 2 | 94 |
FULL:certman | DAC_OVERRIDE, DAC_READ_SEARCH |
0 | 8 | 1 | 65 |
FULL:initjob | DAC_OVERRIDE, DAC_READ_SEARCH |
0 | 10 | 1 | 100 |
FULL:kelvin | DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN |
0 | 97 | 1 | 76 |
BAU :kelvin | none | 0 | 0 | 0 | 22 |
FULL:olmoperator | DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN |
1 | 8 | 1 | 63 |
FULL:pem | BPF, DAC_READ_SEARCH, IPC_LOCK, NET_ADMIN, PERFMON, SYS_ADMIN, SYS_PTRACE, SYSLOG |
0 | 390 | 1 | 122 |
BAU :pem | BPF, DAC_READ_SEARCH, IPC_LOCK, NET_ADMIN, PERFMON, SYS_ADMIN, SYS_PTRACE |
0 | 197 | 0 | 44 |
FULL:pletcd | NET_ADMIN, NET_RAW, SETGID, SETPCAP, SETUID, SYS_ADMIN |
0 | 39 | 10 | 92 |
BAU :pletcd | SETGID, SETPCAP, SETUID, SYS_ADMIN |
0 | 37 | 2 | 53 |
FULL:plnats | DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN |
3 | 11 | 1 | 57 |
BAU :plnats | none | 1 | 1 | 0 | 19 |
FULL:querybroker | DAC_OVERRIDE, DAC_READ_SEARCH, FOWNER, NET_ADMIN |
0 | 20 | 1 | 107 |
BAU :querybroker | DAC_OVERRIDE, FOWNER |
0 | 0 | 0 | 22 |
FULL:viziercloudconnector | DAC_OVERRIDE, DAC_READ_SEARCH, NET_ADMIN |
0 | 18 | 1 | 70 |
BAU :viziercloudconnector | none | 0 | 3 | 0 | 29 |
FULL:viziermeta | NET_ADMIN | 0 | 16 | 1 | 65 |
BAU :viziermeta | none | 0 | 3 | 0 | 24 |
FULL:vizieroperator | NET_ADMIN | 1 | 13 | 1 | 104 |
BAU :vizieroperator | none | 1 | 2 | 0 | 27 |
Additionally to syscalls: a BoB contains fileopens, execs, capabilities and network endpoints/methods. This is reminiscent of Apparmour profiles and network-policies. It is theoretically possible to convert a BoB into an Apparmour profile, a seccomp profile and a set of network-policies.
Which way the community will go in terms of using the information contained in a bob, such that it can be enforced at runtime, remains to be seen.
A BoB, is first of all a vehicle
to transport the information of the runtime behavior. And only secondly, the runtime-anomaly generation method. The fact, that kubescape can rather straightforwardly consume them is a huge plus.
.. over coffee at London KubeCon EU 2025
A Bill of Behaviour helps us detect unexpected or malicious activity. We'll focus on two primary scenarios:
This scenario covers situations where a CVE is present in the app, or it gets exploited.
This scenario covers threats originating from a compromised supply chain. For example:
- The artefact is not the one from the vendor.
- The vendor's supply chain got compromised.
- A typosquatting attack has occurred.
- The artefact contains a beacon, a backdoor, a cryptominer, or something else malicious.
give us feedback , report issues , raise PRs (contributing guidelines will follow)
https://labs.iximiuz.com/courses/bill-of-behaviour-c070da3a
License is Apache 2.0
Using a well-known demo
** app, we deploy a ping utility called webapp
that has:
- a) Desired functionality: it pings things.
- b) Undesired functionality: it is vulnerable to injection (runtime is compromised).
- This is to mimic a CVE in your app.
- c) Tampering with the artefact: In module 2, we will additionally tamper with the artifact and make it create a backdoor (supply chain is compromised).
- This is to mimic a SupplyChain corruption between vendor and you.
cd ~/bobctl
git checkout https://github.com/k8sstormcenter/bobctl
# maybe you need storage, then `make storage`
make kubescape
sleep 30 # TODO proper wait command goes here
make helm-install
Benign (adjective) [bi-ˈnīn]
- Benignity (noun) [bi-ˈnig-nə-tē]
- Benignly (adverb) [bi-ˈnīn-lē]
Definitions/SYNONYMS:
- Of a mild type or character that does not threaten health or life. HARMLESS.
- Of a gentle disposition: GRACIOUS.
- Showing kindness and gentleness. FAVORABLE, WHOLESOME.
In shell 1:
kubectl logs -n honey -l app=node-agent -c node-agent -f
In shell 2
make fwd
curl localhost:8080/ping.php?ip=172.16.0.2
This will be recorded into the above profile reflected by the stanza:
endpoint:
- direction: inbound
endpoint: :8080/ping.php
headers:
Host:
- localhost:8080 # vendor needs to template possible benign endpoints e.g via k8s-dns
internal: false
methods:
- GET
exec:
...
- args:
- /bin/sh
- -c
- ping -c 4 172.16.0.2 #vendor needs to template possible benign IP CIDR
path: /bin/sh
Vendor
Encode the benign
behavior into a test, like a helm hook
apiVersion: v1
kind: Pod
metadata:
labels:
{{- include "mywebapp.labels" . | nindent 4 }}
kubescape.io/ignore: "true"
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded,hook-failed
spec:
containers:
- image: curlimages/curl:8.7.1
command:
URL=\"${SERVICE}.${NAMESPACE}.svc.cluster.local:${PORT}/ping.php?ip=${TARGET_IP}\"
RESPONSE=$(curl -s \"$URL\")
echo \"$RESPONSE\"
echo \"$RESPONSE\" | grep -q \"Ping results for ${TARGET_IP}\"
echo \"$RESPONSE\" | grep -q \"${TARGET_IP} ping statistics\"
User If I now pull the helm-chart and execute helm test -> I can test that I do not see any anomalies.
make helm-test
kubectl logs -n honey -l app=node-agent -c node-agent -f | grep "Unexpected" # You should not see anything
*** Known exceptions: There are expected syscall deviations, those are small. Making those transparent to the user is WIP. Currently you need a superset Bob
In shell 1:
kubectl logs -n honey -l app=node-agent -c node-agent -f
In shell 2
make attack
{"BaseRuntimeMetadata":{"alertName":"Unexpected process launched","arguments":{"args":["/bin/ls"],"exec":"/bin/ls","retval":0},"infectedPID":6972,"severity":5,"size":"4.1 kB","timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected file access","arguments":{"flags":["O_RDONLY","O_NONBLOCK","O_DIRECTORY","O_CLOEXEC"],"path":"/var/www/html"},"infectedPID":6972,"severity":1,"timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected system call","arguments":{"syscall":"sendmmsg"},"infectedPID":15899,"md5Hash":"4e79f11b07df8f72e945e0e3b3587177","sha1Hash":"b361a04dcb3086d0ecf960dbb6e0f6f1d9b7ebf3b"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected system call","arguments":{"syscall":"socketpair"},"infectedPID":15899,"md5Hash":"4e79f11b07df8f72e945e0e3b3587177","sha1Hash":"b361a04dcb3086d0ecf960dbb6e0f6f1d9b7ebf3b"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected domain request","arguments":{"addresses":["216.58.210.174"],"domain":"google.com.","port":50015,"protocol":"UDP"},"infectedPID":20611,"severity":5,"size":"4.1 kB","timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected system call","arguments":{"syscall":"getpeername"},"infectedPID":15899,"md5Hash":"4e79f11b07df8f72e945e0e3b3587177","sha1Hash":"b361a04dcb3086d0ecf960dbb6e0f6f1d9b7ebf3b"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected process launched","arguments":{"args":["/usr/bin/curl","google.com"],"exec":"/usr/bin/curl","retval":0},"infectedPID":20611,"severity":5,"size":"4.1 kB","timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected Service Account Token Access","arguments":{"flags":["O_RDONLY"],"path":"/run/secrets/kubernetes.io/serviceaccount/..2025_07_07_21_05_56.676258237/token"},"infectedPID":20611,"severity":5,"timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected file access","arguments":{"flags":["O_RDONLY"],"path":"/run/secrets/kubernetes.io/serviceaccount/..2025_07_07_21_05_56.676258237/token"},"infectedPID":20611,"severity":5,"timestamp":"2025-05-22T17:16:24Z"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected system call","arguments":{"syscall":"fadvise64"},"infectedPID":15899,"md5Hash":"4e79f11b07df8f72e945e0e3b3587177","sha1Hash":"b361a04dcb3086d0ecf960dbb6e0f6f1d9b7ebf3b"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected file access","arguments":{"flags":["O_RDONLY"],"path":"/var/www/html/index.html"},"infectedPID":20581,"severity":1,"timestamp":"2025-07-07T21:22:04"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected file access","arguments":{"flags":["O_RDONLY","O_NONBLOCK","O_DIRECTORY","O_CLOEXEC"],"path":"/var/www/html"},"infectedPID":20503,"severity":1,"timestamp":"2025-07-07T21:21:53"}}
{"BaseRuntimeMetadata":{"alertName":"Unexpected process launched","arguments":{"args":["/bin/cat","/proc/self/mounts"],"exec":"/bin/cat","retval":0},"infectedPID":20527,"severity":5,"size":"4.1 kB","timestamp":"2025-07-07T21:22:08"}}
WIP