This is unreleased documentation for Network Enforcer 0.4-dev.

Configuring Kubewarden Network Enforcer for production

This page gives configuration for Network Enforcer in a production cluster. It covers the controller, the fluent-bit DaemonSet of the Istio provider, the PriorityClass, the telemetry, and the other chart values that matter at scale.

For the provider and Kubernetes requirements, see Compatibility. For how the components work together, see Architecture. For the debug tools, see Troubleshooting.

Controller

The controller runs as a Kubernetes Deployment with leader election. One replica is active at a time. It reads flows from the data plane, writes proposals and policies, and updates the policy status.

Network Enforcer has no admission webhook. When the controller is down, learning stops and the policy status is not updated. The native policy objects that the controller already rendered stay in place, and the data plane keeps enforcing them.

Controller replicas

controller.replicas defaults to 1. Only the leader does the work. An extra replica is a warm standby. It shortens the failover time, and it does not add throughput.

With the Istio provider, keep controller.replicas at 1. The controller is the OTLP server that receives the ztunnel logs from fluent-bit, and only the leader listens on that port.

With the Calico or the Cilium provider, you can run two replicas:

controller:
  replicas: 2

Schedule the controller

Use controller.tolerations, controller.nodeSelector, and controller.affinity to control where the controller pods run. For example, spread two replicas across different nodes with podAntiAffinity. This example matters only when controller.replicas is more than 1:

controller:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app.kubernetes.io/component: controller
            topologyKey: kubernetes.io/hostname

Set controller resources

controller.resources defaults to a Guaranteed 100m CPU and 256Mi memory, with the same requests and limits. The memory of the controller grows with the number of distinct flows and the number of proposals in the cluster. Watch the controller pod after the install, and raise both values when the pod gets close to its limit. Keep requests equal to limits, so that the pod keeps the Guaranteed class:

controller:
  resources:
    limits:
      cpu: 250m
      memory: 512Mi
    requests:
      cpu: 250m
      memory: 512Mi

Keep the status sync interval

controller.wnpStatusUpdateInterval defaults to 30s. The quick start lowers it to make the demo responsive. A shorter interval writes the policy status more often and adds load on the API server. In production, keep the default. See wnpStatusUpdateInterval latency for what the interval changes.

Add a PodDisruptionBudget for the controller

A PodDisruptionBudget (PDB) limits how many pods a voluntary disruption can remove at the same time, for example a node drain or a cluster upgrade. The chart ships a PDB template for the controller, off by default.

Set controller.pdb.enabled to true. Set minAvailable or maxUnavailable under controller.pdb, in the same way as a PDB spec:

controller:
  pdb:
    enabled: true
    minAvailable: 1

If controller.replicas is 1 and controller.pdb.minAvailable is 1, the PodDisruptionBudget blocks every voluntary disruption of the controller pod, including kubectl drain. Before you turn on the PodDisruptionBudget, set controller.replicas to 2 or more. With the Istio provider, where the controller must stay at one replica, leave the PodDisruptionBudget off.

fluent-bit DaemonSet, Istio provider

With controller.provider.name: istio, the chart deploys fluent-bit as a DaemonSet. One pod runs on each node. It reads the log files of ztunnel and sends the records to the controller. A node without a fluent-bit pod produces no proposals and no monitor-mode violations for the pods on that node. Protect mode still blocks traffic there, because ztunnel enforces the policy on its own.

fluent-bit scheduling

controller.provider.istio.fluentBit has its own resources, tolerations, nodeSelector, and affinity keys. The pod also gets the shared PriorityClass.

fluent-bit must run on every node where ztunnel runs. The chart sets a toleration that matches every taint by default, the same default that ztunnel itself uses. If you set controller.provider.istio.fluentBit.tolerations to [], fluent-bit stops tolerating taints. The pod is then missing from each tainted node that still runs ztunnel. The chart loses the ztunnel logs from that node, together with the proposals and the monitor-mode violations of its workloads.

If you restrict fluent-bit with nodeSelector or affinity, make sure that the selection still matches every node where ztunnel runs.

In the default TLS mode issuer, each fluent-bit pod receives its certificate from the cert-manager CSI driver. Make sure that the CSI driver runs on every node. See Provider TLS for the other modes.

fluent-bit security context

fluent-bit runs as root (runAsUser: 0) and mounts two read-only hostPath volumes, /var/log and /var/lib/docker/containers, to read the container logs of the node. It is not privileged, it does not allow privilege escalation, and it drops all capabilities.

These two requirements come from the job, not from a gap in the chart. Every log shipper that tails node log files needs the same two requirements.

The container runtime writes the log files under /var/log/pods with mode 0640 and owner root:root. A pod’s fsGroup does not change the ownership of a hostPath volume, so it cannot open the files to a non-root user. The capability CAP_DAC_READ_SEARCH is not an option either. The baseline and the restricted Pod Security Admission levels both forbid it. The kernel also does not activate it for a non-root process. Reading the files needs UID 0.

