The agent suggests the client's SBOB — you sign it
Suggest, prove, sign, bind
While the SBOB itself is the simplest possible runtime profile that captures contrast between benign and malicious behavior, and thus needs several iterations of tuning, node-agent can discover the extensions.
The most common case is that an SBOB contains the topology of a deployments implementation details. However, when you connect network to it, you need a way to compose the elements.
Features in this module are not GA.
0. One terminal for logs
Open a second tab logs, run this once and leave it:
kubectl -n honey logs ds/node-agent -c node-agent --all-pods=true -f | sed -u 's/^\[[^]]*\] //' | jq -Rc --unbuffered 'fromjson? | if .RuleID then {t:.time, rule:.RuleID, alert:.BaseRuntimeMetadata.alertName, ns:.RuntimeK8sDetails.namespace, wl:.RuntimeK8sDetails.workloadName, msg:.message} else {t:.ts, level, msg} end'
ready when you see this — the suggester line and every tracer on every node
{"t":"…","level":"info","msg":"suggester enabled"}
{"t":"…","level":"info","msg":"Started tracer"}
{"t":"…","level":"info","msg":"ContainerWatcher started successfully"}
{"t":".","level":"info","msg":"RogueArtifact writer wired: discoverable signoff state enabled"}
{"t":".","level":"info","msg":"queue processor started"}
{"t":".","level":"info","msg":"Successfully verified object signature"}
{"t":".","level":"info","msg":"signed bundle overlays enabled in alert mode"}
{"t":".","level":"warn","msg":"signed bundle overlays anchored to the PUBLISHED DEMO root key: this authenticates nothing, mount a real root before relying on signatures"}
{"t":".","level":"warn","msg":"rule signing DISABLED: the trust policy in force carries no ruleClasses; ANY Rules object in ANY namespace will load without a signature check"}
{"t":".","level":"info","msg":"trust policy in force"}
1. The vendor ships redis with a signed SBOB — no client is known yet

_redis with SBOB signed by vendor _
cd ~/bob/example/redis
export CP=containerprofiles.spdx.softwarecomposition.kubescape.io
kubectl create ns redis
kubectl apply -f vendor/redis-base-signed.yaml
helm install redis oci://registry-1.docker.io/bitnamicharts/redis --version 27.0.18 -n redis --set architecture=standalone --set auth.enabled=false --set image.repository=bitnami/redis --set image.digest=sha256:08863c2c3f4e051fb6139b38fa223e9c13be5033326a59bead182860d899bf98 \
--set 'master.podLabels.kubescape\.io/user-defined-profile=redis'
logs
{"t":".","level":"warn","msg":"signed fragment stored spec diverges from the signed content: the stored spec is display-only and is NOT enforced; enforcement uses the signed content"}
{"t":".","level":"info","msg":"adopted user-authored ContainerProfile as authoritative base"}
{"t":"…","level":"info","msg":"container has a user defined profile"}
{"t":"…","level":"info","msg":"Successfully verified object signature"}
The reason for the tamper alert is that during the first tick, the operator hasnt reconciled
the vendor's SBOB, abridged — 15 execs · 79 opens · 8 capabilities · no ingress
kind: ContainerProfile
metadata:
name: redis
namespace: redis
annotations:
kubescape.io/managed-by: User
signature.kubescape.io/identity: local-key
signature.kubescape.io/issuer: local
signature.kubescape.io/timestamp: "1790366987"
signature.kubescape.io/signature: MEUCIQDd5Ba9Hg2iSh2OAO8w…
signature.kubescape.io/certificate: LS0tLS1CRUdJTiBDRVJUSUZJ…
signature.kubescape.io/content: H4sIAAAAAAAA/7xZ73LiOBJ/…
spec:
matchLabels: {app.kubernetes.io/component: master, app.kubernetes.io/instance: redis, app.kubernetes.io/name: redis}
capabilities: [CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_NET_ADMIN, CAP_SETGID, CAP_SETUID, CAP_SYS_ADMIN, …]
execs:
- {args: [/opt/bitnami/redis/bin/redis-cli, -h, localhost, -p, "6379", ping], path: /opt/bitnami/redis/bin/redis-cli}
- {args: [/opt/bitnami/redis/bin/redis-server, ⋯⋯], path: /opt/bitnami/redis/bin/redis-server}
- {args: [/health/ping_liveness_local.sh, ⋯⋯], path: /usr/bin/bash}
- …
opens:
- {flags: [O_RDONLY], path: /data}
- {flags: [O_APPEND, O_CREAT, O_WRONLY], path: /data/appendonlydir/appendonly.aof.1.incr.aof}
- …
ingress: null
2. A client appears — unprotected

