Education › Security › Stage 1: Foundations

Network security & zero trust

Segmentation, firewalls and security groups, VPNs, DNS and TLS everywhere, why the perimeter is not enough.

Beginner ~30 min read Module 4 of 16

For decades network security meant a hard perimeter: a firewall between the internet and a trusted inside. That model fails the moment one laptop, one pod or one credential inside the perimeter is compromised, because inside everything trusts everything. Modern network security keeps the useful parts of the perimeter (fewer exposed services, filtered traffic) and adds the idea that no connection is trusted for where it comes from, only for what it can prove. This module covers segmentation, firewalls and security groups, encrypted transport everywhere, the DNS and egress controls people forget, and what zero trust means when you have to implement it rather than buy it.

After this module you can
  • Segment a network into zones so that a compromise in one does not reach the others
  • Write stateful firewall and cloud security group rules that allow only what is needed, in both directions
  • Explain why egress filtering and DNS controls matter as much as inbound rules
  • Describe zero trust as identity-based, per-request access and apply it to services and people
  • Verify exposure from the outside with basic scanning tools

Zones and the blast radius

Segmentation divides a network into zones with controlled paths between them. The classic three-tier layout is still a good starting point: a public zone for load balancers and nothing else, a private zone for application servers that accept traffic only from the load balancers, and an isolated zone for databases that accept traffic only from the application zone and have no route to the internet at all. Add a management zone for bastion hosts or, better, no bastion and an identity-aware access service instead.

The goal is blast radius: when an application server is compromised, the attacker should be able to reach the database it already uses, and nothing else. Not the other team's database, not the CI system, not the office network. Every zone boundary is a place to enforce rules and to log traffic, which also makes lateral movement visible.

PUBLICPRIVATEISOLATED: NO INTERNET ROUTE:443 only:8080 from sg lb:5432 from sg app:6379 from sg appegress: mirrors, APIsno routeInternetusers and attackersLoad balancerTLS ends hereNAT gatewayallow-listed egressApp serverssg: appPostgressg: db, egress noneCachesg: cacheBlockeddb -> internet
Three zones, three deliberate flows: the internet reaches only the load balancer, the load balancer reaches only the app tier, the app tier reaches only its data tier, and egress from the private tier passes through an allow-listed NAT. A compromised app server can reach no more than that.
A small VPC layout with one subnet per tier per availability zone; the routing tables define who can reach the internet.
text
VPC 10.20.0.0/16

  public   10.20.0.0/24  10.20.1.0/24    route: 0.0.0.0/0 -> internet gateway
           load balancers, NAT gateway
  private  10.20.10.0/24 10.20.11.0/24   route: 0.0.0.0/0 -> NAT gateway (egress only)
           application servers, Kubernetes nodes
  isolated 10.20.20.0/24 10.20.21.0/24   route: local only
           databases, caches, queues

  allowed flows
    internet  -> public:443
    public    -> private:8080
    private   -> isolated:5432, :6379
    private   -> internet (via NAT) only for allow-listed destinations
Note

Inside Kubernetes the same idea is a NetworkPolicy: by default every pod can talk to every pod; a default-deny policy per namespace plus explicit allows re-creates zones at the workload level. The container security module builds on this.

Firewalls and security groups, done tightly

A stateful firewall tracks connections, so a rule that allows inbound port 443 automatically allows the reply packets; you do not open high ports for responses. Cloud security groups are stateful firewalls attached to instances or network interfaces, and their best feature is that rules can reference other security groups instead of IP ranges: "allow 5432 from the app tier's group" keeps working when instances come and go. Network ACLs are stateless subnet-level filters, useful as a coarse backstop, awkward for anything precise.

Tight rules share four properties: the source is as narrow as possible (a group or a specific range, not 0.0.0.0/0 except on public listeners), the port is specific, the direction is explicit, and every rule has a description saying why it exists. The most common findings in cloud audits are still administrative ports (22, 3389, 5432, 6379, 27017) open to the world; scanners find those within minutes of creation.

