Runtime-Enforcer Quick Start

Welcome to the Runtime-Enforcer Quick Start!

This guide walks you through the following steps:

  • Deploying the Runtime-Enforcer in a Kubernetes cluster

  • Deploying a simple workload and looking at the generated WorkloadPolicyProposal.

  • Converting the WorkloadPolicyProposal into a WorkloadPolicy in monitor mode and observing the alert.

  • Switching the WorkloadPolicy to protect mode and seeing enforcement in action.

Prerequisites

Before deployment, you need to prepare the following:

  • A Kubernetes cluster (you can simply run a kind cluster)

  • helm installed locally

  • kubectl installed locally

  • cert-manager and cert-manager-csi-driver installed in the cluster

Please check your environment satisfies the minimum runtime enforcer requirements.

Install cert-manager and cert-manager-csi-driver

To install cert-manager, run the following commands:

helm repo add jetstack https://charts.jetstack.io

helm repo update

helm install cert-manager jetstack/cert-manager \
	--namespace cert-manager \
	--create-namespace \
	--set crds.enabled=true \
	--wait

For more information on configuring cert-manager, please visit the cert-manager documentation.

The cert-manager-csi-driver is used to automatically provision and mount TLS certificates directly inside runtime enforcer pods (agent and OTEL collector) without creating intermediate Secret resources. To install it, run the following commands:

helm install cert-manager-csi-driver jetstack/cert-manager-csi-driver \
	--namespace cert-manager \
	--wait

Deploy Runtime-Enforcer

Follow these simple steps from your local machine to get Runtime-Enforcer up and running:

Install the Helm chart

Runtime-Enforcer ships with a standalone OTEL collector Deployment that handles violation metrics and events forwarding. No external OpenTelemetry Collector is needed for a basic setup.

helm repo add runtime-enforcer https://rancher-sandbox.github.io/runtime-enforcer/
helm repo update
helm install runtime-enforcer runtime-enforcer/runtime-enforcer \
  --namespace runtime-enforcer \
  --create-namespace \
  --wait

Verify the Deployment

After installation, ensure all pods are running:

kubectl get pods -n runtime-enforcer

Example output:

runtime-enforcer   runtime-enforcer-controller-manager-bfbd8774d-bvjjn   1/1     Running
runtime-enforcer   runtime-enforcer-agent-lsxt4                          1/1     Running
runtime-enforcer   runtime-enforcer-otel-collector-7f8b4c6d9-xk2mv      1/1     Running

The OTEL collector runs as a standalone Deployment (one replica per cluster) rather than as a sidecar in each agent pod.

Verify violation metrics

The OTEL collector Deployment exposes Prometheus metrics on port 9090. To verify:

kubectl port-forward -n runtime-enforcer deployment/runtime-enforcer-otel-collector 9090:9090
curl localhost:9090/metrics | grep runtime_enforcer_violations

Once violations occur, you will see a counter like:

runtime_enforcer_violations_total{action="monitor",k8s_namespace_name="default",node_name="node-1",policy_name="deploy-opensuse-deployment"} 1

Summary

At this point, Runtime-Enforcer is up and running. You’re now ready to write some policies.

WorkloadPolicyProposal generation

When Runtime-Enforcer starts, it observes newly started processes on the node. For each Kubernetes workload, it generates a WorkloadPolicyProposal containing the list of observed executables. Executables that ran before the agent started won’t be visible in the proposal.

Deploy a simple application

Let’s create a fresh new openSUSE Deployment.

kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: opensuse-deployment
  labels:
    app: opensuse-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: opensuse-deployment
  template:
    metadata:
      labels:
        app: opensuse-deployment
    spec:
      containers:
      - name: opensuse
        image: registry.opensuse.org/opensuse/bci/bci-ci:3
        # Create a while loop to continually run sleep and ls.
        # This helps detect two groups of processes in this test:
        # 1. process snapshot (bash)
        # 2. process events (sleep and ls).
        command: ["bash", "-c", "while true; do sleep 5; ls; done"]
