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.