The DaemonSet mounts /var/log and /var/lib/docker/containers to reach those files from the host. No Pod Security Admission level below privileged admits a hostPath volume. So the fluent-bit pod needs the privileged level, and the two recommended policies that this page excludes next.

PriorityClass

The chart creates a PriorityClass named network-enforcer-priorityclass, with value 1000000, and applies it to the controller, the fluent-bit, and the collector pods. This makes the Kubernetes scheduler prefer these pods over most application pods, and keeps the kubelet from evicting them first under node pressure.

To reuse an existing PriorityClass, for example system-cluster-critical, turn off the chart-managed one and point priorityClass.name at the existing class:

priorityClass:
  enabled: false
  name: system-cluster-critical

If you delete a PriorityClass, running pods that use it keep running. New pods that name the deleted class do not start.

Admission policies and Pod Security Admission

What an admission policy accepts depends on the provider.

With the Calico or the Cilium provider, the chart deploys the controller and the collector only. Both pods run as non-root, drop all capabilities, and use no host namespace and no hostPath volume. They meet the restricted Pod Security Admission level, and none of the Kubewarden Admission Controller recommended policies rejects them. The Network Enforcer namespace can carry pod-security.kubernetes.io/enforce: restricted.

With the Istio provider, the fluent-bit pod fails both the baseline and the restricted level. In protect mode, two of the Admission Controller recommended policies reject it:

Policy Rejects fluent-bit because

do-not-share-host-paths

/var/log and /var/lib/docker/containers are not under /tmp.

do-not-run-as-root

runAsUser is 0 and runAsNonRoot is false.

Exclude the Network Enforcer namespace from these policies, with the recommendedPolicies.skipAdditionalNamespaces value of the Admission Controller Helm chart. The default exclusion list of that chart does not include a Network Enforcer namespace, so add yours in every case:

recommendedPolicies:
  skipAdditionalNamespaces:
    - network-enforcer

Keep the namespace at the privileged Pod Security Admission level, or use audit or warn mode only.

See Security hardening for the full list of recommended policies, and Pod Security Admission for the three levels.

Telemetry

By default, the chart deploys its own OpenTelemetry collector, as a single Deployment replica. telemetry.defaultCollector has tolerations, nodeSelector, and affinity values, but the replica count stays fixed at one. The collector keeps its counters in memory, so a restart resets them. This default is enough for a basic setup, but it is a single point of failure for the violation metrics.

For production, point Network Enforcer at an OpenTelemetry collector that your own platform operates and schedules for high availability:

telemetry:
  collectorStrategy: external
  externalCollector:
    protocol: grpc
    endpoint: otel-collector.observability.svc.cluster.local:4317
    otelCollectorCertificateSecret: otel-collector-ca

The Secret in otelCollectorCertificateSecret holds the CA certificate of your collector in the key ca.crt. Without it, the controller sends the events without TLS verification. The Secret must be in the Network Enforcer namespace.

This collector receives the violation events. It is not the OTLP endpoint that receives the ztunnel logs on the Istio provider. The two hops use different ports and different certificates. See OpenTelemetry collector.

Learning scope

The chart has no value that limits the learning scope. Learning is cluster-wide and starts when the controller starts. The provider decides which flows the controller sees:

  • With the Istio provider, only the namespaces that carry the label istio.io/dataplane-mode=ambient send traffic through ztunnel. That label is the learning scope.

  • With the Calico or the Cilium provider, every flow that the data plane reports is learned, in every namespace.

Learning stops for a workload and direction when a WorkloadNetworkPolicy with the same name exists. You can learn the policies in another cluster, for example staging, promote them there, and apply the resulting policies to production as code. The controller then writes a WorkloadNetworkPolicyProposal only for the workloads that have no policy yet.

See Learning starts when the controller starts for what learning does not record.

Keep the flow dumper off

controller.flowDumper.enabled defaults to false. Keep it off in production. When it is on, the chart creates a NodePort Service on port 30080 that exposes the raw flow records on every node. Turning it on or off also changes the controller arguments, so the controller restarts.

See Flow dumper for what it shows and how to read it.

Example production configuration

This example combines the configuration from this page into one Helm values file, for the Istio provider:

controller:
  provider:
    name: istio
  replicas: 1
  resources:
    limits:
      cpu: 250m
      memory: 512Mi
    requests:
      cpu: 250m
      memory: 512Mi
  wnpStatusUpdateInterval: 30s
  flowDumper:
    enabled: false

telemetry:
  collectorStrategy: external
  externalCollector:
    protocol: grpc
    endpoint: otel-collector.observability.svc.cluster.local:4317
    otelCollectorCertificateSecret: otel-collector-ca

With the Calico or the Cilium provider, set controller.provider.name to calico or cilium. You can then raise controller.replicas to 2 and turn on the PodDisruptionBudget:

controller:
  provider:
    name: calico
  replicas: 2
  pdb:
    enabled: true
    minAvailable: 1

Apply the file at install or upgrade time:

helm upgrade --install network-enforcer kubewarden/network-enforcer \
  --namespace network-enforcer \
  --create-namespace \
  -f production-values.yaml \
  --wait