Segmentation and zero trust
From flat networks to identity-aware access: VPC design, security groups, network policy and the per-request checks that zero trust actually means.
01Why flat networks fail
- Design network tiers that limit lateral movement
- Explain zero trust as per-request authn + authz + device context
- Write a default-deny network policy that does not break DNS
- Roll segmentation out without an outage
- Know when a VPN is the wrong answer
In a flat network, one compromised laptop or pod can reach every database. Most real breaches are not a single exploit; they are an initial foothold followed by lateral movement: harvesting credentials, scanning for open ports and hopping host to host until something valuable answers. Segmentation turns one hop into many, each one authenticated and logged.
Macro-segmentation splits the estate into large zones (VPCs, accounts, environments). Micro-segmentation controls traffic between individual workloads, ideally by identity rather than address. You want both: zones contain the blast radius of a misconfiguration, and per-workload rules contain an attacker inside a zone.
- ACTOR
- EDGE
- SERVICE
- DATA
- STREAM
- Live flow
- Lookup / async
02Tiers and default deny
| Tier | Allowed inbound | Example |
|---|---|---|
| Edge | Internet :443 | CDN, WAF, load balancers |
| App | Edge tier only, via mesh identity | APIs, workers |
| Data | App tier, specific service identities, specific ports | Postgres, Redis, Kafka |
| Management | Zero trust gateway only, MFA, session recorded | Bastion, k8s API, consoles |
In the cloud, express tiers with security groups that reference other security groups rather than CIDR ranges: the database group allows 5432 from the app group, so new app instances are allowed automatically and nothing else is, whatever IP it lands on. In Kubernetes, NetworkPolicy does the same with labels, but only if the CNI enforces it (Calico, Cilium and the managed equivalents do).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: payments }
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: payments }
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } }, podSelector: { matchLabels: { k8s-app: kube-dns } } }]
ports: [{ port: 53, protocol: UDP }, { port: 53, protocol: TCP }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-to-db, namespace: payments }
spec:
podSelector: { matchLabels: { app: postgres } }
ingress:
- from: [{ podSelector: { matchLabels: { app: payments-api } } }]
ports: [{ port: 5432 }]Policies are additive, so api-to-db opens ingress to Postgres only. Because egress is denied too, the API pods also need an egress rule to Postgres on 5432, or the connection never leaves the client side. Forgetting DNS is the classic first-day breakage: every lookup times out and it looks like the whole cluster is down.
Data leaves through egress. Default-deny egress with an allow-list of destinations (your APIs, your package mirror) stops most exfiltration and command-and-control, and makes the rest visible in the proxy logs.
03Segmentation in the cloud
resource "aws_security_group" "db" {
name = "payments-db"
vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "db_from_app" {
security_group_id = aws_security_group.db.id
referenced_security_group_id = aws_security_group.app.id
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
description = "payments-api to postgres only"
}Security groups are stateful (return traffic is allowed automatically) and allow-only. Network ACLs are stateless subnet-level filters that can deny, useful as a coarse backstop such as blocking a known bad range, but painful as the main control. Put data stores in private subnets with no route to an internet gateway, and reach cloud APIs through VPC endpoints so that traffic to S3 or KMS never crosses the internet and can be restricted with endpoint policies.
| Mistake | Why it hurts | Better |
|---|---|---|
Security groups opened to 10.0.0.0/8 | Every workload in the company becomes a permitted caller | Reference the caller's security group or identity |
| Shared VPC with one big app subnet | Prod and test sit side by side; a test box reaches prod databases | Separate accounts or VPCs per environment |
| Peering everything with everything | Transitive reach through shared services turns segments back into one network | Hub and spoke through a transit gateway with route tables per segment and inspection |
| SSH open to the office IP range | VPN or office compromise equals server access | No inbound SSH; session manager or a ZT gateway with recording |
| Rules nobody can explain | Afraid to remove them, so they accumulate forever | Description and owner on every rule; flow logs show which rules are unused |
04Segmenting by identity, not address
IP-based rules break down once workloads autoscale, share nodes and move between clusters: the address that was the API a minute ago is now a batch job. A service mesh fixes this by giving each workload an mTLS identity derived from its service account, then letting you write policy against that identity at layer 7.
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata: { name: ledger-callers, namespace: payments }
spec:
selector: { matchLabels: { app: ledger } }
action: ALLOW
rules:
- from: [{ source: { principals: ["cluster.local/ns/payments/sa/payments-api"] } }]
to: [{ operation: { methods: ["POST"], paths: ["/v1/entries"] } }]The rule names a caller identity, a method and a path. A compromised pod in the same namespace with a different service account is refused even though NetworkPolicy lets its packets through. Combine the two: NetworkPolicy as the coarse layer that still holds if the mesh sidecar is bypassed, mesh policy as the fine layer that understands requests. Turn on strict mTLS mode so plaintext traffic between meshed workloads is rejected rather than silently allowed.
05Rolling it out without an outage
- Map real flows first. Collect VPC flow logs or CNI flow data (Cilium Hubble, Calico flow logs) for a few weeks, including month-end jobs and backups.
- Draft policy from observed flows, grouped by service identity, and review it with the owning team. Anything surprising in the map is a finding in its own right.
- Run in audit mode where the tooling supports it (log-only rules, staged policies), and alert on would-be denials.
- Enforce one namespace or tier at a time, starting with the data tier where the allowed callers are fewest and best known.
- Keep a fast rollback and make new services ship with their policies, so the default-deny estate does not decay into allow-all exceptions.
06Zero trust, concretely
Zero trust, as NIST SP 800-207 describes it, removes the idea that network location grants trust. A policy enforcement point (an identity-aware proxy, mesh sidecar or gateway) sits in front of each resource and asks a policy decision point whether this request should proceed, using identity, device and context signals. Google's BeyondCorp is the best-known large deployment.
- Identity on every request: user and workload, never "it came from the internal subnet".
- Device context: managed, disk encrypted, EDR healthy, OS patched. Unknown device gets read-only or nothing.
- Least privilege per app: access to the HR app says nothing about access to the payments console.
- Continuous evaluation: a session that was fine at 9am is re-checked when risk changes (new geo, EDR alert).
A VPN is the wrong answer when it grants network-level access to everything behind it once connected: one phished credential then reaches every internal app. Replacing it with per-app access through a proxy also removes the internet-facing VPN appliance, a perennial target for mass exploitation.
"Zero trust for me is three checks per request, identity, device and policy, enforced at a proxy in front of each app. The VPN goes away because network location stops being a credential. Segmentation stays, because it limits what an attacker can reach when one of those checks is fooled."
A developer needs production database access for an incident. Zero-trust answer?
Q1Does zero trust mean we can drop network segmentation?+
No. Identity checks can be bypassed by a stolen session or a bug; segmentation still bounds what that attacker can reach. They are layers, not alternatives.
Q2How do you handle legacy apps that cannot do modern auth?+
Put them behind an identity-aware proxy that authenticates the user and passes a signed header, and restrict the app's network so only the proxy can reach it.
Q3What breaks first when you enforce default-deny egress?+
DNS, time sync, package mirrors and cloud API endpoints. Allow them explicitly, preferably through VPC endpoints and an egress proxy that logs destinations.
Related lessons
- Zero trust reference architectureSecEng · Advanced: Security Architecture at ScalePro
- Design a zero-trust access gatewayHLD · Design problems: Access & identityFree
- Kubernetes security: hard to get in, harder to get outSecEng · Cloud & Platform SecurityPro
- How to think like a security engineerSecEng · Foundations of Security EngineeringFree