Security groups referencing each other. The database accepts traffic only from members of the app group; nobody opens 5432 to a CIDR.
hcl
resource "aws_security_group" "app" {
  name   = "app"
  vpc_id = var.vpc_id

  ingress {
    description     = "HTTP from the load balancer only"
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.lb.id]
  }
}

resource "aws_security_group" "db" {
  name   = "db"
  vpc_id = var.vpc_id

  ingress {
    description     = "Postgres from app instances only"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }

  egress = []   # a database initiates no outbound connections
}
Watch out

Cloud defaults are permissive on egress: a new security group usually allows all outbound traffic. That is how a compromised host downloads tools and exfiltrates data. Restrict egress deliberately; a database tier needs none.

Egress, DNS and the traffic nobody watches

Inbound rules stop the first step of an intrusion; egress rules stop most of the later ones. Malware calls home, attackers pull tooling, stolen data leaves — all outbound. Route private subnets through a NAT gateway or proxy that only permits known destinations: your package mirrors, your cloud provider's APIs, the third-party services you actually use. Everything else is denied and logged, and a new destination is a change request, not a surprise.

DNS is both a control point and a leak. Point every host at resolvers you run or control, log the queries (a host asking for a domain registered last week is a strong signal), block known-bad domains, and remember that DNS itself can carry data out in query names if it is the one protocol allowed to reach the internet unfiltered. Encrypted DNS (DoH/DoT) to a resolver you trust protects users on hostile networks; on servers, plain DNS to an internal resolver with logging is usually the better trade.

  • Flow logs on every VPC or subnet: who talked to whom, on which port, accepted or rejected. Cheap to keep, invaluable during an incident.
  • Private endpoints for cloud services (object storage, secrets, queues) so that traffic never touches the public internet and can be restricted by policy.
  • TLS everywhere internally: encrypted transport between services is the assumption that lets you stop trusting the network.
  • No direct SSH from the internet: use identity-aware access (cloud session manager, an access proxy) with MFA and per-session logging.

Zero trust, concretely

Zero trust is a design principle, not a product: never grant access based on network location; grant it per request based on verified identity, device state and policy, and assume the network is hostile even inside the data centre. The perimeter does not disappear (you still limit exposure), but it stops being the main control.

Perimeter modelZero trust model
On the VPN means trustedEvery request authenticated and authorised, VPN or not
Internal traffic in plaintextmTLS between services; identity in the certificate
Flat internal networkSegmented; policies reference identities, not IP ranges
Access reviewed yearlyShort-lived credentials; access continuously evaluated
Log at the edgeLog every access decision, inside and out

For people, the concrete form is an identity-aware proxy in front of internal applications: users authenticate with SSO and phishing-resistant MFA, the proxy checks device compliance and policy, and the application receives a signed identity header. No VPN, no flat network access. For services, it is workload identity: each service has a certificate or token that names it, calls between services use mTLS or signed tokens, and authorisation policies say which service may call which endpoint. Service meshes package this, but the same can be done with a sidecar-free approach using SPIFFE-style identities and a small library.

Workload-level policy: only the checkout service may call the payments service, regardless of which node or IP it runs on.
yaml
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: payments-allow-checkout
  namespace: payments
spec:
  selector:
    matchLabels:
      app: payments
  action: ALLOW
  rules:
    - from:
        - source:
            principals: ["cluster.local/ns/shop/sa/checkout"]
      to:
        - operation:
            methods: ["POST"]
            paths: ["/v1/charges"]
Tip

Start zero trust with the two highest-value moves: SSO plus phishing-resistant MFA in front of every internal app (retire the VPN as a trust signal), and mTLS or signed identities between the services that handle money or personal data.

Check your exposure from the outside

Diagrams describe intent; scanners describe reality. Run an external port scan against your own public ranges on a schedule and compare the result with the list of services you intend to expose. Anything unexpected is either a misconfiguration or a forgotten system, and both are findings. Do the same from inside a private zone to confirm that the database tier really is unreachable from, say, the CI runners.

Quick external exposure check with nmap. Only scan systems you own or are authorised to test.
bash
# Which TCP ports answer on your public IP? (-Pn: do not rely on ping, -sS needs root)
sudo nmap -Pn -sS -p- --open 203.0.113.10

