Skip to content

Part 5: Network Policy ​

Review baseline: Calico 3.32.2; Kubernetes 1.34–1.36 is Calico 3.32's tested range. Last Updated: September 12, 2026.

Examples assume the standard Calico API server (projectcalico.org/v3), Kubernetes datastore and a compatible policy-enforcing dataplane. Use a dedicated calico-demo namespace with known workload labels, ready endpoints and a working baseline before applying policies. The sections are independent patterns, not a single manifest bundle. Existing higher-priority policies, DNS implementation, Service NAT, host policy and application behavior affect results. No production cluster, admission server or packet-forwarding test was run for this review.

Introduction ​

Network policies control permitted connections between workloads and other endpoints. Calico adds ordered policy, explicit actions, global scope and host endpoint controls to the standard Kubernetes API. Feature availability depends on the product and enforcement path: DNS-domain policy is a commercial extension, while Open Source HTTP policy requires the documented Istio/Dikastes integration.

This deep dive covers both Kubernetes standard policies and Calico's extended capabilities, providing patterns and examples for enterprise security requirements.


Kubernetes Standard NetworkPolicy ​

NetworkPolicy Fundamentals ​

Kubernetes NetworkPolicy is a namespace-scoped resource that selects Pods. Ingress and egress isolation are independent; the allowed traffic for each isolated direction is the union of matching Kubernetes policies. If both ends are isolated, the source's egress and destination's ingress must allow a new connection. Replies to an allowed connection do not need a separate reverse-direction allow rule.

Entries in a from/to list are OR alternatives. A namespaceSelector and podSelector in the same entry are AND conditions. A pod selector without a namespace selector refers to the policy's namespace.

No applicable Kubernetes policy means no isolation by that API for the direction; it does not override host firewalls, Calico policies or other controls. Policy changes and existing connections are implementation dependent, so validate with new connections. The ipBlock address observed before/after Service or load-balancer NAT also depends on the implementation.

Comparison showing that without a NetworkPolicy every pod can reach every other pod freely, while a NetworkPolicy narrows that mesh down to one explicitly allowed path and blocks the rest.

Comparison showing that without a NetworkPolicy every pod can reach every other pod freely, while a NetworkPolicy narrows that mesh down to one explicitly allowed path and blocks the rest.Open full screen ↗

Arrows represent new connections under the illustrated policies, not response packets of an allowed connection. The example assumes no other policy, firewall or path restriction; merely having any NetworkPolicy does not isolate every Pod/direction.

Basic NetworkPolicy Structure ​

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: example-policy
  namespace: calico-demo
spec:
  # Which pods this policy applies to
  podSelector:
    matchLabels:
      app: web

  # Policy types: Ingress, Egress, or both
  policyTypes:
    - Ingress
    - Egress

  # Ingress rules (who can connect TO these pods)
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
        - namespaceSelector:
            matchLabels:
              purpose: monitoring
        - ipBlock:
            cidr: 10.0.0.0/8
            except:
              - 10.0.1.0/24
      ports:
        - protocol: TCP
          port: 8080

  # Egress rules (where these pods can connect TO)
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432

Kubernetes NetworkPolicy Limitations ​

CapabilityKubernetes NetworkPolicyCalico extension / prerequisite
ScopeNamespaced Pod policyGlobalNetworkPolicy can select workloads across namespaces and HostEndpoints
Ordering/actionsAdditive allow rules; no user-defined policy orderTier/order and Allow, Deny, Log, Pass
PortsTCP/UDP/SCTP, named ports, numeric ranges via endPort (stable since 1.25; plugin support required)Calico port-range syntax and additional IP protocol/ICMP matches
HTTP methods/pathsNot part of this APIOpen Source http rules require configured Istio/Dikastes application-layer enforcement
DNS domain namesNot part of this APIFor example, Calico Enterprise domain-policy capability; domains is absent from the Open Source 3.32 CRD
Host interfacesNot a general node firewallHostEndpoint policy with separate local/forwarded/failsafe semantics

The standard NetworkPolicy object's scope is distinct from newer Kubernetes cluster-policy APIs; do not assume that all Kubernetes network security APIs are namespace-only. Check the selected dataplane's protocol and logging support rather than equating schema acceptance with enforcement.


Calico NetworkPolicy Extensions ​

Extended Protocol Support ​

The SCTP examples require a compatible classic dataplane: Calico 3.32 eBPF does not support SCTP policy or Services.

Calico supports additional protocols beyond TCP and UDP:

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: extended-protocols
  namespace: calico-demo
