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-rc3 installed (see the quickstart install), plus kubectl, docker, jq, and yq.

export SIGN_OBJECT="ghcr.io/k8sstormcenter/sign-object:v0.0.3@sha256:f3d4e321fa62e0a4ca421ba59a3fce3f2ff88714aaf87a7d160322cb8ec2f92b"
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: ApplicationProfile
metadata:
  name: signed-demo
  namespace: sig-demo
spec:
  architectures: ["amd64"]
  containers:
  - name: app
    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 applicationprofile

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
        kubescape.io/user-defined-network: 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 applicationprofile signed-demo --type=json \
  -p '[{"op":"add","path":"/spec/containers/0/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 🤓.