Kubernetes Security

Calico Network Policy Best Practices for Production Kubernetes

Calico is the most widely deployed Kubernetes network plugin. Its policy model is powerful but has gotchas that trip up even experienced teams.

Nayan Dey
Security Engineer
6 min read

Project Calico is the most popular network plugin for Kubernetes, running in environments from small development clusters to massive production deployments. It supports standard Kubernetes NetworkPolicies and extends them with its own CRDs that provide additional capabilities. The flexibility is powerful, but it also means there are many ways to get policies wrong.

This guide covers the practices that matter for production security, drawn from real deployments and real mistakes.

Understanding Calico Policy Model

Calico evaluates policies in order of priority. Unlike standard Kubernetes NetworkPolicies, which are purely additive (any matching allow rule permits traffic), Calico's extended policies support explicit deny rules and ordered evaluation.

Standard vs. Calico Policies

Standard Kubernetes NetworkPolicies are namespace-scoped and only support allow rules. If any NetworkPolicy selects a pod and includes an allow rule that matches the traffic, the traffic is permitted. There is no way to write a deny rule.

Calico NetworkPolicy CRDs extend this model with explicit deny rules, policy ordering via the order field, and global policies that span namespaces. This is more powerful but also more complex.

Policy Evaluation Order

Calico evaluates policies in order from lowest to highest order value. The first matching rule determines the action. If no policy matches, the default action depends on the profile associated with the pod.

This ordering is critical for security. A broad allow policy with a low order number can override a specific deny policy with a higher order number. Always assign your deny policies a lower order number than your allow policies.

Implementing Default Deny

The single most important Calico policy is default deny. Without it, all traffic between pods is allowed by default.

Global Default Deny

Use a GlobalNetworkPolicy to deny all traffic cluster-wide, then add specific allow policies:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default-deny
spec:
  order: 1000
  selector: all()
  types:
    - Ingress
    - Egress

This policy applies to all endpoints in the cluster and denies all ingress and egress traffic. The high order number (1000) ensures that specific allow policies with lower order numbers are evaluated first.

Allow DNS First

Before applying default deny, ensure DNS is allowed. Without DNS, pods cannot resolve service names:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: allow-dns
spec:
  order: 100
  selector: all()
  types:
    - Egress
  egress:
    - action: Allow
      protocol: UDP
      destination:
        selector: k8s-app == 'kube-dns'
        ports:
          - 53
    - action: Allow
      protocol: TCP
      destination:
        selector: k8s-app == 'kube-dns'
        ports:
          - 53

Allow Kubernetes System Traffic

Certain cluster-internal traffic must be permitted for Kubernetes to function: kubelet health checks, API server communication, and metrics collection. Identify these flows and create policies before enabling default deny.

Failing to allow system traffic before enabling default deny is the most common cause of cluster outages during network policy rollouts. Test thoroughly in a staging environment first.

Egress Control

Ingress policies get most of the attention, but egress control is equally important. An attacker who compromises a pod needs egress access to exfiltrate data, contact command-and-control servers, or move laterally.

Restrict External Egress

Most pods do not need unrestricted internet access. Create specific egress policies that allow only the external services each pod requires:

apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: backend-egress
  namespace: production
spec:
  order: 200
  selector: app == 'backend'
  types:
    - Egress
  egress:
    - action: Allow
      protocol: TCP
      destination:
        nets:
          - 10.0.0.0/8
        ports:
          - 5432
    - action: Allow
      protocol: TCP
      destination:
        domains:
          - api.stripe.com
        ports:
          - 443

This policy allows the backend pod to reach the internal database on port 5432 and the Stripe API on port 443. All other egress is denied by the default-deny policy.

Block Known Malicious IPs

Calico supports threat feed integration through GlobalNetworkSets. Subscribe to threat intelligence feeds and create deny policies that block traffic to known malicious IP addresses:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: block-threat-feeds
spec:
  order: 50
  selector: all()
  types:
    - Egress
  egress:
    - action: Deny
      destination:
        selector: threat-feed == 'malicious'

Policy Testing and Validation

Use Calico Policy Audit Mode

Calico Enterprise supports audit mode, which logs policy decisions without enforcing them. Enable audit mode for new policies to verify they do not break legitimate traffic before switching to enforcement.

Dry-Run With calicoctl

Before applying a policy, use calicoctl to verify its syntax and check for conflicts with existing policies:

calicoctl apply -f policy.yaml --dry-run

Monitor Denied Traffic

Enable Calico flow logs to see denied traffic in real time. Denied flows that correspond to legitimate application traffic indicate policies that need adjustment. Denied flows to unexpected destinations may indicate compromise attempts.

Namespace Isolation Patterns

Strict Namespace Boundaries

In multi-tenant clusters, enforce strict namespace isolation. Pods in one namespace should not communicate with pods in another namespace unless explicitly allowed:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: namespace-isolation
spec:
  order: 500
  namespaceSelector: tenant == 'true'
  types:
    - Ingress
    - Egress
  ingress:
    - action: Allow
      source:
        namespaceSelector: projectcalico.org/name == "{{.Namespace}}"
  egress:
    - action: Allow
      destination:
        namespaceSelector: projectcalico.org/name == "{{.Namespace}}"

Shared Service Access

Some services, like monitoring agents, log collectors, and mesh sidecars, need cross-namespace access. Create specific policies for these shared services rather than weakening namespace isolation.

Common Mistakes

Applying default deny without allowing system traffic: This immediately breaks cluster functionality. Always test in staging and allow kubelet, API server, and DNS traffic first.

Using too-broad selectors: A policy with selector: all() and action: Allow at a low order number effectively disables all other policies.

Forgetting egress policies: Teams focus on ingress and forget that egress control is critical for preventing data exfiltration and C2 communication.

Not testing policy changes: Network policies can break applications in subtle ways. Changes to DNS resolution, health check paths, or cross-service communication can go undetected without proper testing.

How Safeguard.sh Helps

Safeguard.sh works alongside Calico to provide a comprehensive security posture. While Calico controls network-level access between pods, Safeguard.sh ensures the software running in those pods is secure. It generates SBOMs for your container images, identifies vulnerable components before they reach your cluster, and provides the supply chain visibility needed to understand risk across your entire Kubernetes environment.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.