spec:
  selector: app == 'network-tools'

  ingress:
    # ICMP ping
    - action: Allow
      protocol: ICMP
      icmp:
        type: 8  # Echo Request
        code: 0

    # ICMPv6
    - action: Allow
      protocol: ICMPv6
      icmp:
        type: 128  # Echo Request

    # SCTP
    - action: Allow
      protocol: SCTP
      destination:
        ports:
          - 3868  # Diameter

    # UDP with port range
    - action: Allow
      protocol: UDP
      destination:
        ports:
          - "5000:6000"  # Port range

Port Ranges and Named Ports ​

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: port-examples
  namespace: calico-demo
spec:
  selector: app == 'multi-port-app'

  ingress:
    # Port range
    - action: Allow
      protocol: TCP
      destination:
        ports:
          - "8080:8090"

    # Named ports (from pod spec)
    - action: Allow
      protocol: TCP
      destination:
        ports:
          - http      # References containerPort name
          - metrics   # References containerPort name

    # Mix of specific ports and ranges
    - action: Allow
      protocol: TCP
      destination:
        ports:
          - 22
          - 80
          - 443
          - "3000:3100"

Enhanced Selector Syntax ​

Calico uses expression selectors. The following rules illustrate alternatives: combining every broad Allow into one policy widens the permitted set. app != 'untrusted' also matches resources without the label; it is not evidence of trust.

selector: !has(x) matches known in-scope resources lacking the label. notSelector: has(x) negates the packet match and can also match external addresses absent from that selector. A rule's selector: all() does not match every packet; omit endpoint selector conditions to match all packets.

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: selector-examples
  namespace: calico-demo
spec:
  # Label equality
  selector: app == 'web'

  ingress:
    # Set membership
    - action: Allow
      source:
        selector: app in {'frontend', 'api-gateway', 'monitoring'}

    # Negation
    - action: Allow
      source:
        selector: app != 'untrusted'

    # Label existence
    - action: Allow
      source:
        selector: has(security-cleared)

    # Combining conditions (AND)
    - action: Allow
      source:
        selector: app == 'backend' && tier == 'internal'

    # OR inside one selector expression
    - action: Allow
      source:
        selector: (app == 'frontend') || (app == 'api')

    # Namespace selector
    - action: Allow
      source:
        namespaceSelector: environment == 'production'
        selector: app == 'authorized-client'

GlobalNetworkPolicy ​

GlobalNetworkPolicy is non-namespaced and can select workload endpoints across namespaces or HostEndpoints. selector: all() alone is not “only all application Pods.” These examples explicitly limit the selected workloads to the demo namespace, preserving system and host traffic.

Default Deny with Explicit Exceptions ​

The empty policy selects both directions. Its omitted order follows explicitly ordered policies; 10,000 is not a special “lowest priority” value. Empty rules do not override an earlier terminal Allow. Use a separately controlled earlier tier for restrictions that application policies must not override.

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.demo-default-deny
spec:
  tier: default
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  types: [Ingress, Egress]
  ingress: []
  egress: []
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.demo-essential-egress
spec:
  tier: default
  order: 100
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  selector: needs-platform == 'true'
  types: [Egress]
  egress:
    - action: Allow
      protocol: UDP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      destination:
        services:
          name: kubernetes
          namespace: default

This example grants platform egress only to workloads labeled needs-platform=true. The Service match follows the actual kubernetes/default endpoints and ports on the Kubernetes datastore; it is ignored with an etcd datastore. Do not mix that Service match with destination ports, CIDRs or selectors. Network access to the API is distinct from API authentication/RBAC.

Confirm the actual DNS deployment. Namespace plus Pod selectors prevent an unrelated Pod labeled kube-dns from becoming a trusted resolver. Node-local DNS requires a different match for the actual path. Creating an empty policy on kube-system before enumerating system dependencies can break the cluster; demonstrate that pattern in a separate test namespace instead.

An Earlier Egress Guardrail ​

This independent example blocks the IPv4 metadata address for selected demo workloads, then delegates other traffic at the end of its tier. It does not claim complete SSRF protection, protection of privileged/host-networked processes, or coverage of every platform metadata endpoint.

yaml
apiVersion: projectcalico.org/v3
kind: Tier
metadata:
  name: egress-guardrail
spec:
  order: 50
  defaultAction: Pass
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: egress-guardrail.block-imds-v4
spec:
  tier: egress-guardrail
  order: 10
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  types: [Egress]
  egress:
    - action: Deny
      destination:
        nets: [169.254.169.254/32]

Do not place an unconditional Pass before later restrictions in the same tier. Preserve platform-required identity/DNS paths and review the actual workload-to-host enforcement behavior.


NetworkSet and GlobalNetworkSet ​

NetworkSets label reusable IP/CIDR groups; policy selectors reference their labels, not their object names. The address examples below are illustrative and are not a real country/threat feed. A label selector may also match endpoints carrying the same labels, so use controlled labels and the intended namespace/global scope.

Use namespaceSelector: global() in a namespaced policy's entity match when selecting a GlobalNetworkSet. Namespaced NetworkSets remain in their selected namespace. If trusted and blocked ranges overlap, evaluate the deny first; an earlier Allow is terminal.

NetworkSet (Namespace-scoped) ​

yaml
apiVersion: projectcalico.org/v3
kind: NetworkSet
metadata:
  name: corporate-networks
  namespace: calico-demo
  labels:
    network-type: corporate
spec:
  nets:
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16

---
# Reference in policy
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-corporate
  namespace: calico-demo
spec:
  selector: app == 'internal-app'

  ingress:
    - action: Allow
      source:
        selector: network-type == 'corporate'  # References NetworkSet by label

GlobalNetworkSet (Cluster-scoped) ​

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkSet
metadata:
  name: external-trusted-ips
  labels:
    network-group: external-trusted
spec:
  nets:
    - 203.0.113.0/24     # Partner network
    - 198.51.100.0/24    # CDN network
    - 192.0.2.50/32      # Specific trusted IP

---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkSet
metadata:
  name: demo-blocked-networks
  labels:
    network-group: blocked
spec:
  nets:
    # Illustrative test ranges, not geolocation or threat intelligence
    - 192.0.2.128/25
    - 203.0.113.128/25

---
# Reference in GlobalNetworkPolicy
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: external-access-control
spec:
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  selector: has(external-facing)
  order: 200
  types:
    - Ingress

  ingress:
    # Deny overlaps before considering trusted ranges.
    - action: Deny
      source:
        namespaceSelector: global()
        selector: network-group == 'blocked'
    - action: Allow
      source:
        namespaceSelector: global()
        selector: network-group == 'external-trusted'

Tiered Policies ​

Tiers are available in Calico Open Source 3.32. They group namespaced and global Calico policies; they are not a progression from Kubernetes NetworkPolicy to GlobalNetworkPolicy.

Evaluation and Defaults ​

For normal endpoint policy, evaluate tiers by increasing order, then policies within each tier by increasing order. An unset policy order follows explicitly ordered policies. Consider the selected endpoint and traffic direction.

SituationResult
No policy in the tier selects that endpoint/directionSkip the tier
A rule Allows or DeniesFinish this endpoint/direction's policy decision
A rule LogsContinue to the next rule
A rule PassesSkip the remaining policies in this tier and try the next applicable tier
Applicable tier has no terminal rule matchApply its defaultAction, which defaults to Deny
Last applicable tier PassesEvaluate endpoint Profiles; no profile allow means deny

This is not “no rule matches, therefore always move to the next tier.” Also, an Allow at one endpoint does not bypass the other endpoint's policy. Pre-DNAT and untracked host policy have different fall-through behavior, covered below.

The built-in default tier has fixed order 1,000,000, not 1,000 or infinity. Kubernetes NetworkPolicy and Calico policies without an explicit tier belong there. Current kube-admin and kube-baseline tiers use 1,000 and 10,000,000 with Pass defaults for the corresponding Kubernetes cluster-policy integration; therefore default is not universally the last possible tier.

Separate Security, Platform and Application Decisions ​

These example tiers use end-of-tier Pass for security/platform so all their applicable rules are checked before delegation. Tier creation and reordering require centrally controlled privileges.

yaml
apiVersion: projectcalico.org/v3
kind: Tier
metadata:
  name: security
spec:
  order: 100
  defaultAction: Pass
---
apiVersion: projectcalico.org/v3
kind: Tier
metadata:
  name: platform
spec:
  order: 200
  defaultAction: Pass
---
apiVersion: projectcalico.org/v3
kind: Tier
metadata:
  name: application
spec:
  order: 500
  defaultAction: Deny

The example below uses a documentation-only threat address, then a separate restricted-data rule. A Pass at the end of the first security policy would skip the second policy. Keeping delegation at the end of the tier avoids that bypass. The restricted-data labels illustrate segmentation, not complete PCI DSS compliance.

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkSet
metadata:
  name: demo-threats
  labels:
    network-group: demo-threat