EOF

# wait for the pod to be ready
kubectl wait --for=condition=Ready pod -l app=opensuse-deployment --timeout=300s

Proposal generation

After a few seconds, you should see a new WorkloadPolicyProposal resource created for the openSUSE deployment.

kubectl get workloadpolicyproposals.security.rancher.io deploy-opensuse-deployment -o yaml
apiVersion: security.rancher.io/v1alpha1
kind: WorkloadPolicyProposal
metadata:
  name: deploy-opensuse-deployment
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    blockOwnerDeletion: true
    controller: true
    kind: Deployment
    name: opensuse-deployment
    uid: f5e1a25e-8a80-4c2a-b21a-b70f28a0651c
  uid: 6aeac998-d4b6-4e17-9ce1-4d76bc4def61
spec:
  rulesByContainer:
    opensuse:
      executables:
        allowed:
        - /usr/bin/ls
        - /usr/bin/sleep
  selector:
    matchLabels:
      app: opensuse
      type: deployment

Notes on this proposal:

  • The proposal includes a list of observed executables for the opensuse container. As expected, it captured the ls and sleep commands.

  • As the name suggests, this is only a proposal and not a definitive policy yet, so nothing is enforced at this stage. To enforce it, we will create a WorkloadPolicy.

  • This proposal is tied to a specific workload. Its name is always in the form of <workload-type>-<workload-name>. There is also an owner reference to the Deployment so that when the workload is deleted, the proposal can be cleaned up automatically.

WorkloadPolicy (monitor mode)

This proposal looks reasonable for the Deployment, so the next step is converting it into a definitive policy. To do that, label the WorkloadPolicyProposal with the security.rancher.io/policy-ready label set to true.

kubectl label workloadpolicyproposals.security.rancher.io deploy-opensuse-deployment security.rancher.io/policy-ready=true

You can also use the kubectl plugin to perform the same step:

kubectl runtime-enforcer proposal promote deploy-opensuse-deployment

After a few seconds, you should see a new Custom Resource called WorkloadPolicy.

kubectl get workloadpolicy.security.rancher.io deploy-opensuse-deployment -o yaml
apiVersion: security.rancher.io/v1alpha1
kind: WorkloadPolicy
metadata:
  name: deploy-opensuse-deployment
  namespace: default
  ownerReferences:
  - apiVersion: security.rancher.io/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: WorkloadPolicyProposal
    name: deploy-opensuse-deployment
    uid: 3dcb1758-cbde-4523-bbbf-4170758aa1dd
  uid: 394449cb-db06-4a15-aec0-6274d1f34d8e
spec:
  mode: monitor
  rulesByContainer:
    opensuse:
      executables:
        allowed:
        - /usr/bin/ls
        - /usr/bin/sleep
  selector:
    matchLabels:
      app: opensuse-deployment

The syntax is similar to the proposal, but there is one important difference: the spec.mode: monitor field. monitor is a passive mode: violations of the executable list are reported but not blocked. This means that if a process other than ls or sleep is executed, it’s reported as an OTEL event, but it won’t be blocked.

Container enforcement is scoped by container name in .spec.rulesByContainer. If an additional container is added to an already protected Pod, it is intentionally left unenforced. See Monitor phase and Protect phase for details.

Converting a proposal into a policy isn’t enough to apply it to a workload: the workload needs to bind itself to the policy. This is done by adding a label to the workload pods. Since we are using a Deployment, the correct approach is to add the label to the Deployment’s pod template.

You should set the security.rancher.io/policy label at Pod creation time only. Changing this label on a running Pod (adding, removing, or modifying its value) is prohibited.

By default in the runtime-enforcer Helm chart, pods with a non-existing policy are prevented from running. This ensures that when a pod starts, it has all protections ready. To enable fail-open behavior, set agent.nriFailopen=true.

