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.
- 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.
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 destinationsInside 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.
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
}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 model | Zero trust model |
|---|---|
| On the VPN means trusted | Every request authenticated and authorised, VPN or not |
| Internal traffic in plaintext | mTLS between services; identity in the certificate |
| Flat internal network | Segmented; policies reference identities, not IP ranges |
| Access reviewed yearly | Short-lived credentials; access continuously evaluated |
| Log at the edge | Log 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.
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"]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.
# 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 5432Keep 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.