spec:
  nets:
    - 192.0.2.100/32
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: security.block-threats
spec:
  tier: security
  order: 10
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  types: [Ingress, Egress]
  ingress:
    - action: Deny
      source:
        namespaceSelector: global()
        selector: network-group == 'demo-threat'
  egress:
    - action: Deny
      destination:
        namespaceSelector: global()
        selector: network-group == 'demo-threat'
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: security.restricted-data
spec:
  tier: security
  order: 20
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  selector: data-scope == 'restricted'
  types: [Ingress]
  ingress:
    - action: Deny
      source:
        notSelector: data-scope == 'restricted'
---
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: platform.dns
spec:
  tier: platform
  order: 10
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  types: [Egress]
  egress:
    - action: Allow
      protocol: UDP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: application.frontend
  namespace: calico-demo
spec:
  tier: application
  order: 10
  selector: app == 'frontend'
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == 'gateway'
      destination:
        ports: [8080]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: app == 'backend'
        ports: [8080]

namespaceSelector: global() plus a separate label selector selects the GlobalNetworkSet. global(label == 'value') is not valid syntax. The platform DNS Allow is an intentional terminal exception for the selected workload's egress; application-tier rules cannot subsequently narrow that exception. The application policy governs only the selected frontend, not every workload in the namespace.

The DNS example assumes conventional CoreDNS Pods with verified namespace/labels. NodeLocal DNSCache or EKS Auto Mode node-local DNS needs rules for the actual resolver path, not a Pod selector copied unchanged. Keep access to the required resolver and validate both UDP and TCP queries.

Tier RBAC Integration ​

Calico tier RBAC uses the pseudo-resources tier.networkpolicies and tier.globalnetworkpolicies, plus get on the target Tier. The Calico authorizer explicitly checks synthetic names such as application.*. This is not a general Kubernetes resourceNames wildcard on ordinary networkpolicies.

The following complete binding example grants one service account namespaced policy editing in the application tier. It does not grant tier creation/reordering or global policy administration.

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: policy-editor
  namespace: calico-demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: demo-get-application-tier
rules:
  - apiGroups: ["projectcalico.org"]
    resources: ["tiers"]
    resourceNames: ["application"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: demo-get-application-tier
subjects:
  - kind: ServiceAccount
    name: policy-editor
    namespace: calico-demo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: demo-get-application-tier
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: demo-edit-application-policies
  namespace: calico-demo
rules:
  - apiGroups: ["projectcalico.org"]
    resources: ["tier.networkpolicies"]
    resourceNames: ["application.*"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: demo-edit-application-policies
  namespace: calico-demo
subjects:
  - kind: ServiceAccount
    name: policy-editor
    namespace: calico-demo
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: demo-edit-application-policies

Global policy editing uses tier.globalnetworkpolicies in a deliberately scoped ClusterRole/ClusterRoleBinding. Keep get on the intended Tier separate from permissions to modify Tier order.

The example assumes the standard Calico aggregated API server. Native v3 CRDs use an admission webhook for tier authorization of create/update/delete; that webhook cannot restrict GET/LIST/WATCH. Do not claim identical read isolation between the two modes. Plain kubectl auth can-i does not validate the combined Calico tier checks: test actual allowed and forbidden requests in an isolated environment with a principal that has no broader bindings. Kubernetes RBAC is additive, so an existing broad grant can defeat this intended restriction.


FQDN-Based Egress Policy ​

The Open Source 3.32 CRD has no destination.domains or Felix dnsTrustedServers field. Setting policySyncPathPrefix enables the policy-sync path used by application-layer integrations; it does not add DNS-domain policy to Open Source.

Calico Enterprise 3.23 documents DNS-domain matches on egress Allow rules. The controller learns A/AAAA/CNAME answers from trusted DNS servers and permits matching destination IPs. This is IP-based enforcement, not HTTPS hostname authentication, so shared destination IPs and application identity still matter. Use workload/Service selectors for in-cluster services.

The following commercial example assumes that feature is enabled, the trusted resolver is verified, and no earlier terminal Allow bypasses it. Each rule has one destination map; repeating that YAML key could silently discard its domain restriction.

yaml
# Calico Enterprise 3.23 example; NOT an Open Source 3.32 resource.
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-approved-domains
  namespace: calico-demo
spec:
  selector: app == 'external-api-client'
  types: [Egress]
  egress:
    - action: Allow
      protocol: UDP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      protocol: TCP
      destination:
        domains:
          - api.github.com
          - "*.example.com"
        ports: [443]

Replace the documentation domain with an approved real domain. *.example.com matches api.example.com and deep.api.example.com, but not the apex example.com. The wildcard must occupy a complete component and only one wildcard is supported. Inline DNS policy mode supports prefix wildcards; non-prefix wildcard patterns require an appropriate documented mode. A broad suffix such as *.amazonaws.com is not an AWS account or IAM boundary.

Keep the actual resolver reachable over UDP and TCP and align its IPs with the trusted DNS configuration. Node-local resolvers require deployment-specific handling. The commercial guide excludes domain policy on the egress hook of egress-gateway Pods because its node-wide DNS cache can make matches absent or intermittent.

On Open Source, use an explicit egress proxy/gateway with its own application authorization, or maintained IP/CIDR NetworkSets when suitable. Do not substitute a one-time DNS lookup for a continuously enforced domain policy.


HTTP Method Filtering (Layer 7) ​

Calico Open Source 3.32 supports HTTP policy through the documented Istio + Dikastes integration. Merely installing an arbitrary Envoy proxy or adding an http field to a normal CNI policy does not enable Layer 7 enforcement.

Prerequisites include the Felix Policy Sync API, the Calico CSI socket mount, Dikastes injection and Envoy external authorization on the relevant traffic path. The current integration guide describes Kubernetes native-sidecar Istio injection and recommends Istio 1.28.1; that statement is not a blanket compatibility promise for every newer Istio. Check the maintained Istio installation guide, both projects' support windows and the actual integration before selecting a production combination. No such integration deployment was run here.

These are ingress Allow examples on an already integrated workload. HTTPS methods/paths require the enforcement proxy to see the HTTP request after TLS termination. Source Pod labels identify workloads, not authenticated end users; configure the integration's trusted workload identity/mTLS path and preserve the network access needed by DNS and the Istio control plane. Validate allowed and denied requests, including proxy/authorization-service failure behavior, before relying on the policy.

HTTP Match Rules ​

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: l7-http-policy
spec:
  namespaceSelector: kubernetes.io/metadata.name == 'calico-demo'
  selector: app == 'api-server'
  order: 300
  types:
    - Ingress

  ingress:
    # Allow only GET and HEAD for read-only clients
    - action: Allow
      source:
        selector: role == 'reader'
      http:
        methods:
          - GET
          - HEAD
        paths:
          - prefix: /api/v1/

    # Allow the listed methods for admin-labeled workloads
    - action: Allow
      source:
        selector: role == 'admin'
      http:
        methods:
          - GET
          - POST
          - PUT
          - DELETE
          - PATCH

    # Allow health checks
    - action: Allow
      http:
        methods:
          - GET
        paths:
          - exact: /health
          - exact: /ready

Path-Based Filtering ​

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: path-based-policy
  namespace: calico-demo
spec:
  selector: app == 'web-app'

  ingress:
    # Public endpoints
    - action: Allow
      http:
        paths:
          - prefix: /public/
          - exact: /

    # Admin endpoints - restricted
    - action: Allow
      source:
        selector: role == 'admin'
      http:
        paths:
          - prefix: /admin/

    # API endpoints - authenticated only
    - action: Allow
      source:
        selector: has(api-access)
      http:
        paths:
          - prefix: /api/

Host Endpoint Protection ​

HostEndpoint represents an interface on a node that Calico manages. Creating one can change host connectivity immediately. defaultEndpointToHostAction controls workload-to-local-host behavior; it does not create HostEndpoints. Installation.calicoNetwork.hostPorts controls hostPort support, not automatic host protection.

Manual Host Endpoint and Policy ​

The following field example is not a complete host firewall. It assumes a self-managed test worker demo-worker at 10.0.1.10, bastion 10.0.0.100, control-plane source 10.0.1.5 and interface eth0. Replace them with verified identities/addresses and prepare all required management, DNS, DHCP, BGP, API, health and egress rules before creating the HostEndpoint. It does not model an EKS-managed control-plane node.

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.demo-worker-ingress
spec:
  order: 100
  selector: host-demo == 'true' && !has(projectcalico.org/namespace)
  types: [Ingress]
  ingress:
    - action: Allow
      protocol: TCP
      source:
        nets: [10.0.0.100/32]
      destination:
        ports: [22]
    - action: Allow
      protocol: TCP
      source:
        nets: [10.0.1.5/32]
      destination:
        ports: [10250]
---
apiVersion: projectcalico.org/v3
kind: HostEndpoint
metadata:
  name: demo-worker-eth0
  labels:
    host-demo: "true"
spec:
  node: demo-worker
  interfaceName: eth0
  expectedIPs: [10.0.1.10]

The kubelet rule targets the worker's authenticated 10250 endpoint. Do not open the obsolete unauthenticated 10255 read-only port as a default requirement. The example defines ingress only; a manually created HostEndpoint without an egress policy/profile may deny host-originated traffic. Complete the actual baseline first.

Calico's default failsafes include inbound TCP 22 and other connectivity ports. They bypass the restrictive intent of the SSH rule above, so this rule alone does not limit SSH to the bastion. Review the failsafe list and a tested recovery path before changing it; do not blindly empty the lists.

Automatic Host Endpoints ​

The node controller's KubeControllersConfiguration.spec.controllers.node.hostEndpoint.autoCreate controls automatic creation. Use a merge patch to preserve other controller settings:

bash
kubectl get kubecontrollersconfiguration.projectcalico.org default -o yaml
# Apply only after reviewing existing host endpoints and global policies.
kubectl patch kubecontrollersconfiguration.projectcalico.org default --type=merge \
  -p '{"spec":{"controllers":{"node":{"hostEndpoint":{"autoCreate":"Enabled"}}}}}'

This can affect all eligible nodes. Automatic endpoints normally carry a default-allow profile; that profile does not override matching policy Deny. Custom templates and createDefaultHostEndpoint can narrow the generated endpoint set, but changing them on an existing deployment requires reviewing existing endpoints and policies first.

Local and Forwarded Traffic ​

Normal host policy defaults applyOnForward to false. With true it also applies to forwarded traffic, which must still pass the relevant workload policy. If no forward policy selects the endpoint/direction, forwarded traffic is allowed by default; selected forward policy with no allow denies it. Locally terminated host traffic has a different default-deny behavior (subject to profiles/failsafes).

DoNotTrack and PreDNAT Policies ​

These are Linux host-policy patterns. They apply to HostEndpoints, not a way to disable tracking on ordinary selected Pods. Confirm support on the chosen dataplane. doNotTrack and preDNAT cannot both be true, and either requires applyOnForward: true.

DoNotTrack ​

An untracked Allow skips connection tracking for matching traffic. It is not a universal performance improvement and may conflict with a Service/NAT path that needs conntrack. Requests and responses require explicit rules; this example assumes a DNS process on the test host serving the trusted client subnet directly.

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.demo-untracked-dns
spec:
  selector: host-demo == 'true' && !has(projectcalico.org/namespace)
  order: 10
  types: [Ingress, Egress]
  doNotTrack: true
  applyOnForward: true
  ingress:
    - action: Allow
      protocol: UDP
      source:
        nets: [10.0.0.0/24]
      destination:
        ports: [53]
    - action: Allow
      protocol: TCP
      source:
        nets: [10.0.0.0/24]
      destination:
        ports: [53]
  egress:
    - action: Allow
      protocol: UDP
      source:
        ports: [53]
      destination:
        nets: [10.0.0.0/24]
    - action: Allow
      protocol: TCP
      source:
        ports: [53]
      destination:
        nets: [10.0.0.0/24]

Unlike normal endpoint policy, a miss in the untracked stage does not impose an end-of-tier default drop; subsequent tracked policy can still apply. This is not an implicit deny-all firewall for the host.

PreDNAT ​

Pre-DNAT policy sees the original destination IP/port before DNAT. It is ingress-only and uses normal connection tracking for permitted return traffic. This example protects the specific TCP NodePort 30080 on the selected host path:

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.demo-nodeport
spec:
  selector: host-demo == 'true' && !has(projectcalico.org/namespace)
  order: 20
  types: [Ingress]
  preDNAT: true
  applyOnForward: true
  ingress:
    - action: Allow
      protocol: TCP
      source:
        nets: [10.0.0.0/24]
      destination:
        ports: [30080]
    - action: Deny
      protocol: TCP
      destination:
        ports: [30080]

There is no default-drop at the pre-DNAT stage. Unmatched traffic continues to subsequent host/workload policy. The explicit second rule rejects untrusted traffic to this NodePort; other ports are outside this example. A path that goes directly to a Pod without traversing this host NodePort is not covered by the rule.


Policy Debugging ​

Inspect actual labels, namespaces, Service endpoints, all applicable tiers and both directions. In the standard Calico API server, a list without a tier selector can default to the default tier; -A means all namespaces, not automatically all tiers.

bash
kubectl get networkpolicies.networking.k8s.io -n calico-demo -o yaml
kubectl get tiers.projectcalico.org -o yaml
for CALICO_TIER in $(kubectl get tiers.projectcalico.org -o jsonpath='{.items[*].metadata.name}'); do
  kubectl get networkpolicies.projectcalico.org -n calico-demo \
    -l "projectcalico.org/tier=$CALICO_TIER" -o yaml
  kubectl get globalnetworkpolicies.projectcalico.org \
    -l "projectcalico.org/tier=$CALICO_TIER" -o yaml
done
calicoctl get workloadendpoint -n calico-demo \
  --selector="app == 'frontend'" -o yaml
bash
CALICO_NAMESPACE=calico-system
CALICO_NODE=demo-worker
CALICO_POD="$(kubectl -n "$CALICO_NAMESPACE" get pods -l k8s-app=calico-node \
  --field-selector "spec.nodeName=$CALICO_NODE" -o jsonpath='{.items[0].metadata.name}')"
test -n "$CALICO_POD"
kubectl -n "$CALICO_NAMESPACE" logs "$CALICO_POD" -c calico-node --tail=200
kubectl -n "$CALICO_NAMESPACE" exec "$CALICO_POD" -c calico-node -- \
  calico-node -felix-ready
bash
# Assumes named, ready test Pods and an nc binary in the client image.
TARGET_POD=backend-test
TARGET_IP="$(kubectl -n calico-demo get pod "$TARGET_POD" -o jsonpath='{.status.podIP}')"
test -n "$TARGET_IP"
kubectl -n calico-demo exec frontend-client -- nc -z -w 3 "$TARGET_IP" 8080

The calico-node -felix-ready command checks readiness. It is not a policy trace or a measurement of per-packet evaluation time. The released Open Source calicoctl does not provide the former policy-trace command. Listing an endpoint or grepping policy text also does not calculate the complete effective policy.

Test an allowed client, an untrusted client, a wrong port, a cross-namespace client and resolver access using new connections. Check the target's destination port, not the client's ephemeral source port. Test direct Pod and Service addresses separately to distinguish policy from endpoint/NAT/forwarding problems; kube-proxy or its replacement can therefore be relevant.

In the iptables dataplane, inspect actual cali- chains on the correct node/network namespace; a placeholder hash is not a real chain name. iptables Log actions write to the host kernel log, whereas Felix stdout is primarily component/controller diagnostics. Neither a missing stdout message nor a zero counter proves an unused policy under every path. eBPF/nftables require their own backend diagnostics; tc filter show alone does not explain an effective policy verdict.

Stage before Enforcement ​

Open Source 3.32 provides StagedNetworkPolicy, StagedGlobalNetworkPolicy and StagedKubernetesNetworkPolicy. Staged resources do not enforce packet decisions. With the flow-log/Whisker pipeline configured, inspect policies.pending to preview observed effects.

yaml
apiVersion: projectcalico.org/v3
kind: StagedNetworkPolicy
metadata:
  name: default.preview-backend-egress
  namespace: calico-demo
spec:
  tier: default
  order: 100
  selector: app == 'frontend'
  types: [Egress]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: app == 'backend'
        ports: [8080]

This preview deliberately contains only the backend connection. Check whether required DNS or other flows would be denied before creating an equivalent enforced policy. Absence of observed traffic is not proof that a dependency is unnecessary. action: Log alone is not a universal audit-only policy mode: evaluation continues and may still end in a deny.


Common Policy Patterns Library ​

Frontend → Backend → Database ​

The demo assumes ready workloads with the shown labels and listeners. Every server port is a destination port; matching source.ports: [8080] would normally reject clients using ephemeral source ports. All numeric port rules specify TCP. The demo gateway is an application fixture, not an assumed label of a particular ingress controller.

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: default.frontend
  namespace: calico-demo
spec:
  order: 100
  selector: app == 'frontend'
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == 'gateway'
      destination:
        ports: [8080]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: app == 'backend'
        ports: [8080]
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: default.backend
  namespace: calico-demo
spec:
  order: 100
  selector: app == 'backend'
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == 'frontend'
      destination:
        ports: [8080]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: app == 'database'
        ports: [5432]
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: default.database
  namespace: calico-demo
spec:
  order: 100
  selector: app == 'database'
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == 'backend'
      destination:
        ports: [5432]
  egress: []

Allow DNS separately for the clients that need it, using the scoped essential-egress pattern and its prerequisite label. The database starts no new egress connections in this simplified pattern; stateful replies still work. Backups, replication and external dependencies need their own reviewed rules.

Tenant Isolation ​

Calico does not interpolate $(namespace.tenant), ${namespace.labels.tenant} or ${namespace.name} inside selectors. Generate one policy per explicit tenant value, or use a namespaced policy for same-namespace isolation.

yaml
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: default.team-a-isolation
spec:
  order: 500
  namespaceSelector: tenant == 'team-a'
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      source:
        namespaceSelector: tenant == 'team-a'
  egress:
    - action: Allow
      destination:
        namespaceSelector: tenant == 'team-a'
    - action: Allow
      protocol: UDP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        selector: k8s-app == 'kube-dns'
        ports: [53]

This allows all intra-team-a traffic, including across namespaces with that label. Namespace-label administration and policy-edit permissions must be controlled; earlier Allows can override the intended isolation. Test cross-tenant and unlabelled namespace cases.

Same Namespace plus Shared Services ​

In this namespaced policy, entity selectors without a namespace selector stay within calico-demo. This is an alternative to the restrictive microservice pattern, not an additional policy to layer on top of it.

yaml
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: default.namespace-and-shared
  namespace: calico-demo
spec:
  order: 200
  selector: all()
  types: [Ingress, Egress]
  ingress:
    - action: Allow
      source:
        selector: all()
  egress:
    - action: Allow
      destination:
        selector: all()
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'logging'
        selector: app == 'log-receiver'
        ports: [24224]
    - action: Allow
      protocol: TCP
      destination:
        namespaceSelector: kubernetes.io/metadata.name == 'auth'
        selector: app == 'identity-provider'
        ports: [8080]

Add the required resolver rule separately and ensure destination-side policy permits the client. A logging port or identity-provider label is an explicit demo assumption; verify actual receiver protocol/port and application authentication.

Open Source Egress Control ​

Maintain approved addresses in a NetworkSet when the service has an address contract. The example IP is documentation-only; it is not a real API endpoint or a permanent DNS resolution.

yaml
apiVersion: projectcalico.org/v3
kind: NetworkSet
metadata:
  name: approved-api-ips
  namespace: calico-demo
  labels:
    destination-group: approved-api
spec:
  nets: [203.0.113.10/32]
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: default.approved-api-egress
  namespace: calico-demo
spec:
  order: 100
  selector: app == 'external-api-client'
  types: [Egress]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: destination-group == 'approved-api'
        ports: [443]

Use the earlier DNS rule if the application resolves names. For dynamic external services, use an appropriately authorized proxy or the separately documented commercial domain policy. Allowing all RFC1918 space does not mean “only this cluster” and may grant access to unrelated private networks.

Default Deny as Part of a Security Design ​

Combine a scoped default-deny baseline with explicit required connections, controlled label/RBAC ownership and application authentication. This is network segmentation, not by itself a complete zero-trust or compliance implementation. Stage policies and test negative paths before enforcement.


Policy Performance Impact ​

Policy count alone is not a capacity benchmark. Cost depends on endpoint count, selector changes, rule structure, active flows, update rate and the chosen dataplane. The former “1,000 policies is very slow” and linear-cost assertions had no measured environment or raw results.

Equality, set membership and label-existence selectors can all benefit from Calico's selector optimizations. Do not merge policies in a way that broadens access solely to reduce object count. Reuse maintained NetworkSets, bound logging volume and measure convergence under representative changes.

Readiness checks, grepping “Policy sync” and counting lines from iptables -L do not measure rule-evaluation latency. Enable and scrape the documented Felix metrics endpoint, inspect metric TYPE/HELP and units, then correlate programming/update measurements with a controlled workload:

bash
# After enabling the documented Felix metrics endpoint through its config owner:
kubectl -n "$CALICO_NAMESPACE" port-forward "pod/$CALICO_POD" 9091:9091
bash
# In another terminal while the localhost port-forward remains active:
curl --fail --silent --show-error http://127.0.0.1:9091/metrics

Felix metrics are disabled by default; enable prometheusMetricsEnabled through the configuration owner first. The endpoint must be reachable on the selected node. This is metrics discovery, not a published performance result. Test positive and negative traffic paths while changing policies and preserve the exact Calico/Kubernetes/kernel versions, dataplane, topology, load and measurements.

Operational Principles ​

  1. Start in an isolated namespace, inventory required connections, then stage and enforce explicit rules.
  2. Treat labels, namespace labels and policy-edit RBAC as part of the authorization boundary.
  3. Review terminal Allow/Pass effects whenever tiers or their order change.
  4. Keep host management/failsafe and Service/DNS requirements separate from application rules.
  5. Combine network segmentation with workload/end-user authentication; do not claim that an IP rule alone prevents every SSRF or compliance failure.

References ​