kubectl patch deployment opensuse-deployment --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"security.rancher.io/policy":"deploy-opensuse-deployment"}}}}}'
# wait for the new pods to be ready
kubectl wait --for=condition=Ready pod -l security.rancher.io/policy=deploy-opensuse-deployment --timeout=300s

This label update causes a deployment rollout. To avoid this, you should put the label on the workload at creation time.

After the rollout, the policy will be applied. Let’s test it.

In one terminal, check the OTEL collector logs:

kubectl logs -n runtime-enforcer deployment/runtime-enforcer-otel-collector -f

In another terminal, run an allowed command:

kubectl exec -n default deployment/opensuse-deployment -- ls

Nothing should be reported.

Now, run a command that is not in the allowlist:

kubectl exec -n default deployment/opensuse-deployment -- ps

In the OTEL collector logs you should see an event for the ps command being executed.

monitor 3f3235e0e92e6143965d46b967691cc1 9a6b46fa3165e86d evt.time=2026-01-14T10:39:01Z evt.rawtime=1768387141935180372 policy.name=deploy-opensuse-deployment k8s.ns.name=default k8s.workload.name=opensuse-deployment k8s.workload.kind=Deployment k8s.pod.name=opensuse-deployment-f69df6b94-7s7f4 container.full_id=2f6eb089830e2c281551274e8d0e94bdb182a5444fc1a4ab7316f33dff8a5017 container.name=opensuse proc.exepath=/usr/bin/ps action=monitor

Violations are also visible directly on the WorkloadPolicy status. After the controller’s next sync tick (up to 30 seconds), you can inspect them with kubectl:

kubectl describe workloadpolicy.security.rancher.io deploy-opensuse-deployment -n default

In the output, look for the Violations section under Status:

Status:
  Violations:
  - timestamp: "2026-01-14T10:39:01Z"
    podName: opensuse-deployment-f69df6b94-7s7f4
    containerName: opensuse
    executablePath: /usr/bin/ps
    nodeName: node-1
    action: monitor

The controller scrapes violation records from agents via gRPC during its periodic status sync and stores the most recent 100 entries in the WorkloadPolicy status. No external observability stack is required to view violations.

WorkloadPolicy (protect mode)

Once confident in the policy and wanting to enforce it, change the mode to protect. From now on, every violation of the executable list is blocked.

kubectl patch workloadpolicy deploy-opensuse-deployment -n default --type='json' -p='[{"op": "replace", "path": "/spec/mode", "value": "protect"}]'

You can also use the kubectl plugin to perform the same step:

kubectl runtime-enforcer policy protect deploy-opensuse-deployment -n default

Now try again an allowed binary, nothing should be reported.

kubectl exec -n default deployment/opensuse-deployment -- ls

This time, if running a not permitted binary, you should see not only a report but also the process being blocked.

kubectl exec -n default deployment/opensuse-deployment -- ps

The terminal tells us the process is blocked with EPERM.

exec /usr/bin/ps: operation not permitted
command terminated with exit code 255

And there is a log entry for it:

protect 37298bcde8726ade2516b5d3c63aa663 cb28aa0b03259160 evt.time=2026-01-14T10:49:11Z evt.rawtime=1768387751471907924 policy.name=deploy-opensuse-deployment k8s.ns.name=default k8s.workload.name=opensuse-deployment k8s.workload.kind=Deployment k8s.pod.name=opensuse-deployment-f69df6b94-7s7f4 container.full_id=2f6eb089830e2c281551274e8d0e94bdb182a5444fc1a4ab7316f33dff8a5017 container.name=opensuse proc.exepath=/usr/bin/ps action=protect

Advanced Configuration

Use an external collector

If you already have an OTEL collector deployed in your cluster, you can use the external collector strategy to point the agent directly at it:

helm upgrade runtime-enforcer runtime-enforcer/runtime-enforcer \
  --namespace runtime-enforcer \
  --set telemetry.collectorStrategy=external \
  --set telemetry.externalCollector.protocol=grpc \
  --set telemetry.externalCollector.endpoint=https://otel-collector.otel-collector.svc.cluster.local:4317