no profile, no trust: the client is a rogue artefact
sed '/kubescape.io\/user-defined-profile/d' client.yaml | kubectl apply -f -
kubectl -n redis rollout status deploy/redis-client --timeout=120s
Alerts R1017 Rogue artefact, R0012 Unexpected Ingress Network Traffic on redis for every connection, R0040 Unexpected process arguments
{"t":"…","rule":"R1017","alert":"Rogue artefact","ns":"redis","wl":"redis-client","msg":"Container 'client' in namespace 'redis' has no bound profile — an unmanaged/unknown container is running in the cluster"}
{"t":"…","rule":"R0012","alert":"Unexpected Ingress Network Traffic","ns":"redis","wl":"redis-master","msg":"Unexpected ingress network communication from: 10.42.0.49:6379 using TCP to: redis"}
{"t":"…","rule":"R0040","alert":"Unexpected process arguments","ns":"redis","wl":"redis-client","msg":"Unexpected process arguments: redis-cli with PID 76287"}
3. The agent suggests a profile (~2 min)
watch -n5 "kubectl -n redis get $CP -l kubescape.io/profile-role=suggestion"
kubectl -n redis get events --field-selector reason=SBoBSuggested -o custom-columns=POD:.involvedObject.name,MSG:.message
discovered vs. portable
125 execs · 13 opens · 63 syscalls · 2 egress
spec:
execs:
- {path: /bin/busybox, args: [/bin/sh, -c, "i=0\nwhile true; do\n redis-cli -h redis-master -p 6379 SET \"k:$i\" \"v:$i\" >/dev/null\n i=$((i+1)); sleep 2\ndone\n"]}
- {path: /bin/busybox, args: [sleep, "2"]}
- {path: /pause, args: [/pause]}
- {path: /usr/local/bin/redis-cli, args: [/usr/local/bin/redis-cli, -h, redis-master, -p, "6379", SET, k:32, v:32]}
- {path: /usr/local/bin/redis-cli, args: [/usr/local/bin/redis-cli, -h, redis-master, -p, "6379", SET, k:33, v:33]}
- {path: /usr/local/bin/redis-cli, args: [/usr/local/bin/redis-cli, -h, redis-master, -p, "6379", SET, k:34, v:34]}
- … 119 more redis-cli lines, one per SET
opens:
- {path: /etc/resolv.conf, flags: [O_CLOEXEC, O_LARGEFILE, O_RDONLY]}
- {path: /etc/hosts, flags: [O_CLOEXEC, O_LARGEFILE, O_RDONLY]}
- {path: /lib/libssl.so.3, flags: [O_CLOEXEC, O_LARGEFILE, O_RDONLY]}
- {path: /proc/sys/vm/overcommit_memory, flags: [O_CLOEXEC, O_RDONLY]}
- … 9 more
syscalls: [accept4, bind, brk, clone, close, connect, dup2, execve, exit_group, fcntl, getpid, ioctl, … 51 more]
egress:
- identifier: f8ed3623…
type: internal
serviceRefName: redis-master
serviceRefNamespace: redis
podSelector:
matchLabels: {app.kubernetes.io/component: master, app.kubernetes.io/instance: redis, app.kubernetes.io/name: redis}
ports: [{name: TCP-6379, port: 6379, protocol: TCP}]
- identifier: f14f2fcf…
type: internal
serviceRefName: kube-dns
serviceRefNamespace: kube-system
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
ports: [{name: UDP-53, port: 53, protocol: UDP}]
4. You sign it with your key and bind it
no patience? the suggested profile, as a file
yolo/client-sbob.json is exactly what the agent suggests for this client. Use it instead of the first three lines below:
cp yolo/client-sbob.json client-sbob.json
~/sign-object generate-keypair --output operator.pem
N=$(kubectl -n redis get $CP -l kubescape.io/profile-role=suggestion -o jsonpath='{.items[0].metadata.name}')
kubectl -n redis get $CP "$N" -o json | python3 -c '
import sys,json
d=json.load(sys.stdin)
d["metadata"]={"name":"redis-client","namespace":"redis","annotations":{"kubescape.io/managed-by":"User"}}
json.dump(d,sys.stdout)' > client-sbob.json
~/bobctl validate --file client-sbob.json
~/sign-object sign --file client-sbob.json --key operator.pem --output client-sbob-signed.yaml
kubectl apply -f client-sbob-signed.yaml
kubectl -n redis patch deploy redis-client -p '{"spec":{"template":{"metadata":{"labels":{"kubescape.io/user-defined-profile":"redis-client"}}}}}'
kubectl -n redis rollout status deploy/redis-client --timeout=120s
logs
{"t":"…","level":"info","msg":"Successfully verified object signature"}
{"t":"…","level":"info","msg":"adopted user-authored ContainerProfile as authoritative base"}
client-sbob-signed.yaml, abridged
kind: ContainerProfile
metadata:
name: redis-client
namespace: redis
annotations:
kubescape.io/managed-by: User
signature.kubescape.io/identity: local-key
signature.kubescape.io/issuer: local
signature.kubescape.io/signature: MEUCIQClx5bZ…
signature.kubescape.io/content: H4sIAAAAAAAA/7RW…
spec:
architectures: [amd64]
matchLabels: {app: redis-client}
execs:
- {args: [/bin/sh, ⋯⋯], path: /bin/busybox}
- {args: [/usr/local/bin/redis-cli, -h, redis-master, -p, "6379", SET, ⋯, ⋯], path: /usr/local/bin/redis-cli}
- …
egress:
- {serviceRefName: redis-master, serviceRefNamespace: redis, ports: [{name: TCP-6379, port: 6379, protocol: TCP}], …}
- {serviceRefName: kube-dns, serviceRefNamespace: kube-system, ports: [{name: UDP-53, port: 53, protocol: UDP}], …}
5. The operator side: derive the ingress overlay, re-sign redis
no patience? the healed redis profile, as a file
yolo/redis-healed.json is the redis SBOB with the client ingress already merged (it also admits unit 2's web-client). Sign and apply it instead of running overlay.sh:
~/sign-object sign --file yolo/redis-healed.json --key operator.pem --output redis-healed-signed.yaml
kubectl apply -f redis-healed-signed.yaml
The client's suggestion says where it talks to (serviceRefName: redis-master). The vendor shipped redis without knowing this client; allowing it is the operator's decision, signed with the operator key.
~/overlay.sh operator.pem redis "$(kubectl -n redis get $CP -l kubescape.io/profile-role=suggestion -o jsonpath='{.items[0].metadata.name}')"
overlay for redis/redis: +ingress redis-client from redis
2026-09-25T19:44:15Z healed redis/redis (ingress overlay merged, re-signed by the operator)
the overlay — what the operator added to the redis SBOB before re-signing it
kind: ContainerProfile
metadata:
name: redis-ingress-redis-client
namespace: redis
spec:
ingress:
- identifier: redis-client
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: redis}
podSelector:
matchLabels: {app: redis-client}
ports: [{name: TCP-6379, port: 6379, protocol: TCP}]
logs — redis re-verified, R0012 stops within a minute
{"t":"…","level":"info","msg":"Successfully verified object signature"}
6. Quiet — until something new happens
The logs tab goes silent while SET traffic continues. Now do something the profile never saw:
kubectl -n redis exec deploy/redis-client -- id
logs — the exec is judged against YOUR profile now, the shadow is still there and forensically recording
{"t":"…","rule":"R0001","alert":"Unexpected process launched","ns":"redis","wl":"redis-client","msg":"Unexpected process launched: id with PID 83923"}
{"t":"…","rule":"R0004","alert":"Linux Capabilities Anomalies in container","ns":"redis","wl":"redis-client","msg":"Unexpected capability used: CAP_SYS_ADMIN in syscall read with PID 82970"}
7. Tamper
sed 's#signature.kubescape.io/signature: ME#signature.kubescape.io/signature: MF#' client-sbob-signed.yaml | kubectl apply -f -
logs (within a minute)
{"t":"…","level":"warn","msg":"Object signature verification failed"}
{"t":"…","level":"warn","msg":"user-defined ContainerProfile signature mismatch (tamper detected)"}
kubectl apply -f client-sbob-signed.yaml
kubectl get $CP lists come back with every spec empty — storage serves metadata only for lists. Take the name from the list, then kubectl get $CP <name> for the object. Signing a list item signs an empty profile: the agent enforces nothing and everything alerts. bobctl validate catches it before you sign.
Fully automated — the system accepts and signs
1. Start the auto-acceptor
cd ~/bob/example/redis
pkill -f "[a]cceptor.sh"; NS_SKIP="honey kube-system redis" nohup ~/acceptor.sh operator.pem > acceptor.log 2>&1 &
2. We add a new client from another namespace
It fetches a web page over TLS and writes a timestamp into redis across namespaces:
export CP=containerprofiles.spdx.softwarecomposition.kubescape.io
kubectl create ns client
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-client
namespace: client
labels: {app: web-client}
spec:
replicas: 1
selector:
matchLabels: {app: web-client}
template:
metadata:
labels: {app: web-client}
spec:
containers:
- name: client
image: redis:7-alpine
command: ["/bin/sh", "-c"]
args:
- |
i=0
while true; do
wget -qO /tmp/quote.txt https://labs.iximiuz.com/ || true
redis-cli -h redis-master.redis.svc.cluster.local -p 6379 SET "quote:$i" "$(date -u +%s)" >/dev/null
i=$((i+1)); sleep 3
done
EOF
kubectl -n client rollout status deploy/web-client --timeout=120s
{"t":"…","rule":"R1017","alert":"Rogue artefact","ns":"client","wl":"web-client","msg":"Container 'client' in namespace 'client' has no bound profile — an unmanaged/unknown container is running in the cluster"}
{"t":"…","rule":"R0012","alert":"Unexpected Ingress Network Traffic","ns":"redis","wl":"redis-master","msg":"Unexpected ingress network communication from: 10.42.1.23:6379 using TCP to: redis"}
Note how any new deployment (currently anything that contains a pod and/or job) is marked a ROGUE.
You can heal it if it was your intention. But its shadow collects forensics and the shadow is tamper-proof.
3. Watch it happen (~2–3 min)
no patience? the suggested profile, as a file
yolo/web-client-sbob.json is what the agent suggests for this client. Sign, apply, bind by hand — the acceptor will then leave it alone:
~/sign-object sign --file yolo/web-client-sbob.json --key operator.pem --output web-client-signed.yaml
kubectl apply -f web-client-signed.yaml
kubectl -n client patch deploy web-client -p '{"spec":{"template":{"metadata":{"labels":{"kubescape.io/user-defined-profile":"web-client"}}}}}'
tail -f acceptor.log
2026-09-25T19:23:47Z accepted client/replicaset-web-client-767858fd8c-client-4ae4-9b79-suggested -> Deployment/web-client
overlay for redis/redis: +ingress web-client from client
2026-09-25T19:23:48Z healed redis/redis (ingress overlay merged, re-signed by the operator)
kubectl -n client get pods -L kubescape.io/user-defined-profile
NAME READY STATUS RESTARTS AGE USER-DEFINED-PROFILE
web-client-6b86b5754f-s5ncm 1/1 Running 0 2m replicaset-web-client-767858fd8c-client-4ae4-9b79-suggested
logs — suggested, signed, verified, adopted, in 10 seconds
{"t":"…","level":"info","msg":"suggester: SBoBSuggested"}
{"t":"…","level":"info","msg":"container has a user defined profile"}
{"t":"…","level":"info","msg":"Successfully verified object signature"}
{"t":"…","level":"info","msg":"adopted user-authored ContainerProfile as authoritative base"}
discovered vs. portable
96 execs · 25 opens · 75 syscalls · 6 egress. Addresses, one per resolution; commands, one per invocation:
spec:
execs:
- {path: /bin/busybox, args: [wget, -qO, /tmp/quote.txt, https://labs.iximiuz.com/]}
- {path: /usr/bin/ssl_client, args: [ssl_client, -s, "3", -n, labs.iximiuz.com]}
- {path: /bin/busybox, args: [sleep, "3"]}
- {path: /usr/local/bin/redis-cli, args: [/usr/local/bin/redis-cli, -h, redis-master.redis.svc.cluster.local, -p, "6379", SET, quote:17, "1790417063"]}
- {path: /usr/local/bin/redis-cli, args: [/usr/local/bin/redis-cli, -h, redis-master.redis.svc.cluster.local, -p, "6379", SET, quote:18, "1790417066"]}
- … 90 more
opens:
- {path: /etc/resolv.conf, flags: [O_CLOEXEC, O_LARGEFILE, O_RDONLY]}
- {path: /etc/ssl/certs/ca-certificates.crt, flags: [O_RDONLY]}
- {path: /tmp/quote.txt, flags: [O_CREAT, O_LARGEFILE, O_TRUNC, O_WRONLY]}
- {path: /tmp/quote.txt, flags: [O_LARGEFILE, O_RDONLY]}
- … 21 more
syscalls: [accept4, bind, brk, clone, close, connect, getsockopt, ioctl, poll, … 66 more]
egress:
- identifier: 8e352065…
type: external
dns: labs.iximiuz.com.
dnsNames: [labs.iximiuz.com.]
ipAddress: 104.26.15.92
ports: [{name: TCP-443, port: 443, protocol: TCP}]
- identifier: 6eccf30d…
type: external
dns: labs.iximiuz.com.
dnsNames: [labs.iximiuz.com.]
ipAddress: 104.26.14.92
ports: [{name: TCP-443, port: 443, protocol: TCP}]
- identifier: a986953e…
type: external
ipAddress: 104.26.15.92
ports: [{name: TCP-443, port: 443, protocol: TCP}]
- identifier: db6b8f18…
type: external
ipAddress: 104.26.14.92
ports: [{name: TCP-443, port: 443, protocol: TCP}]
- … redis-master, kube-dns
4. Quiet — until something new happens
kubectl -n client exec deploy/web-client -- id
logs
{"t":"…","rule":"R0002","alert":"Files Access Anomalies in container","ns":"client","wl":"web-client","msg":"Unexpected file access detected: id with PID 83923 to /etc/group"}
5. The agent never overwrites what you signed
sleep 120
kubectl -n client get $CP -l kubescape.io/profile-role=suggestion -o jsonpath='{range .items[*]}{.metadata.name} {.metadata.annotations.signature\.kubescape\.io/identity}{"\n"}{end}'
replicaset-web-client-767858fd8c-client-4ae4-9b79-suggested local-key
what the acceptor added to redis — the second discovered part
spec:
ingress:
- identifier: redis-client
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: redis}
podSelector:
matchLabels: {app: redis-client}
ports: [{name: TCP-6379, port: 6379, protocol: TCP}]
- identifier: web-client
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: client}
podSelector:
matchLabels: {app: web-client}
ports: [{name: TCP-6379, port: 6379, protocol: TCP}]
Two keys, two roles: the vendor signed the redis base and is done. Everything added afterwards — the client SBOB and the ingress overlay on redis — is the operator's and carries operator.pem. A client profile never grants itself ingress into someone else's workload; the operator does, by merging the overlay and re-signing. In whole-object mode any key verifies, so the roles here are convention; unit 3 makes them enforced.
Fragments — who may sign what
Units 1–2 signed whole objects: one profile, one signature, any key verifies. A bundle splits the profile into fragments, and a root-signed trust policy says per class who may sign and which spec paths the class may touch.