# What is actually listening on a host you administer, and which process owns it
sudo ss -tlnp

# Confirm the database is NOT reachable from an application host (should time out)
nc -zv -w 3 10.20.20.15 5432

Keep an inventory of internet-facing assets: domains, IPs, load balancers, object storage buckets with public access, and the SaaS consoles that hold your data. The attack surface you do not know about is the one that gets used. Cloud providers offer configuration scanners that flag public buckets, open groups and missing flow logs; turn them on, and treat a new public exposure as an alert, not a report.

Hands-on practice

Segment a small environment and prove the rules hold

  1. Draw the three-tier layout from the lesson for the zero-to-production project (or any small service): public, private, isolated. Write the allowed flows as a table of source, destination, port.
  2. In a cloud test account, create the three security groups with Terraform, referencing groups rather than CIDRs, with egress = [] on the database group. Run terraform plan and read every rule back against your table.
  3. Launch one small instance in the private tier and one in the isolated tier. From the private instance, confirm nc -zv reaches the isolated instance on 5432 and nothing else; from the isolated instance, confirm outbound curl https://example.com fails.
  4. Enable VPC flow logs, generate some traffic, and find both an ACCEPT and a REJECT record for your test connections.
  5. From your laptop run nmap -Pn -p- --open against the public IP of the load balancer or instance and list what answers. Compare with intent; close anything extra.
  6. Write a default-deny NetworkPolicy for a namespace on a local kind cluster, deploy two pods, and confirm one cannot reach the other until you add an explicit allow.
  7. Tear the test resources down.
Cheat sheet

Network security & zero trust — at a glance

Main things to focus on

  • Zones limit blast radius: public, private, isolated, with explicit flows between them
  • Security groups are stateful and can reference other groups; never open admin ports to 0.0.0.0/0
  • Egress is where exfiltration happens: default-deny outbound, allow-list destinations
  • Log DNS and network flows; a new destination is a signal
  • Zero trust = identity per request, mTLS between services, no trust from network location
  • Verify exposure with scans and asset inventory, not diagrams

Layout and rules

public / private / isolated subnetsLoad balancers / apps / data; routing decides internet reach
security_groups = [other_sg.id]Reference a tier, not an IP range
egress = []Database tier makes no outbound connections
NAT gateway + allow-listed egressPrivate tier reaches only known destinations
network ACLStateless subnet backstop; both directions must be written
description = "why"Every rule explains itself

Kubernetes and mesh

NetworkPolicy: podSelector: {} + policyTypes [Ingress, Egress]Default deny for a namespace
ingress.from.podSelector / namespaceSelectorExplicit allows by label
AuthorizationPolicy principals: [ns/sa]Service-to-service allow by workload identity
PeerAuthentication mode: STRICTRequire mTLS in a namespace (Istio)

Verification commands

sudo nmap -Pn -sS -p- --open HOSTAll open TCP ports from outside (authorised targets only)
sudo ss -tlnpListening sockets and their processes on a host
nc -zv -w 3 HOST PORTIs a port reachable from here?
dig +short NAME @RESOLVERWhat does this host resolve, via which resolver
traceroute / mtr HOSTWhich path and which hop drops the traffic

Zero trust building blocks

SSO + phishing-resistant MFAIdentity is the perimeter
identity-aware proxyInternal apps without a VPN; per-request policy
mTLS / SPIFFE identitiesServices prove who they are on every connection
device postureManaged, patched, disk-encrypted before access
log every access decisionInside and outside the old perimeter

Common pitfalls

  • Opening SSH, RDP or a database port to the world "temporarily"; scanners find it in minutes and temporary becomes permanent.
  • Leaving default allow-all egress on private tiers, giving a compromised host a free path out.
  • Treating the VPN as authentication: once connected, everything on the flat network is reachable.
  • Writing rules against IP ranges that change when instances are replaced, then widening them to make things work.
  • Assuming internal traffic is safe in plaintext; the network you trust is the one the attacker is on.
  • Never scanning your own perimeter, so the forgotten test server stays exposed for years.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →