Skip to content

Signing and tamper detection (experimental)

A user-defined profile is an allow-list — whoever can edit it controls what the runtime treats as "normal". Same goes for the CEL rules1 and configuration. Which is why we decided to make them signable and bundlable.

R1016 tamper detection

For up-to-date examples, consult the component tests Test_29 (a signed profile is accepted), Test_30 (tampering invalidates the signature), and Test_31 (the R1016 alert fires). This implementation will change in the future2.

Implemented as annotation

The signature travels with the object, as an annotation.

      signature.kubescape.io/certificate: LS0tLS1CRUdJTiBDRVJUSUZJ…(base64)   
      signature.kubescape.io/identity: local-key
      signature.kubescape.io/issuer: local
      signature.kubescape.io/signature: MEQCIDEsKzuY5LQM6TO1kNb4…(base64)   
      signature.kubescape.io/timestamp: "1783454246"

Try it yourself

Prerequisites: a cluster with Kubescape sbob-rc4 installed (see the quickstart install), plus kubectl, docker, jq, and yq.

export SIGN_OBJECT="ghcr.io/k8sstormcenter/sign-object:sbob-rc4@sha256:baa78f7c9e2692146d9148760557839a60da4a4bffd224ff000a8d70b1dc00a8"
sign_object() { docker run --rm -v "$PWD:/work" "$SIGN_OBJECT" "$@"; }
mkdir -p sbob-signing && cd sbob-signing

1. Generate a keypair.

sign_object generate-keypair --output cosign.key    

2. Author a profile.

cat > my-profile.yaml <<'EOF'
apiVersion: spdx.softwarecomposition.kubescape.io/v1beta1
kind: ContainerProfile
metadata:
  name: signed-demo
  namespace: sig-demo
spec:
  architectures: ["amd64"]
  matchLabels: { app: signed-demo }
  execs:
  - { path: /bin/sleep, args: ["/bin/sleep", "infinity"] }
EOF

3. Sign it.

sign_object sign --file my-profile.yaml --output my-profile.signed.yaml \
  --key cosign.key --type containerprofile

Signer identity and authorization

Signature verification guarantees integrity (that the profile was not altered since it was signed), but does not restrict which key was used to sign it. In this release candidate, node-agent accepts any valid signature. Ensure Kubernetes RBAC restricts write access to ContainerProfile resources so unauthorized keys cannot be applied.

4. Apply the signed profile first, then a workload that references it by label.

kubectl create namespace sig-demo
kubectl apply -f my-profile.signed.yaml
sleep 5
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata: { name: signed-demo, namespace: sig-demo }
spec:
  replicas: 1
  selector: { matchLabels: { app: signed-demo } }
  template:
    metadata:
      labels:
        app: signed-demo
        kubescape.io/user-defined-profile: signed-demo
    spec:
      containers:
      - { name: app, image: busybox:1.36, command: ["/bin/sleep", "infinity"] }
EOF
kubectl -n sig-demo rollout status deploy/signed-demo

node-agent binds and verifies the signature — no alert. Confirm it's quiet

kubectl -n kubescape logs -l app=node-agent --tail=-1 | grep -c '"RuleID":"R1016"'

5. Tamper. Add /bin/sh to the signed profile, keeping the old signature. Any in-place edit works — kubectl patch, edit, apply, replace, or delete + re-apply. The signature no longer matches the spec:

kubectl -n sig-demo patch containerprofile signed-demo --type=json \
  -p '[{"op":"add","path":"/spec/execs/-","value":{"path":"/bin/sh","args":["/bin/sh"]}}]'

6. Re-bind the pod, then read R1016. The signature is verified at bind time, so a running pod isn't re-checked until it re-binds2 — restart it:

kubectl -n sig-demo rollout restart deploy/signed-demo

Read the alert from the node-agent pods

kubectl -n kubescape logs -l app=node-agent --tail=-1 | grep '"RuleID":"R1016"' \
  | jq -c '{RuleID, alert: .BaseRuntimeMetadata.alertName, severity: .BaseRuntimeMetadata.severity, workload: .RuntimeK8sDetails.workloadName}'
{"RuleID":"R1016","alert":"Signed profile tampered","severity":10,"workload":"signed-demo"}

Clean up: cd .. && kubectl delete namespace sig-demo.

Next


  1. Rules are signable (sign-object --type rules), but verification is gated on enableSignatureVerification (default off, unset by the chart). If set to ON a tampered rule is rejected (fail-closed skip, logged skippedByVerification; not an R1016 alert), and unsigned rules are rejected too — the default library drops to 0 enabled unless every rule is signed. ↩

  2. We currently have a backlog where the late-binding of profiles will be allowed. This requires a lot of testing and process-design, which is why currently, you need to restart a pod to bind the whole process to a new profile (irrespective of signature). This does not apply to the rules and config - they can be changed in real-time. Stay tuned for updates 🤓. ↩↩