Skip to content

Network policies

Audience

PLANA staff working on cluster security or troubleshooting pod-to-pod connectivity.

PLANA uses Kubernetes NetworkPolicies to enforce that pods can only reach the network destinations they actually need. The default posture is deny everything; explicit allow rules open exactly the flows the application requires.

The default-deny baseline

Every product namespace has a default-deny NetworkPolicy applied at namespace creation:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: <product-namespace>
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

This blocks all ingress and egress traffic to/from every pod in the namespace. Allow rules added on top open specific destinations.

What each namespace can reach

The allow-list is intentional and minimal. The pattern is to list the specific service-port destinations needed.

Tenant Odoo namespaces (plana-odoo*)

Egress destinationReason
pg01.planapulse.com:5432 / :6432PostgreSQL (and pgbouncer)
redis.redis:6379Sessions, bus, queues
nfs01.planapulse.com:2049NFS filestore
auth.planapulse.com:443OIDC discovery + token exchange
kube-dns:53DNS resolution
Egress to internet on 443For Anthropic API calls (AI agents), Stripe webhooks
Ingress sourceReason
envoy-gateway-systemAll public traffic comes via Envoy
crossplane-systemComposition Jobs need to run against workers

pulse-account (account portal + BOS frontend + API)

EgressReason
pg01:5432plana_pulse_account DB
redis.redis:6379Session blocklist, cache
pulse-banking.pulse-banking:3200PSD2 proxy
ai-agents.ai-bos-agent:8000Agent chat proxy
pulse-events.pulse-events:3001Event bus
Tenant Odoo *.planapulse.app:443XML-RPC to tenant ERPs

ai-bos-agent

EgressReason
Tenant Odoo *.planapulse.app:443JSON-RPC tool calls
pulse-data.pulse-data:8000Neural Business Network
api.anthropic.com:443LLM inference
openrouter.ai:443Fallback LLM

Tools namespaces

Each tool (Forgejo, Matrix, Matomo, etc.) has its own narrow allow list, typically:

  • PostgreSQL for its own DB
  • Redis where applicable
  • Object storage where applicable (Matomo to SOS for archives, etc.)
  • Authentik on 443 for OIDC

Cross-namespace traffic

To allow pod A in namespace X to reach service B in namespace Y, two things must align:

  1. Egress allowed from X to Y/B in X's NetworkPolicy
  2. Ingress allowed from X to Y/B in Y's NetworkPolicy

Both must agree. Missing either side blocks the connection silently (it just times out).

Default-allow for the cluster control plane

Some flows are allowed implicitly by Calico:

  • Pod → kube-apiserver (required for in-cluster API calls)
  • Pod → metadata service (locked-down by Exoscale, no IMDS leak)
  • DNS resolution to kube-dns

These don't require explicit NetworkPolicy entries.

Source of truth

All NetworkPolicies live in infra/k8s/network-policies/ and are reconciled by the network-policies Flux Kustomization.

Two patterns in the directory:

  • infra/k8s/network-policies/cluster-wide.yaml — policies applied via namespaceSelector to many namespaces at once
  • infra/k8s/<namespace>/network-policy.yaml — namespace-specific policies

Adding a new flow

When a service needs a new destination:

  1. Identify the source pod label and destination service:port
  2. Add an egress entry to the source namespace's NetworkPolicy
  3. Add an ingress entry to the destination namespace's NetworkPolicy
  4. PR to infra repo, merge, Flux reconciles within ~60 seconds
  5. Verify with kubectl exec from the source pod: nc -zv <dest-host> <port>

Common diagnostic: connection times out

Symptom: a service-to-service call hangs and eventually times out.

Diagnosis:

bash
# From inside the source pod
nc -zv <dest-host> <dest-port>

# Check Calico's pod policy log
kubectl logs -n calico-system -l k8s-app=calico-node --tail=200 \
  | grep "DROP" | grep -i <source-or-dest-name>

If Calico reports DROP, the NetworkPolicy is missing a rule. Add the flow per "Adding a new flow" above.

Where to read more

© PLANA Digital Ltd.