(a) fragments and their signers · (b) who may sign what · (c) the three checks · (d) composite or refusal
The chart ships a policy anchored to a published demo root; its class signers are the two demo keys in keys/. Keep the logs tab from unit 1 running.
the trust policy in force (what the agent mounted)
kubectl -n honey get cm node-agent-bundle-policy -o jsonpath='{.data.trust-policy\.json}' | python3 -c 'import sys,json; p=json.load(sys.stdin)["policy"]["classes"]; [print(c, "signers:", [s[:20]+"…" for s in v["signers"]], "paths:", v["allowedSpecPaths"]) for c,v in p.items()]'
base signers: ['key:162a08d59135652ee0…'] paths: ['architectures', 'capabilities', 'execs', 'opens', 'syscalls', 'seccompProfile', 'endpoints', 'imageID', 'imageTag', 'rulePolicies', 'identifiedCallStacks', 'labelSelector']
overlay signers: ['key:58fd31c363a8fac06d…'] paths: ['execs', 'opens']
admission signers: ['key:58fd31c363a8fac06d…'] paths: ['ingress', 'egress']
for k in vendor operator; do echo "$k key:$(openssl pkey -in keys/$k.pem -pubout -outform DER 2>/dev/null | sha256sum | cut -c1-64)"; done
vendor key:162a08d59135652ee095ad2e0d927104c9e4eefe94f4ae844e7387ba1a7352d2
operator key:58fd31c363a8fac06d6eb596c2e8c63d5913b4dc4b971ee62579ec41fff78826
1. The vendor ships the base fragment
Same content as the unit-1 base, now labelled as a base fragment of bundle redis, signed with the vendor key:
export CP=containerprofiles.spdx.softwarecomposition.kubescape.io
cd ~/bob/example/redis && mkdir -p frags
kubectl create --dry-run=client -o json -f vendor/redis-base-signed.yaml | python3 -c '
import sys,json
d=json.load(sys.stdin)
d["metadata"]={"name":"redis-base","namespace":"redis","labels":{"signature.kubescape.io/bundle":"redis","signature.kubescape.io/fragment-class":"base"},"annotations":{"kubescape.io/managed-by":"User"}}
d["spec"]["ingress"]=None
json.dump(d,sys.stdout)' > frags/redis-base.json
~/sign-object sign --file frags/redis-base.json --key keys/vendor.pem --output frags/redis-base-signed.yaml
kubectl apply -f frags/redis-base-signed.yaml
kubectl -n redis delete $CP redis --ignore-not-found
The pod label kubescape.io/user-defined-profile: redis did not change — it now resolves to the bundle redis instead of an object named redis.
logs — one verified leaf, one root; and R0012 is back, because the base has no ingress
{"t":"…","level":"info","msg":"assembled signed bundle overlay"}
{"t":"…","rule":"R0012","alert":"Unexpected Ingress Network Traffic","ns":"redis","wl":"redis-master","msg":"Unexpected ingress network communication from: 10.42.0.49:6379 using TCP to: redis"}
kubectl -n honey logs ds/node-agent -c node-agent --all-pods=true --since=2m | grep -o '"msg":"assembled signed bundle overlay".*' | tail -1
"msg":"assembled signed bundle overlay","bundle":"redis","namespace":"redis","fragments":1,"root":"7c1e…"
2. The operator ships the admission fragment
The ingress the client needs — the same content the agent derived in unit 1 — as an admission fragment, signed with the operator key:
cat > frags/redis-client-ingress.json <<'EOF'
{"apiVersion":"spdx.softwarecomposition.kubescape.io/v1beta1","kind":"ContainerProfile",
"metadata":{"name":"redis-client-ingress","namespace":"redis","labels":{"signature.kubescape.io/bundle":"redis","signature.kubescape.io/fragment-class":"admission"},"annotations":{"kubescape.io/managed-by":"User"}},
"spec":{"ingress":[{"identifier":"redis-client","type":"internal",
"namespaceSelector":{"matchLabels":{"kubernetes.io/metadata.name":"redis"}},
"podSelector":{"matchLabels":{"app":"redis-client"}},
"ports":[{"name":"TCP-6379","port":6379,"protocol":"TCP"}]}]}}
EOF
~/sign-object sign --file frags/redis-client-ingress.json --key keys/operator.pem --output frags/redis-client-ingress-signed.yaml
kubectl apply -f frags/redis-client-ingress-signed.yaml
frags/redis-client-ingress-signed.yaml
kind: ContainerProfile
metadata:
name: redis-client-ingress
namespace: redis
labels:
signature.kubescape.io/bundle: redis
signature.kubescape.io/fragment-class: admission
annotations:
kubescape.io/managed-by: User
signature.kubescape.io/identity: local-key
signature.kubescape.io/signature: MEQCIB…
signature.kubescape.io/content: H4sIAAAA…
spec:
ingress:
- identifier: redis-client
type: internal
namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: redis}
podSelector:
matchLabels: {app: redis-client}
ports: [{name: TCP-6379, port: 6379, protocol: TCP}]
logs — two leaves, a new root, R0012 stops
"msg":"assembled signed bundle overlay","bundle":"redis","namespace":"redis","fragments":2,"root":"a94f…"
3. The operator ships an overlay — and only execs/opens
cat > frags/redis-ops-overlay.json <<'EOF'
{"apiVersion":"spdx.softwarecomposition.kubescape.io/v1beta1","kind":"ContainerProfile",
"metadata":{"name":"redis-ops-overlay","namespace":"redis","labels":{"signature.kubescape.io/bundle":"redis","signature.kubescape.io/fragment-class":"overlay"},"annotations":{"kubescape.io/managed-by":"User"}},
"spec":{"execs":[{"path":"/usr/bin/df","args":["/usr/bin/df","-h"]}]}}
EOF
~/sign-object sign --file frags/redis-ops-overlay.json --key keys/operator.pem --output frags/redis-ops-overlay-signed.yaml
kubectl apply -f frags/redis-ops-overlay-signed.yaml
sleep 60
kubectl -n redis exec sts/redis-master -- df -h >/dev/null
kubectl -n redis exec sts/redis-master -- id >/dev/null
logs — three leaves; `df -h` is silent, `id` is not
"msg":"assembled signed bundle overlay","bundle":"redis","namespace":"redis","fragments":3,"root":"e21b…"
{"t":"…","rule":"R0001","alert":"Unexpected process launched","ns":"redis","wl":"redis-master","msg":"Unexpected process launched: id with PID …"}
4. Three ways to be refused
Wrong key for the class — the vendor signs an admission fragment:
~/sign-object sign --file frags/redis-client-ingress.json --key keys/vendor.pem --output frags/wrongkey.yaml
kubectl apply -f frags/wrongkey.yaml
Wrong paths for the class — an admission fragment smuggling an exec:
python3 -c '
import json
d=json.load(open("frags/redis-client-ingress.json")); d["metadata"]["name"]="redis-sneaky"
d["spec"]["execs"]=[{"path":"/bin/bash","args":["/bin/bash"]}]
json.dump(d,open("frags/sneaky.json","w"))'
~/sign-object sign --file frags/sneaky.json --key keys/operator.pem --output frags/sneaky-signed.yaml
kubectl apply -f frags/sneaky-signed.yaml
Tampered content — rewrite the signed bytes of the overlay:
kubectl -n redis get $CP redis-ops-overlay -o jsonpath='{.metadata.annotations.signature\.kubescape\.io/content}' \
| python3 -c 'import sys,base64,gzip; d=gzip.decompress(base64.b64decode(sys.stdin.read())); print(base64.b64encode(gzip.compress(d.replace(b"/usr/bin/df", b"/bin/backdoor"))).decode())' \
| xargs -I{} kubectl -n redis annotate $CP redis-ops-overlay --overwrite signature.kubescape.io/content={}
logs — each refusal names its reason; the workload keeps the last verified composite
signer not permitted for this fragment class: "redis-client-ingress"
fragment sets a spec path not permitted for its class: "redis-sneaky"
fragment signature does not verify (tampered): "redis-ops-overlay"
{"t":"…","level":"warn","msg":"signed bundle overlay refused: a verified member no longer verifies; keeping the last verified composite"}
{"t":"…","rule":"R1016","alert":"Signed profile tampered","ns":"redis","wl":"redis-master","msg":"…"}
Clean up the three and re-ship the good overlay:
kubectl -n redis delete $CP redis-sneaky
kubectl apply -f frags/redis-client-ingress-signed.yaml
kubectl apply -f frags/redis-ops-overlay-signed.yaml
5. Leaves are never rewritten
for f in redis-base redis-client-ingress redis-ops-overlay; do
kubectl -n redis get $CP $f -o yaml > /tmp/leaf.yaml && ~/sign-object verify --file /tmp/leaf.yaml --strict=false >/dev/null && echo "leaf $f: OK"
done
leaf redis-base: OK
leaf redis-client-ingress: OK
leaf redis-ops-overlay: OK
The keys in keys/ and the root the chart trusts are published demo material — the agent says so at boot (anchored to the PUBLISHED DEMO root key: this authenticates nothing). A real deployment mounts its own root public key, ships its own policy, and keeps every private key off the cluster.
- Previous lesson
- Software Bill of Behaviour for CNCF projects