Networking
for DevOps
Complete notes for absolute beginners. They follow a real request from a user's browser, through DNS, HTTPS, Nginx, Docker, Kubernetes and the cloud, all the way to a database and back.
Why networking matters in DevOps
Every deployment, container, cluster and cloud account depends on one question: can A talk to B? Networking is the skill that lets you answer it.
As a DevOps engineer you will spend a lot of time making things talk to each other: a browser to a website, Nginx to a backend, a backend to a database, a container to another container, a Kubernetes pod to an external API, a server in one cloud subnet to a server in another. When one of those conversations fails, someone has to work out why. Usually that someone is you.
You do not need to become a network engineer who configures Cisco routers. You do need enough networking to deploy, secure and troubleshoot applications on your own. That is exactly what these notes teach.
It's Friday evening and your company's website shows "502 Bad Gateway". The developer says "the code is fine". The database admin says "the database is fine". The networking knowledge in these notes is what lets you find out in five minutes that the backend container restarted on a different port, so Nginx is knocking on an empty door.
What you will be able to do
🌐 Explain
How a URL typed in a browser becomes packets, then reaches the right server, port and process.
🛠️ Configure
IP addressing, DNS records, firewalls, Nginx reverse proxies, HTTPS, Docker networks, Kubernetes Services and cloud VPCs.
🔍 Troubleshoot
"Site not opening", 502/504 errors, DNS failures, blocked ports, unreachable databases and containers that can't see each other.
🔒 Secure
Expose only what must be public and keep databases private, using SSH keys, TLS, least-privilege firewall rules and segmentation.
How these notes are organised
Each module follows the same pattern: concept → analogy → diagram → real-world example → hands-on lab → troubleshooting → quiz. The coloured boxes mean:
Command blocks look like this. The green $ is the prompt (don't type it), and grey text is sample output. On screen, the Copy button copies only the commands.
$ echo "Hello, network!" Hello, network!
Contents
Set up your practice lab
Networking is learned with your hands. One Ubuntu machine is all you need for almost every lab in these notes.
Option A: a local virtual machine (free, safest)
- Install VirtualBox (or VMware Workstation Player / Hyper-V / UTM on Mac).
- Download the Ubuntu Server 24.04 LTS ISO (22.04 also works).
- Create a VM: 2 CPU, 2–4 GB RAM, 20 GB disk. Choose the Bridged Adapter network so the VM gets an IP on your home network.
- During installation, tick Install OpenSSH server.
Option B: a cheap cloud VPS (most realistic)
Any provider works (Hetzner, DigitalOcean, Vultr, Linode, Contabo, AWS Lightsail, an Oracle Cloud free-tier VM and so on). Pick Ubuntu 24.04. A VPS gives you a real public IP, which makes the DNS, HTTPS and firewall labs feel exactly like production.
On a remote VPS, a wrong firewall command can cut off your own SSH session. Before you ever run ufw enable, always run sudo ufw allow OpenSSH first. Most providers also offer a web "console" you can use to get back in if that happens.
Install the tools used in these notes
$ sudo apt update $ sudo apt install -y iproute2 iputils-ping traceroute mtr-tiny dnsutils \ netcat-openbsd curl wget tcpdump nmap ufw nginx python3 jq # Docker (official convenience script – fine for a lab machine) $ curl -fsSL https://get.docker.com | sudo sh $ sudo usermod -aG docker $USER # log out and back in afterwards
dnsutils gives you dig and nslookup. mtr-tiny gives you mtr, a live combination of ping and traceroute. Kubernetes (k3s) is installed later, in Module 7.
Connect to your lab machine with SSH from your own laptop (ssh user@VM-IP) instead of typing in the VM window. You'll get copy-paste, and you'll be practising a networking skill from day one.
Meet ShopNow
The same small online shop appears in every module, and its infrastructure grows as you learn.
ShopNow is an imaginary online store at shop.example.com. It has three parts, just like most real web applications:
🖥️ Frontend
A React website that runs in the customer's browser.
⚙️ Backend API
A Node.js/Python service listening on port 5000 that handles orders, logins and payments.
💾 PostgreSQL
The database holding products and orders, on port 5432. It must never be reachable from the internet.
By the end of these notes you will be able to explain every arrow in this picture, build it on a VPS, move it into Docker and then Kubernetes, place it in a cloud VPC, secure it, and troubleshoot it when it breaks.
Expose the minimum. Users talk to Nginx (ports 80/443). Nginx talks to the backend. Only the backend talks to the database. Every module reinforces this principle.
Networking fundamentals
The alphabet of networking: what a network is, how devices are addressed, and how traffic finds its way out of your network.
- Tell a LAN, a WAN, the Internet and an intranet apart
- Read an IPv4 and IPv6 address
- Say whether an IP is public or private
- Explain the difference between MAC and IP
- Calculate a subnet's range from its CIDR
- Explain what a default gateway does
1.1 What is a network?
A network is two or more devices (called nodes or hosts) connected so they can exchange data. The connection can be a cable (Ethernet), radio waves (Wi-Fi, 4G/5G) or fibre-optic light. The devices can be laptops, phones, servers, printers, routers, containers, or even virtual machines inside a single physical server.
Think of a network as a country's postal system. Every house has an address (IP address). Letters are packets. Local post offices are switches and routers, and the roads between cities are the links. You never drive your letter to the destination yourself. You hand it to the system and it gets forwarded hop by hop. Networking works the same way.
Types of networks
| Type | What it is | Real-life example | Why a DevOps engineer cares |
|---|---|---|---|
| LAN Local Area Network | Devices in one building or site, usually connected by a switch or Wi-Fi router. Fast and private. | Your home Wi-Fi; an office floor; servers in one data-centre rack. | Servers in the same LAN or VPC subnet talk directly and quickly. |
| WAN Wide Area Network | Connects LANs over long distances using ISP lines, leased circuits or VPN tunnels. | A bank connecting branch offices in different cities to its head office. | Linking cloud regions, branch offices and on-premises data centres. |
| Internet | The global "network of networks", where thousands of ISPs and companies interconnect using public IP addresses. | Opening youtube.com from your phone. | Where your users come from. Anything exposed here can be attacked. |
| Intranet | A private network that uses Internet technology (web, DNS, HTTP) but is only for one organisation's members. | A company's HR portal, wiki or Jenkins server that only opens on office Wi-Fi or VPN. | Internal tools (Grafana, CI servers, admin panels) belong here, not on the public Internet. |
1.2 Client-server architecture
Almost everything in DevOps follows the client-server model:
- A server is a program that runs continuously and listens on a network address and port, waiting for requests. Examples are Nginx, PostgreSQL and an SSH daemon.
- A client is a program that starts the conversation by sending a request. Examples are a browser,
curl,psqland your SSH app.
When something "can't connect", always ask two questions: who is the client and who is the server? Then check that the server is listening, that the client is using the right address and port, and that nothing in between (a firewall, a security group or a Docker network) is blocking the path. That one habit solves most connectivity problems.
In peer-to-peer (P2P) networks every node is both client and server, as in BitTorrent or some video-call systems. You'll rarely build P2P systems in DevOps, but the term comes up in interviews.
1.3 IP addresses: IPv4 and IPv6
An IP address (Internet Protocol address) is a number that identifies a device's network interface so that packets can be delivered to it. Every packet carries a source IP (who sent it) and a destination IP (where it's going).
IPv4
IPv4 addresses are 32 bits long, written as four decimal numbers (called octets) separated by dots. Each octet is 8 bits, so it can only be 0 to 255.
4.3 billion addresses sounds like a lot, but the world ran out of fresh IPv4 blocks years ago. That's why we have private addresses + NAT (section 1.4) and IPv6.
IPv6
IPv6 addresses are 128 bits, written as eight groups of four hexadecimal digits separated by colons:
# Full form 2001:0db8:0000:0000:0000:ff00:0042:8329 # Rule 1: drop leading zeros in each group 2001:db8:0:0:0:ff00:42:8329 # Rule 2: replace ONE run of all-zero groups with :: 2001:db8::ff00:42:8329
| IPv4 | IPv6 | |
|---|---|---|
| Size | 32 bits | 128 bits |
| Format | 203.0.113.10 | 2001:db8::10 |
| Number of addresses | ~4.3 billion | ~340 undecillion (3.4 × 10³⁸), effectively unlimited |
| Loopback (this machine) | 127.0.0.1 | ::1 |
| Private / local | 10.x, 172.16–31.x, 192.168.x | fd00::/8 (unique local), fe80::/10 (link-local) |
| NAT needed? | Usually yes | Usually no: every device can get a global address |
| DNS record | A | AAAA |
Special IPv4 addresses every DevOps engineer must know
| Address | Meaning | Where you'll see it |
|---|---|---|
127.0.0.1 (and all of 127.0.0.0/8) | Loopback, "this machine itself". Traffic never leaves the computer. | curl http://127.0.0.1:5000 to test an app locally. localhost resolves to it. |
0.0.0.0 | When a server binds to it, it means "listen on ALL interfaces". In a route it means "any destination" (the default route). | app.run(host="0.0.0.0"), Docker port bindings, ss -tln output. |
169.254.x.x | Link-local: a device gave itself this address because no DHCP server answered. | Seeing it on a server usually means a DHCP problem. In the cloud, 169.254.169.254 is the instance metadata service. |
255.255.255.255 | Broadcast to everyone on the local network. | DHCP discovery. |
An app that listens on 127.0.0.1:5000 can only be reached from the same machine. It works with curl localhost:5000 on the server and fails from everywhere else, including from other Docker containers. If something outside needs to connect, the app must listen on 0.0.0.0 (or on the server's specific IP). Then protect the port with a firewall.
1.4 Public vs private IP addresses
Public IPs are globally unique and routable on the Internet. They're assigned by ISPs and cloud providers. Private IPs are reserved ranges (defined in RFC 1918) that anyone can reuse inside their own network. Internet routers will not carry them.
| Private range | CIDR | Addresses | Typically used by |
|---|---|---|---|
10.0.0.0 – 10.255.255.255 | 10.0.0.0/8 | 16.7 million | Cloud VPCs (AWS default VPC uses 172.31/16, but custom VPCs often use 10.x), Kubernetes pod networks, corporate networks |
172.16.0.0 – 172.31.255.255 | 172.16.0.0/12 | 1 million | Docker (172.17.0.0/16 is the default bridge), AWS default VPCs |
192.168.0.0 – 192.168.255.255 | 192.168.0.0/16 | 65,536 | Home and small-office routers |
So how does a laptop with a private IP open Google? Through NAT (Network Address Translation). Your router swaps your private source IP for its single public IP on the way out, remembers the mapping, and swaps it back when the reply arrives.
When you rent a VPS, it usually gets a public IP directly on its network card. In AWS, an EC2 instance only has a private IP (e.g. 10.0.1.25) on its card, and AWS maps a public/Elastic IP to it at the edge. That's why ip addr inside EC2 never shows the public IP. To see the public IP, run curl ifconfig.me.
Many mobile and home ISPs put customers behind Carrier-Grade NAT using 100.64.0.0/10. Your "public" router IP is then actually shared with other customers. That's why you often can't host a server at home without a tunnel or VPS.
1.5 MAC addresses and ARP
A MAC address (Media Access Control) is a 48-bit hardware address burned into each network interface (it can be changed in software). It's written as six hex pairs, e.g. 08:00:27:4e:66:a1. The first three pairs identify the manufacturer.
IP address = "postal address"
Logical, and assigned by the network. It changes when you move to a different network, and it's used to route packets across networks.
MAC address = "name on the door"
Physical, and tied to the network card. It's used to deliver frames within one local network (one hop). Routers replace it at every hop.
Before your laptop can send anything to its router, it needs the router's MAC address. It finds it with ARP (Address Resolution Protocol):
Switches forward traffic using MAC addresses inside a LAN (Layer 2). Routers forward traffic using IP addresses between networks (Layer 3). You'll meet these "layers" in Module 2.
1.6 Subnet masks, CIDR and subnetting
Every IP address has two parts: a network part (which "street") and a host part (which "house" on that street). The subnet mask says where the split happens.
| Written as a mask | Written as CIDR | Meaning |
|---|---|---|
255.0.0.0 | /8 | First 8 bits are network; 24 bits for hosts |
255.255.0.0 | /16 | First 16 bits are network; 16 bits for hosts |
255.255.255.0 | /24 | First 24 bits are network; 8 bits for hosts |
255.255.255.192 | /26 | First 26 bits are network; 6 bits for hosts |
CIDR (Classless Inter-Domain Routing) notation is just the short way of writing this: 192.168.1.0/24 means "the network whose first 24 bits are 192.168.1". It's what you'll type in cloud consoles, Docker, Kubernetes and firewall rules.
Addresses in a block = 2(32 − prefix). Usable hosts = that number − 2. The first address is the network address and the last is the broadcast address, and neither can be given to a host. (AWS reserves 5 per subnet, not 2.)
| CIDR | Addresses | Usable hosts | Typical use |
|---|---|---|---|
/32 | 1 | 1 (a single host) | Firewall rule for exactly one IP, e.g. "allow SSH from 198.51.100.7/32" |
/30 | 4 | 2 | Point-to-point router links |
/28 | 16 | 14 | A tiny subnet for a few servers |
/26 | 64 | 62 | A small department or application tier |
/24 | 256 | 254 | Home network; a typical cloud subnet |
/20 | 4,096 | 4,094 | A large cloud subnet (e.g. AWS default subnets) for big EKS clusters |
/16 | 65,536 | 65,534 | A whole VPC; Docker's default bridge |
/0 | all 4.3 billion | — | 0.0.0.0/0 = "anywhere on the Internet" |
Subnetting: splitting one network into smaller ones
Why split? So that web servers, application servers and databases can live in separate subnets with different security rules, and so that addresses aren't wasted. Here's 192.168.10.0/24 split into four /26 subnets:
Worked example: the "block size" trick
Question: Which subnet does 192.168.1.130/26 belong to, and what is its usable range?
Find which octet the prefix falls in. /26 sits in the 4th octet (24 < 26 ≤ 32).
Work out the block size: 2(32−26) = 64. So subnets start at .0, .64, .128 and .192.
130 falls between 128 and 191, so the subnet is 192.168.1.128/26.
The network address is .128, the broadcast is .191 (the next block start minus one), and the usable hosts are .129 – .190 (62 hosts).
Check your answer on Linux with ipcalc (installed in the lab below).
An AWS VPC 10.0.0.0/16 holds public subnets 10.0.1.0/24 and 10.0.2.0/24 and private subnets 10.0.11.0/24 and 10.0.12.0/24. Docker's default bridge is 172.17.0.0/16. k3s gives pods 10.42.0.0/16 and Services 10.43.0.0/16. A firewall rule "allow 5432 from 10.0.11.0/24" means "only the app subnet may reach the database".
If two networks you want to connect (two VPCs, an office and a VPC, or a VPN and Docker) use the same or overlapping ranges, routing breaks, because the computer can't tell which 10.0.1.5 you mean. Plan non-overlapping ranges before building. Changing them later is painful.
1.7 Default gateway and routing basics
When your computer sends a packet, it first asks: "Is the destination in my own subnet?"
- Yes: deliver it directly (using ARP to find the MAC).
- No: send it to the default gateway, the router that is the "door" out of the local network. The router then forwards it hop by hop towards the destination.
A routing table is a list of "destination → next hop" rules. When several rules match, the most specific one (the longest prefix) wins. A route for 10.0.5.0/24 beats 10.0.0.0/16, which beats 0.0.0.0/0 (the default route).
The default gateway is the reception desk of an office building. Mail for someone inside the building is delivered directly. Everything else goes to reception, which knows how to send it out.
When a device joins a network, it uses DHCP to ask for configuration. The four-step exchange is called DORA: Discover → Offer → Request → Acknowledge. The DHCP server (usually the router) hands out an IP, subnet mask, default gateway and DNS servers. Servers in production often use static IPs, or DHCP reservations, so their address never changes.
Show all network interfaces and their addresses:
$ ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ... inet 127.0.0.1/8 scope host lo 2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ... state UP link/ether 08:00:27:4e:66:a1 brd ff:ff:ff:ff:ff:ff inet 192.168.1.50/24 brd 192.168.1.255 scope global dynamic enp0s3 inet6 fe80::a00:27ff:fe4e:66a1/64 scope link
Reading the output: lo is loopback; enp0s3 (it may be eth0 or ens3 on your machine) is the real interface. link/ether is the MAC, inet is the IPv4/CIDR, and inet6 fe80:: is the IPv6 link-local address.
Find the default gateway:
$ ip route default via 192.168.1.1 dev enp0s3 proto dhcp src 192.168.1.50 metric 100 192.168.1.0/24 dev enp0s3 proto kernel scope link src 192.168.1.50
Compare your private IP with your public IP:
$ hostname -I # all local IPs 192.168.1.50 $ curl -s ifconfig.me ; echo # the IP the Internet sees 203.0.113.25
If the two are different, you are behind NAT. On a VPS they're often the same.
Look at the ARP / neighbour cache (IP → MAC):
$ ping -c 2 192.168.1.1 > /dev/null ; ip neigh 192.168.1.1 dev enp0s3 lladdr aa:bb:cc:dd:ee:01 REACHABLE
Practise subnetting with ipcalc:
$ sudo apt install -y ipcalc $ ipcalc 192.168.1.130/26 Address: 192.168.1.130 Netmask: 255.255.255.192 = 26 Network: 192.168.1.128/26 HostMin: 192.168.1.129 HostMax: 192.168.1.190 Broadcast: 192.168.1.191 Hosts/Net: 62 Class C, Private Internet # (output trimmed)
Now try ipcalc 10.0.0.0/16, ipcalc 172.17.0.0/16 and ipcalc 10.0.1.0/28. Predict each answer on paper first.
See the 127.0.0.1 vs 0.0.0.0 difference for yourself:
$ python3 -m http.server 8000 --bind 127.0.0.1 # terminal 1 $ curl -I http://127.0.0.1:8000 # terminal 2 → works # From your laptop's browser: http://VM-IP:8000 → fails. Stop it (Ctrl+C) and run: $ python3 -m http.server 8000 --bind 0.0.0.0 # Now http://VM-IP:8000 works from your laptop
1.8 Troubleshooting scenarios
🚑 "It works on the server but not from my browser"
curl localhost:5000 on the server returns the page, but http://SERVER-IP:5000 from a laptop times out or is refused.127.0.0.1, or a firewall blocks port 5000.ss -tlnp | grep 5000. If it shows 127.0.0.1:5000, change the app to listen on 0.0.0.0. Better still, keep it on 127.0.0.1 and put Nginx in front (Module 5).🚑 "The VPN connects, but I can't reach the office server"
ping 192.168.1.20 (an office server) gets no reply.192.168.1.0/24. Your laptop thinks 192.168.1.20 is on your home LAN and never sends the traffic into the VPN.10.20.0.0/16. You can confirm the problem with ip route get 192.168.1.20, which shows which interface would be used.🔑 Key takeaways
- A network is connected devices exchanging packets; the Internet is a network of networks.
- Servers listen and clients start the conversation, and one program can be both.
- IPv4 is 32 bits (four octets of 0–255); IPv6 is 128 bits in hex.
- Private ranges (10/8, 172.16/12, 192.168/16) aren't routable on the Internet. NAT shares one public IP.
- MAC addresses deliver within a LAN; IP addresses route between networks. ARP maps IP → MAC.
- CIDR /n: block size = 2(32−n). Network and broadcast addresses aren't usable.
- Traffic for other networks goes to the default gateway, and the most specific route wins.
127.0.0.1= this machine only; binding to0.0.0.0= all interfaces.
📝 Quiz: Module 1
1. Which of these is a private IP address?
- 8.8.8.8
- 172.20.5.4
- 172.32.1.1
- 203.0.113.9
Show answer
2. How many usable host addresses does a /27 subnet have?
Show answer
3. What is the network address and broadcast address of 10.0.0.77/28?
Show answer
4. A Flask app is started with app.run(host="127.0.0.1", port=5000). Can a different server in the same subnet reach it?
Show answer
0.0.0.0 to listen on all interfaces.5. What does ARP do?
- Translates domain names to IPs
- Finds the MAC address for a known IP on the local network
- Assigns IP addresses automatically
- Encrypts traffic
Show answer
6. Your laptop is 192.168.1.10/24. It sends a packet to 192.168.1.200. Does the packet go to the default gateway?
Show answer
7. Which route wins for destination 10.1.2.3: 0.0.0.0/0, 10.0.0.0/8 or 10.1.0.0/16?
Show answer
8. Why doesn't ip addr on an AWS EC2 instance show its public IP?
Show answer
Networking models & protocols
How the work of networking is split into layers, and the protocols (rules of conversation) you will use every single day.
- Name the OSI and TCP/IP layers and place protocols in them
- Explain encapsulation
- Choose between TCP and UDP
- Draw the TCP 3-way handshake
- Tell "connection refused" from "timed out"
- Remember the common ports
- Read an HTTP request and response
- Use SSH, SFTP and ICMP tools
2.1 Why do we need layers?
Sending data across the world involves many separate problems: turning bits into electrical signals, finding the next device, routing across continents, making delivery reliable, and making sure both apps speak the same language. Instead of one giant program solving all of that, networking splits the work into layers. Each layer does one job and only talks to the layer directly above and below it.
You write a letter (application), put it in an envelope with a tracking number (transport), write the destination address (network), and the courier loads it on a truck for the next depot (data link), which drives on actual roads (physical). The truck driver never reads your letter, and you never choose the truck's route. Each layer trusts the others to do their job.
2.2 The OSI model and the TCP/IP model
The OSI model (7 layers) is a teaching and troubleshooting reference. The TCP/IP model (4 layers) is what the Internet actually runs on. You need both: interviews and documentation say "Layer 4 load balancer" or "Layer 7 firewall" using OSI numbers.
From layer 1 up to layer 7: Please Do Not Throw Sausage Pizza Away: Physical, Data link, Network, Transport, Session, Presentation, Application.
- "L4 load balancer" (e.g. AWS NLB) forwards TCP/UDP connections by IP and port. It doesn't read HTTP.
- "L7 load balancer" (e.g. Nginx, AWS ALB) reads HTTP, so it can route
/apiand/imagesto different servers. - "It's a Layer 1 problem" = the cable is unplugged or the interface is down. "Layer 3 issue" = wrong IP or route.
2.3 Encapsulation: how data is wrapped for travel
As your data moves down the layers on the sender, each layer adds its own header (an envelope with that layer's information). The receiver unwraps them in reverse order, going up. This is called encapsulation and decapsulation.
As a packet crosses routers, the MAC addresses (L2) are rewritten at every hop, but the source and destination IP (L3) stay the same end to end (unless NAT changes them). Each router also lowers the packet's TTL (time to live) by 1 and discards it at 0. That prevents endless loops, and it's how traceroute works. A frame is limited by the MTU, usually 1500 bytes on Ethernet.
2.4 TCP vs UDP
At the Transport layer there are two main protocols. Both use ports, but they behave very differently.
| Feature | TCP | UDP |
|---|---|---|
| Connection | Connection-oriented (3-way handshake first) | Connectionless: just send |
| Reliability | Guaranteed delivery, re-transmits lost data | Best effort, no retries |
| Order | Data arrives in order | May arrive out of order |
| Speed / overhead | Slower, bigger header (20+ bytes) | Faster, 8-byte header |
| Analogy | 📞 A phone call: "Hello?" "Hi!" … "Bye." | 📮 Throwing postcards in a mailbox |
| DevOps examples | Web (80/443), SSH (22), PostgreSQL (5432), MySQL (3306), Redis (6379) | DNS (53), NTP (123), syslog (514), WireGuard (51820), QUIC/HTTP/3 (443/udp) |
Firewall rules and Docker/Kubernetes port settings are per protocol. Opening 53/tcp doesn't open 53/udp. When a DNS server or WireGuard VPN "doesn't work", check that you allowed UDP.
2.5 The TCP three-way handshake
Before any TCP data flows, client and server agree to talk using three messages: SYN → SYN-ACK → ACK.
ss output."Connection refused" vs "connection timed out": the most useful clue in networking
Refused = fast "no": you reached the machine, but the door (port) is closed, so check the app. Timeout = no answer at all: something in the path is silently dropping you, so check firewalls, security groups, IPs and routes.
2.6 Ports and sockets
An IP address gets traffic to the right machine. A port (a number from 0 to 65535) gets it to the right program on that machine. A socket is the combination IP:port, for example 203.0.113.10:443.
The IP address is an apartment building's street address. The port is the flat number. The postman (the network) gets the parcel to the building, and the flat number decides whether it goes to Nginx (flat 443), SSH (flat 22) or PostgreSQL (flat 5432).
| Range | Name | Notes |
|---|---|---|
0 – 1023 | Well-known ports | Standard services (22, 80, 443). On Linux, binding to them needs root or CAP_NET_BIND_SERVICE. |
1024 – 49151 | Registered ports | Applications: 3000, 3306, 5432, 6379, 8080, 9090… |
49152 – 65535 | Ephemeral / dynamic | Random source ports for outgoing client connections. (Linux actually uses 32768–60999 by default.) |
Ports you should know by heart
| Port | Protocol | Service | Port | Protocol | Service |
|---|---|---|---|---|---|
| 22 | TCP | SSH / SFTP / SCP | 3306 | TCP | MySQL / MariaDB |
| 25 / 587 | TCP | SMTP (mail sending) | 5432 | TCP | PostgreSQL |
| 53 | UDP + TCP | DNS | 6379 | TCP | Redis |
| 80 | TCP | HTTP | 27017 | TCP | MongoDB |
| 443 | TCP (+UDP for HTTP/3) | HTTPS | 6443 | TCP | Kubernetes API server |
| 20 / 21 | TCP | FTP (insecure) | 3000 / 8080 | TCP | Common dev/app ports (Grafana uses 3000) |
| 123 | UDP | NTP (time sync) | 9090 / 9100 | TCP | Prometheus / node_exporter |
2.7 HTTP and HTTPS
HTTP (HyperText Transfer Protocol) is the language of the web and of almost every API. It is plain text: a client sends a request, and the server returns a response. HTTPS is the same HTTP sent inside an encrypted TLS tunnel. Module 5 covers TLS in detail.
GET /api/products?page=2 HTTP/1.1 ← method, path, version Host: shop.example.com ← which website User-Agent: Mozilla/5.0 ... Accept: application/json Authorization: Bearer eyJhbGc... Cookie: session=abc123 ← blank line (body: empty for GET; JSON for POST)
HTTP/1.1 200 OK ← status Content-Type: application/json Content-Length: 512 Cache-Control: max-age=60 Server: nginx ← blank line {"products":[{"id":7,"name":"Shoes"}]}
Common methods
GET read · POST create · PUT/PATCH update · DELETE remove · HEAD headers only (curl -I) · OPTIONS CORS pre-flight
Status code families
1xx info · 2xx success · 3xx redirect · 4xx client's fault · 5xx server's fault
| Code | Meaning | What it usually means for a DevOps engineer |
|---|---|---|
| 200 OK / 201 Created | Success | All good. |
| 301 / 302 / 308 | Redirect | HTTP → HTTPS redirects, or www → non-www. Loops here mean misconfigured proxies. |
| 400 Bad Request | Malformed request | Client bug, or a header too large for the proxy. |
| 401 / 403 | Unauthenticated / Forbidden | A missing/expired token (401) or no permission (403). It can also be a Nginx deny rule or file permissions. |
| 404 Not Found | No such path | Wrong URL, wrong Nginx location/root, or a wrong Ingress path. |
| 413 Payload Too Large | Body too big | Raise client_max_body_size in Nginx for uploads. |
| 500 Internal Server Error | App crashed | Read the application logs: it's a code or config bug. |
| 502 Bad Gateway | Proxy got a bad/no response from upstream | The backend is down, on the wrong port, or crashed. The most common networking 5xx. |
| 503 Service Unavailable | Temporarily can't serve | No healthy backends, maintenance, or rate limiting. |
| 504 Gateway Timeout | Upstream too slow | The backend is alive but slow (a slow DB query), or a firewall is dropping proxy→backend traffic. |
2.8 SSH, FTP/SFTP and ICMP
SSH: secure remote access (TCP 22)
SSH (Secure Shell) gives you an encrypted terminal on a remote server. It's the DevOps engineer's front door to every Linux machine. It also carries file transfers (SCP/SFTP) and can create tunnels.
$ ssh ubuntu@203.0.113.10 # log in $ ssh -i ~/.ssh/shop_key -p 2222 deploy@203.0.113.10 # custom key and port $ scp app.tar.gz deploy@203.0.113.10:/opt/shop/ # copy a file up $ sftp deploy@203.0.113.10 # interactive file transfer over SSH
$ ssh -N -L 5432:127.0.0.1:5432 deploy@203.0.113.10 # -L localPort:destinationHost:destinationPort (destination as seen FROM the server) # -N don't open a shell, just keep the tunnel # Now connect pgAdmin/psql on your laptop to localhost:5432
Companies keep databases private and give engineers access through a bastion host (jump server) with SSH tunnels, or through a VPN. ssh -J bastion user@private-server jumps through the bastion in one command.
FTP vs SFTP vs SCP
| Tool | Port | Encrypted? | Use it? |
|---|---|---|---|
| FTP | 21 (control) + 20/random (data) | ❌ No. Passwords travel in plain text. | Avoid. Legacy only (FTPS adds TLS). |
| SFTP | 22 (over SSH) | ✅ Yes | ✅ Standard for file transfer to servers. |
| SCP / rsync over SSH | 22 | ✅ Yes | ✅ Quick copies; rsync is great for deployments and backups. |
ICMP: the network's messenger (ping and traceroute)
ICMP (Internet Control Message Protocol) is a Layer 3 helper protocol with no ports. It carries error and diagnostic messages such as "echo request/reply" (ping), "destination unreachable" and "time exceeded" (used by traceroute).
$ ping -c 4 8.8.8.8 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=12.4 ms ... 4 packets transmitted, 4 received, 0% packet loss, time 3004ms rtt min/avg/max/mdev = 12.1/12.5/13.0/0.3 ms $ traceroute -n google.com # each line = one router hop $ mtr -rwc 20 google.com # traceroute + ping combined, with loss % per hop
Many servers and clouds block ICMP. Azure VMs and AWS security groups drop ping by default unless you allow it. A failed ping does not prove a server is down. Test the real port instead: nc -vz host 443 or curl -I https://host.
nc, see the handshake with tcpdump, and read real HTTP headers.See which ports are listening on your machine:
$ sudo ss -tulnp Netid State Local Address:Port Process udp UNCONN 127.0.0.53%lo:53 users:(("systemd-resolve",pid=612)) tcp LISTEN 0.0.0.0:22 users:(("sshd",pid=901)) tcp LISTEN 0.0.0.0:80 users:(("nginx",pid=1201))
-t TCP, -u UDP, -l listening, -n numeric, -p process. Memorise ss -tulnp.
Terminal 1: start capturing packets on port 9000. Terminal 2: start a TCP server with netcat.
$ sudo tcpdump -i lo -n tcp port 9000 # terminal 1 $ nc -l 9000 # terminal 2 (server)
Terminal 3: connect as a client and type a message:
$ nc 127.0.0.1 9000
hello server!
In terminal 1 you'll see the handshake. [S] = SYN, [S.] = SYN-ACK, [.] = ACK, [P.] = data, [F.] = FIN:
IP 127.0.0.1.45678 > 127.0.0.1.9000: Flags [S], seq 1234...
IP 127.0.0.1.9000 > 127.0.0.1.45678: Flags [S.], seq 9876..., ack 1235
IP 127.0.0.1.45678 > 127.0.0.1.9000: Flags [.], ack 1
IP 127.0.0.1.45678 > 127.0.0.1.9000: Flags [P.], length 14Produce the two famous errors yourself:
$ nc -vz 127.0.0.1 9999 # nothing listening nc: connect to 127.0.0.1 port 9999 (tcp) failed: Connection refused $ nc -vz -w 5 10.255.255.1 22 # an IP that doesn't answer nc: connect to 10.255.255.1 port 22 (tcp) timed out: Operation now in progress
Try UDP. There's no handshake, so check tcpdump: you'll see a single packet.
$ nc -u -l 9001 # UDP server $ echo "hi" | nc -u -w1 127.0.0.1 9001 # UDP client
Read a real HTTP conversation, including DNS, TCP, TLS and headers:
$ curl -v https://example.com -o /dev/null * Trying 93.184.215.14:443... * Connected to example.com (93.184.215.14) port 443 * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 > GET / HTTP/2 > Host: example.com < HTTP/2 200 < content-type: text/html $ curl -I http://example.com # headers only
Lines starting with * are connection info, > is what you sent, and < is what the server replied. (The IP you see may differ.)
2.9 Troubleshooting scenarios
🚑 "ping fails, but the website opens"
ping shop.example.com shows 100% packet loss, yet the browser loads the site fine.curl -I https://shop.example.com or nc -vz shop.example.com 443. Allow ICMP only if your monitoring needs it.🚑 "psql: connection refused" vs "timeout"
ECONNREFUSED 10.0.11.20:5432 on one day and ETIMEDOUT on another.sudo ss -tlnp | grep 5432, check listen_addresses in postgresql.conf, then restart. Timeout → check UFW/security group rules from the backend's IP, then nc -vz 10.0.11.20 5432 from the backend host.🔑 Key takeaways
- OSI has 7 layers and TCP/IP has 4. DevOps cares most about L7 (HTTP/DNS), L4 (TCP/UDP + ports) and L3 (IP).
- Each layer wraps the data in its own header (encapsulation).
- TCP is reliable and ordered, with a SYN → SYN-ACK → ACK handshake. UDP is fast with no guarantees.
- Refused = reached the host, but no listener. Timed out = dropped in the path.
- IP finds the machine and the port finds the program.
ss -tulnpshows the listeners. - HTTPS = HTTP inside TLS. Learn your status codes, especially 502 and 504.
- Use SSH/SFTP (22), never plain FTP. SSH tunnels give safe access to private services.
- ICMP (ping) is often blocked, so test the actual port.
📝 Quiz: Module 2
1. At which OSI layer do TCP and UDP work, and what does that layer add?
Show answer
2. Put the handshake in order: ACK, SYN, SYN-ACK.
Show answer
3. Which protocol does DNS mainly use, and why?
- TCP, for reliability
- UDP, because queries are tiny and need to be fast
- ICMP
- HTTP
Show answer
4. curl http://10.0.0.5:8080 instantly returns "Connection refused". What does this tell you?
Show answer
5. Nginx returns 502 Bad Gateway. Which component do you check first?
Show answer
proxy_pass? Read the Nginx error log for "connect() failed (111: Connection refused)".6. Which is the secure way to transfer files to a server: FTP, SFTP or Telnet?
Show answer
7. What does this command do: ssh -N -L 6379:127.0.0.1:6379 deploy@203.0.113.10?
Show answer
8. How can thousands of users connect to the same server port 443 at once?
Show answer
DNS & domain management
DNS is the Internet's phonebook. It turns shop.example.com into an IP address. When DNS breaks, everything looks broken.
- Break a URL into its parts
- Explain step by step how a name becomes an IP
- Create the right DNS record for each job
- Plan TTLs for a safe migration
- Explain registrar vs DNS host
- Debug DNS with
dig
3.1 What is DNS and why does it exist?
Computers connect using IP addresses, but humans remember names. The Domain Name System (DNS) is a huge, distributed database that answers the question "What is the IP address of this name?" (and a few other questions, such as "Which server handles this domain's email?").
You tap "Mom", not her number. Your phone looks up the name and dials the number. DNS does the same for the Internet. And just as Mom can change her number while you keep tapping "Mom", you can move a website to a new server by updating its DNS record. Users keep typing the same domain.
Anatomy of a URL and a domain name
Domains form a tree, read from right to left. There is an invisible root . at the very end, so shop.example.com. is the "fully qualified" form.
3.2 How DNS resolution works, step by step
- Your computer checks its own caches and
/etc/hosts. If there's no answer, it asks its configured recursive resolver: "What is shop.example.com?" - The resolver (if it has nothing cached) asks a root server.
- The root replies: "I don't know, but here are the
.comTLD servers." - The resolver asks a .com TLD server.
- The TLD server replies: "example.com's authoritative nameservers are ns1.dns-host.net and ns2.dns-host.net."
- The resolver asks the authoritative nameserver.
- The authoritative server answers: "
shop.example.com A 203.0.113.10, TTL 300." - The resolver caches this for 300 seconds and returns it to your computer, which caches it too. Now the browser can open a TCP connection to 203.0.113.10.
Recursive resolver = the one that asks around on your behalf (ISP, Google 8.8.8.8, Cloudflare 1.1.1.1). Authoritative nameserver = the one that owns the answer for your domain (Cloudflare DNS, AWS Route 53, your registrar's DNS).
3.3 DNS record types
| Record | Purpose | Example | Real-life use |
|---|---|---|---|
| A | Name → IPv4 address | shop.example.com → 203.0.113.10 | Point your domain at a VPS or load balancer. |
| AAAA | Name → IPv6 address | shop.example.com → 2001:db8::10 | Same as A, for IPv6. |
| CNAME | Name → another name (an alias) | www.example.com → example.comapp.example.com → myapp.herokuapp.com | Point to a cloud service whose IP changes (load balancers, CDNs, SaaS). |
| MX | Which servers receive email (with priority) | example.com MX 10 mail.example.com | Google Workspace / Microsoft 365 mail setup. |
| TXT | Free text, used for verification and email security | "v=spf1 include:_spf.google.com ~all" | SPF, DKIM, DMARC; domain verification for Google, Let's Encrypt DNS challenges and so on. |
| NS | Which nameservers are authoritative for the zone | example.com NS ns1.dns-host.net | Delegating your domain to Cloudflare/Route 53. |
| SOA | Zone metadata: primary NS, admin, serial, timers | Created automatically | Rarely edited by hand. |
| PTR | IP → name (reverse DNS) | 10.113.0.203.in-addr.arpa → mail.example.com | Mail server reputation; set in your VPS/IP provider's panel. |
| CAA | Which CAs may issue certificates for the domain | example.com CAA 0 issue "letsencrypt.org" | Extra protection against mis-issued certificates. |
| SRV | Service location (host + port) | _sip._tcp.example.com | VoIP, LDAP, some Kubernetes/Consul discovery. |
Here is what ShopNow's zone might look like in a DNS provider's dashboard (shown in standard "zone file" format):
; name TTL class type value
example.com. 3600 IN A 203.0.113.10
www 3600 IN CNAME example.com.
shop 300 IN A 203.0.113.10
api 300 IN A 203.0.113.11
example.com. 3600 IN MX 10 mail.example.com.
mail 3600 IN A 203.0.113.20
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.20 include:_spf.google.com ~all"
_dmarc 3600 IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
example.com. 3600 IN CAA 0 issue "letsencrypt.org"
A CNAME cannot sit at the zone apex (the bare example.com), and a name with a CNAME can't have any other records. If you need the apex to point at a load balancer's hostname, use your provider's ALIAS / ANAME / "CNAME flattening" feature (Route 53 Alias records, Cloudflare flattening).
3.4 TTL, caching and "DNS propagation"
Every record has a TTL (Time To Live) in seconds, which says how long resolvers may cache the answer. Caching makes DNS fast and resilient, but it also means changes aren't instant. Resolvers that cached the old value keep using it until their copy expires. People call this delay "DNS propagation", though really it's just caches expiring.
- A day before, lower the record's TTL from 3600/86400 to 300.
- Wait for the old TTL to pass, so every cache now holds the short TTL.
- Switch the A record to the new server. Keep the old server running for a while.
- Verify with
dig @1.1.1.1anddig @8.8.8.8. Once stable, raise the TTL again.
3.5 Registrar vs nameservers vs DNS hosting
This confuses almost every beginner. Buying a domain and hosting its DNS records can happen at two different companies.
3.6 Reverse DNS (PTR)
Normal ("forward") DNS maps a name to an IP. Reverse DNS maps an IP to a name using PTR records in the special in-addr.arpa zone. The owner of the IP (your VPS or cloud provider) controls it, not your domain's DNS host. You usually set it in the provider's control panel.
If you run a mail server on 203.0.113.20 and its PTR doesn't resolve to mail.example.com (which in turn must resolve back to 203.0.113.20), Gmail and Outlook will likely mark your emails as spam. Reverse DNS also makes logs and traceroute output readable.
3.7 DNS on a Linux server
| File / tool | What it does |
|---|---|
/etc/hosts | Static name → IP entries, checked before DNS. Great for testing a new server before switching real DNS. |
/etc/resolv.conf | Which DNS server the system asks. On Ubuntu it points to 127.0.0.53, the local systemd-resolved stub. |
resolvectl status | Shows the real upstream DNS servers that systemd-resolved uses on each interface. |
resolvectl flush-caches | Clears the local DNS cache. |
/etc/nsswitch.conf | The hosts: line sets the lookup order (files = /etc/hosts first, then dns). |
Add 203.0.113.50 shop.example.com to your laptop's hosts file (/etc/hosts, or C:\Windows\System32\drivers\etc\hosts on Windows). Only your machine now goes to the new server, so you can test it fully (even HTTPS) before you change DNS for everyone. Remove the line afterwards! curl --resolve shop.example.com:443:203.0.113.50 https://shop.example.com does the same for a single request.
Basic lookup, and how to read it:
$ dig github.com ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4512 ;; QUESTION SECTION: ;github.com. IN A ;; ANSWER SECTION: github.com. 60 IN A 140.82.121.4 ;; Query time: 24 msec ;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
status NOERROR = success (NXDOMAIN = name doesn't exist; SERVFAIL = the server failed). In the ANSWER section, 60 is the remaining TTL. SERVER is the resolver that answered. (Your IPs and TTLs will differ.)
Short answers and specific record types:
$ dig +short github.com A $ dig +short google.com AAAA $ dig +short www.github.com CNAME $ dig +short gmail.com MX $ dig +short google.com TXT $ dig +short github.com NS
Follow the full chain from the root, exactly like Figure 3.3:
$ dig +trace github.com . IN NS a.root-servers.net. ← root com. IN NS a.gtld-servers.net. ← TLD github.com. IN NS dns1.p08.nsone.net. ← authoritative github.com. 60 IN A 140.82.121.4 ← final answer # (output trimmed)
Ask specific resolvers, which is how you check "propagation":
$ dig @1.1.1.1 +short github.com $ dig @8.8.8.8 +short github.com $ dig @$(dig +short github.com NS | head -1) +short github.com # ask the authoritative server directly
Reverse lookup, and nslookup (also available on Windows):
$ dig -x 8.8.8.8 +short dns.google. $ nslookup github.com 1.1.1.1
Override DNS locally with /etc/hosts, then undo it:
$ echo "127.0.0.1 shop.test" | sudo tee -a /etc/hosts $ getent hosts shop.test # uses the same order as applications 127.0.0.1 shop.test $ dig +short shop.test # empty! dig only asks DNS, it ignores /etc/hosts $ sudo sed -i '/shop.test/d' /etc/hosts
dig and nslookup bypass /etc/hosts. getent hosts, ping and curl use it. That difference explains many "but dig says…" confusions.
(If you have a domain + VPS) In your DNS provider, create lab.yourdomain.com A <VPS-IP> with TTL 300. Then watch it appear with watch -n 10 dig +short lab.yourdomain.com @1.1.1.1.
3.8 Troubleshooting DNS failures
| What you see | Likely cause | Check with |
|---|---|---|
NXDOMAIN / browser: DNS_PROBE_FINISHED_NXDOMAIN | The record doesn't exist, there's a typo, records were added at the wrong provider, or the domain expired | dig +short NS domain, whois domain, dig @authoritative-ns name |
SERVFAIL | Authoritative servers are unreachable or broken, or there's a DNSSEC validation failure | dig +trace name, dig +cd name (disables DNSSEC checking) |
| Timeout / "no servers could be reached" | The server can't reach its resolver: firewall blocks UDP 53, or a bad /etc/resolv.conf | resolvectl status, dig @1.1.1.1 name |
| Some users get the old IP | Caching, because the TTL hasn't expired | Compare dig @1.1.1.1 vs @8.8.8.8 vs the authoritative server |
| Works with IP, fails with name | DNS problem on that client only | getent hosts name, check /etc/hosts, flush the cache |
🚑 "I updated the A record two hours ago, but the site still shows the old server"
dig @ns1.provider.com shop.example.com). Flush your local cache (resolvectl flush-caches; on Windows, ipconfig /flushdns). Then wait for the TTL to expire. Next time, lower the TTL first.🚑 "I added the records, but the domain still doesn't resolve at all"
dig shop.example.com returns NXDOMAIN, even though the record is visible in the registrar's DNS panel.dig +short NS example.com shows where the records must go. Add them at that provider, or change the nameservers at the registrar.🚑 "Our emails go to spam"
v=spf1 … ~all), the DKIM key from your mail provider, and a DMARC policy. Set reverse DNS at your VPS provider. Then check with dig TXT example.com and dig -x IP.🔑 Key takeaways
- DNS maps names to IPs (and more). Only the hostname is resolved, never the port or path.
- The resolver asks root → TLD → authoritative servers, then caches the answer for the TTL.
- A/AAAA → IP · CNAME → another name · MX → mail · TXT → verification and email security · NS → delegation · PTR → reverse.
- "Propagation" is really cache expiry. Lower the TTL before migrations.
- Records only count at the nameservers the domain is delegated to.
dig,dig +trace,dig @resolveranddig -xare your DNS toolkit.digignores/etc/hosts.
📝 Quiz: Module 3
1. Which record would you create to point blog.example.com to example.ghost.io?
Show answer
2. You want example.com (the apex) to point to an AWS load balancer hostname. Can you use a CNAME?
Show answer
3. What does a TTL of 300 mean?
Show answer
4. Which server owns the final answer for shop.example.com?
- The root server
- The .com TLD server
- The authoritative nameserver for example.com
- 8.8.8.8
Show answer
5. ping shop.test works, but dig shop.test returns nothing. Why?
Show answer
6. Which record type proves domain ownership to Google, or holds an SPF policy?
Show answer
7. Who sets the PTR (reverse DNS) record for your VPS IP?
Show answer
8. Which command shows every step from the root servers down to the final answer?
Show answer
dig +trace nameLinux networking & troubleshooting
Almost every server you'll manage runs Linux. This module is your toolbox for inspecting, configuring, protecting and debugging a server's network.
- List and read interfaces and IPs
- Configure a static IP with Netplan, safely
- Read and change the routing table
- Find which process owns a port
- Build a least-privilege UFW firewall
- Troubleshoot bottom-up: link → IP → route → DNS → port → app
4.1 Network interfaces
An interface is anything that can send and receive packets. Some are physical (a network card) and many are virtual (created by Docker, Kubernetes or VPNs). Each interface can hold one or more IP addresses.
| Name pattern | What it is |
|---|---|
lo | Loopback, 127.0.0.1. Always present. |
eth0, ens3, enp0s3, eno1 | Wired/virtual NICs. Modern Ubuntu uses "predictable" names based on the hardware slot, so the name varies by machine. |
wlan0 / wlp2s0 | Wi-Fi. |
docker0, br-xxxx, veth… | Docker bridges and the virtual cables to containers (Module 6). |
cni0, flannel.1, cali… | Kubernetes pod networking (Module 7). |
wg0, tun0 | VPN tunnels (WireGuard, OpenVPN). |
$ ip -br link # brief: name, state, MAC lo UNKNOWN 00:00:00:00:00:00 ens3 UP 52:54:00:ab:cd:ef docker0 DOWN 02:42:7a:11:22:33 $ ip -br addr # brief: name, state, IPs lo UNKNOWN 127.0.0.1/8 ::1/128 ens3 UP 203.0.113.10/24 2001:db8::10/64 $ ip -s link show ens3 # traffic counters, including errors and drops
sudo ip link set ens3 down on a remote server cuts off your SSH session immediately. Changes made with ip addr add or ip route add are temporary and disappear on reboot. Use Netplan for permanent changes.
4.2 Permanent configuration with Netplan
Ubuntu stores network configuration as YAML in /etc/netplan/*.yaml. Here's a static IP for a lab VM:
network: version: 2 ethernets: enp0s3: dhcp4: false addresses: [192.168.1.50/24] routes: - to: default via: 192.168.1.1 nameservers: addresses: [1.1.1.1, 8.8.8.8]
$ sudo chmod 600 /etc/netplan/01-static.yaml $ sudo netplan try # applies it, and auto-reverts after 120 s unless you press Enter $ sudo netplan apply # apply permanently
Always use netplan try on remote machines. If the new config breaks your connection, you simply don't press Enter, and it rolls back on its own. On cloud VPSs the file is usually 50-cloud-init.yaml, managed by cloud-init. Prefer the provider's panel for IP changes there.
Important network files
| File | Purpose |
|---|---|
/etc/netplan/*.yaml | Interface IPs, routes and DNS (Ubuntu) |
/etc/hostname | The machine's hostname (change it with hostnamectl set-hostname web-01) |
/etc/hosts | Static name → IP mappings |
/etc/resolv.conf | DNS resolver (managed by systemd-resolved on Ubuntu) |
/etc/services | The port-number ↔ service-name list (why ss shows "ssh" instead of 22 without -n) |
/etc/sysctl.conf, /etc/sysctl.d/ | Kernel network settings, e.g. net.ipv4.ip_forward=1 to let a server route/NAT (needed for VPN gateways; Docker and Kubernetes enable it themselves) |
/etc/ssh/sshd_config | The SSH server's port, authentication methods and so on (Module 9) |
4.3 Routing tables
$ ip route default via 203.0.113.1 dev ens3 proto static ← everything else → ISP gateway 10.0.1.0/24 dev ens4 proto kernel scope link src 10.0.1.10 ← private subnet, directly attached 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 203.0.113.0/24 dev ens3 proto kernel scope link src 203.0.113.10 $ ip route get 10.0.2.15 # "which route would be used to reach this IP?" 10.0.2.15 via 203.0.113.1 dev ens3 src 203.0.113.10 # ← oops: going out the public NIC!
In the output above, traffic to 10.0.2.15 (another private subnet) would leave through the public interface, which is wrong. You'd fix it by adding a route through the private network's router:
$ sudo ip route add 10.0.2.0/24 via 10.0.1.1 dev ens4 # temporary # permanent: add under ens4 in netplan → routes: [{to: 10.0.2.0/24, via: 10.0.1.1}]
ip route get <IP> is the quickest way to answer "which interface and gateway will my packet use?". Routes are chosen by the most specific prefix, and default is used only when nothing else matches.
4.4 Listening ports and running services
$ sudo ss -tulnp Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1201,fd=6)) tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=901,fd=3)) tcp LISTEN 0 511 127.0.0.1:5000 0.0.0.0:* users:(("node",pid=2210,fd=19)) tcp LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=845,fd=7)) tcp LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=1201,fd=7))
| Local Address shows… | Meaning | Reachable from |
|---|---|---|
0.0.0.0:80 / *:80 | All IPv4 interfaces | Anywhere the firewall allows |
[::]:80 | All IPv6 interfaces (often IPv4 too) | Anywhere the firewall allows |
127.0.0.1:5432 | Loopback only | This machine only ✅ (perfect for a DB behind an app on the same host) |
10.0.1.10:5432 | One specific interface | Only via the private network |
$ sudo ss -tnp state established # current live connections $ sudo ss -tn state time-wait | wc -l # how many connections are closing $ sudo lsof -i :443 # who uses port 443 $ systemctl status nginx # is the service running? $ journalctl -u nginx --since "10 min ago" # its recent logs $ curl -sI http://127.0.0.1:5000/health # does it answer locally?
Only one process can listen on a given IP:port. If Nginx fails to start with bind() to 0.0.0.0:80 failed (98: Address already in use), run sudo ss -tlnp | grep :80 to find the culprit. Often it's Apache or another Nginx, or a Docker container publishing port 80.
4.5 Firewalls: UFW, nftables and iptables
The Linux kernel's built-in packet filter is called netfilter. You configure it with a tool: nftables (nft, the modern one), iptables (legacy syntax, which on Ubuntu 22.04+ runs on top of nftables), or UFW ("Uncomplicated Firewall"), a friendly front-end that writes the rules for you.
UFW: the everyday choice on Ubuntu
$ sudo ufw default deny incoming $ sudo ufw default allow outgoing $ sudo ufw allow OpenSSH # ① ALWAYS FIRST on a remote server! $ sudo ufw allow 80,443/tcp # web $ sudo ufw allow from 10.0.1.0/24 to any port 5432 proto tcp # DB only from app subnet $ sudo ufw limit OpenSSH # rate-limit brute-force attempts $ sudo ufw enable $ sudo ufw status numbered To Action From -- ------ ---- [ 1] OpenSSH LIMIT IN Anywhere [ 2] 80,443/tcp ALLOW IN Anywhere [ 3] 5432/tcp ALLOW IN 10.0.1.0/24 $ sudo ufw delete 3 # remove a rule by number
nftables and iptables (good to recognise)
table inet filter { chain input { type filter hook input priority 0; policy drop; ct state established,related accept iif "lo" accept tcp dport { 22, 80, 443 } accept ip saddr 10.0.1.0/24 tcp dport 5432 accept icmp type echo-request accept } }
# list rules with packet counters sudo iptables -L -n -v --line-numbers # allow established + SSH + web, drop rest sudo iptables -A INPUT -m conntrack \ --ctstate ESTABLISHED,RELATED -j ACCEPT sudo iptables -A INPUT -i lo -j ACCEPT sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT sudo iptables -A INPUT -p tcp -m multiport \ --dports 80,443 -j ACCEPT sudo iptables -P INPUT DROP
Linux firewalls are stateful. They track connections, so when your server starts an outgoing connection (say, to download updates), the replies are allowed back in automatically ("established,related"). You only write rules for new incoming connections.
Pick one tool per server. Don't mix UFW with hand-written nft/iptables rules. Also, Docker writes its own iptables rules, and published container ports bypass UFW. Module 6 shows how to handle that.
4.6 Troubleshooting connectivity, latency and packet loss
Work bottom-up: there's no point debugging Nginx if the server has no route to the Internet. Each step only makes sense once the step below it passes.
$ ping -c 20 203.0.113.10 # look at "% packet loss" and avg/mdev (jitter) $ mtr -rwbzc 50 shop.example.com HOST Loss% Snt Last Avg Best Wrst StDev 1. 192.168.1.1 0.0% 50 1.1 1.3 0.9 3.2 0.4 2. 10.20.0.1 0.0% 50 8.2 8.9 7.6 15.1 1.4 3. ae-1.isp-core.net 40.0% 50 12.1 12.4 11.8 14.0 0.5 ← loss only here? 4. 203.0.113.1 0.0% 50 21.3 21.8 20.9 25.2 0.8 5. 203.0.113.10 0.0% 50 22.0 22.4 21.5 26.0 0.9 ← final hop: 0% → fine
Loss at a middle hop that does not continue to the following hops is just that router rate-limiting ICMP, so ignore it. Real loss starts at one hop and continues all the way to the destination. Rough latency guide: same data centre < 1 ms · same country 5–40 ms · across continents 100–250 ms.
| Symptom | Most likely cause | Command that proves it |
|---|---|---|
| Can't reach anything, not even the gateway | Interface down, wrong IP/subnet, cable/VM network | ip -br addr, ip -s link |
| Gateway OK, Internet IPs fail | Missing default route, upstream/NAT problem | ip route, traceroute -n 1.1.1.1 |
| IPs work, names fail | DNS (resolver unreachable, bad resolv.conf) | resolvectl status, dig @1.1.1.1 name |
| Name resolves, port times out | Firewall / security group / service not public | nc -vz host port, sudo ufw status |
| Slow or intermittent | Packet loss, congestion, DNS slowness, overloaded server | mtr, curl -w timings (Module 9), ip -s link errors |
4.7 The Linux networking command toolkit
| Command | Purpose | Example |
|---|---|---|
ip addr | View IP addresses | ip -br addr show |
ip link | Interface state / MAC | ip -br link |
ip route | Routing table / path for an IP | ip route get 8.8.8.8 |
ip neigh | ARP / neighbour cache | ip neigh show |
ping | Reachability and latency (ICMP) | ping -c 4 8.8.8.8 |
traceroute / mtr | Path, plus per-hop loss and latency | mtr -rwc 20 google.com |
ss | Listening ports and connections | sudo ss -tulnp |
curl | Test HTTP/API endpoints | curl -I https://example.com |
dig / nslookup | Troubleshoot DNS | dig +short example.com |
nc | Test TCP/UDP port connectivity | nc -vz 10.0.0.5 5432 |
tcpdump | Capture packets | sudo tcpdump -i any port 443 |
ufw | Manage the firewall | sudo ufw status verbose |
hostname | Hostname / IPs | hostname -I |
nmap | Scan which ports are open (only on your own hosts!) | nmap -Pn -p 1-1000 203.0.113.10 |
resolvectl | Local DNS settings and cache | resolvectl status |
Start three test services on the server:
$ sudo systemctl start nginx # "web" on 80 $ nohup python3 -m http.server 8080 --bind 0.0.0.0 & # "admin panel" on 8080 $ nohup python3 -m http.server 9000 --bind 127.0.0.1 & # "internal API" on 9000 $ sudo ss -tlnp | grep -E ':(22|80|8080|9000)\b'
From your laptop (replace with your server's IP), see what's reachable before the firewall:
$ nc -vz SERVER-IP 80 # succeeded $ nc -vz SERVER-IP 8080 # succeeded ← an admin panel open to the world! $ nc -vz SERVER-IP 9000 # refused ← bound to 127.0.0.1, good
Apply the policy: SSH and web are public, and the admin panel is allowed only from your laptop's IP.
$ sudo ufw default deny incoming && sudo ufw default allow outgoing $ sudo ufw allow OpenSSH $ sudo ufw allow 80/tcp $ sudo ufw allow from YOUR-LAPTOP-IP to any port 8080 proto tcp $ sudo ufw enable && sudo ufw status numbered
Find YOUR-LAPTOP-IP with curl ifconfig.me from the laptop (VPS lab), or from your LAN IP (local VM lab).
Verify from the laptop, and from a different network (such as your phone's hotspot) if you can:
$ nc -vz -w 5 SERVER-IP 8080 # laptop: succeeds · other network: times out
Notice that it's a timeout, not "refused": UFW silently drops packets by default.
Look at the firewall's logs, then clean up:
$ sudo ufw logging on ; sudo tail -f /var/log/ufw.log # look for [UFW BLOCK] $ pkill -f http.server
4.8 Troubleshooting scenarios
🚑 "I enabled UFW and lost SSH"
sudo ufw enable, the SSH session freezes, and new connections time out.sudo ufw allow 22/tcp (or your custom port) and sudo ufw reload. Prevent it next time: allow SSH first, then enable.🚑 "apt update fails: Temporary failure resolving 'archive.ubuntu.com'"
ping 1.1.1.1 works.resolvectl status to see the DNS servers. dig @1.1.1.1 archive.ubuntu.com to test past the local stub. Add nameservers in Netplan, then netplan apply.🚑 "Users complain the site is slow only in the evening"
mtr from a client towards the server during the slow window; check ip -s link for drops; check bandwidth with vnstat/iftop and load with htop. Then add caching or a CDN, or upgrade the plan or move to a closer region.🔑 Key takeaways
ipis the modern tool for interfaces, addresses, routes and neighbours (ifconfig/routeare legacy).ip addr/route addis temporary; Netplan YAML is permanent. Usenetplan tryremotely.ss -tulnpshows who listens where. Check whether the local address is 0.0.0.0 or 127.0.0.1.- Firewall rule: default deny incoming, then allow only the necessary ports from the necessary sources. Allow SSH first!
- UFW → iptables/nftables → netfilter (kernel). Use one tool per server. Docker bypasses UFW for published ports.
- Troubleshoot bottom-up: link → IP → gateway → Internet → DNS → port → app.
📝 Quiz: Module 4
1. Which command shows which process listens on port 5432?
Show answer
sudo ss -tlnp | grep 5432 (or sudo lsof -i :5432).2. Why use netplan try rather than netplan apply on a remote VPS?
Show answer
3. Write UFW rules so PostgreSQL (5432) is reachable only from 10.0.1.0/24.
Show answer
sudo ufw allow from 10.0.1.0/24 to any port 5432 proto tcp, with the default incoming policy set to deny.4. ping 1.1.1.1 works, but ping google.com says "Temporary failure in name resolution". Which layer/component is broken?
Show answer
5. mtr shows 60% loss at hop 4 but 0% at hops 5–9 (the destination). Is there a problem?
Show answer
6. What does the "established,related" rule allow?
Show answer
7. ip route get 10.0.2.15 shows it would leave via the public interface. What is missing?
Show answer
10.0.2.0/24 via the private network's gateway, e.g. ip route add 10.0.2.0/24 via 10.0.1.1 dev ens4 (made permanent in Netplan).Web server networking
How Nginx (or HAProxy) sits in front of your apps: it terminates HTTPS, routes requests, balances load, checks health and keeps WebSockets alive.
- Explain forward vs reverse proxies
- Write an Nginx reverse-proxy config
- Pass the real client IP to the backend
- Load-balance across several backends
- Get and auto-renew a free HTTPS certificate
- Proxy WebSockets
- Diagnose 502, 504 and redirect loops
5.1 What is a reverse proxy?
A reverse proxy is a server that sits in front of your applications and receives every client request on their behalf. It then forwards each request to the right backend ("upstream") and passes the answer back. Clients only ever see the proxy.
Why use one?
✔ One place for HTTPS certificates
✔ Backends stay private (127.0.0.1 / private IPs)
✔ Route by path or domain
✔ Load balancing and failover
✔ Compression, caching, rate limiting, security headers
Popular reverse proxies
Nginx (most common) · HAProxy (load-balancing specialist) · Caddy (automatic HTTPS) · Traefik (Docker/Kubernetes-aware) · Envoy (service meshes) · cloud LBs (AWS ALB, GCP LB, Azure App Gateway)
5.2 An Nginx reverse proxy for ShopNow
upstream shop_api { server 127.0.0.1:5000; keepalive 32; # reuse connections to the backend } server { # ① plain HTTP: only redirect listen 80; listen [::]:80; server_name shop.example.com; return 301 https://$host$request_uri; } server { # ② HTTPS listen 443 ssl http2; # on nginx ≥ 1.25: "listen 443 ssl;" + "http2 on;" listen [::]:443 ssl http2; server_name shop.example.com; ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem; root /var/www/shop; # React build output index index.html; location / { try_files $uri /index.html; # single-page-app routing } location /api/ { proxy_pass http://shop_api; # no trailing slash → backend receives /api/... proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; # can't connect in 5 s → 502/504 proxy_read_timeout 60s; # backend silent for 60 s → 504 client_max_body_size 10m; # bigger uploads → 413 } }
$ sudo ln -s /etc/nginx/sites-available/shopnow /etc/nginx/sites-enabled/ $ sudo nginx -t # ALWAYS test the syntax first nginx: configuration file /etc/nginx/nginx.conf test is successful $ sudo systemctl reload nginx # reload = no dropped connections (restart drops them)
With location /api/, proxy_pass http://shop_api; sends /api/products to the backend unchanged. But proxy_pass http://shop_api/; (with a trailing /) strips the prefix and sends /products. Pick whichever your backend expects. Getting it wrong produces mysterious 404s.
5.3 HTTP headers and request forwarding
When Nginx forwards a request, the backend sees a TCP connection from Nginx (127.0.0.1), not from the user. Without extra headers, your app logs every visitor as 127.0.0.1, rate limiting breaks, and the app thinks every request was plain HTTP.
| Header | Purpose |
|---|---|
Host | Which site was requested. Needed when one backend serves several domains. Without it, the backend sees "shop_api". |
X-Real-IP | The client's IP as Nginx saw it. |
X-Forwarded-For | The chain of client and proxy IPs (client, proxy1, proxy2). Used behind CDNs and load balancers. |
X-Forwarded-Proto | The original scheme (http/https). Apps use it to build correct links and to avoid redirect loops. |
Upgrade / Connection | Needed to switch a connection to WebSocket (section 5.8). |
Clients can fake X-Forwarded-For. Your app should only trust these headers when the request comes from your own proxy. Most frameworks have a "trusted proxies" setting, e.g. Express app.set('trust proxy', 1) or Django SECURE_PROXY_SSL_HEADER.
5.4 Load balancing
When one backend isn't enough, or you need no downtime when a server fails, run several copies and let the proxy spread requests across them.
| Algorithm | How it chooses | Good for | Nginx directive |
|---|---|---|---|
| Round robin | Takes turns: 1, 2, 3, 1, 2, 3… | Identical, stateless backends (the default) | (default) |
| Weighted | Bigger servers get more requests | Mixed server sizes, canary releases (e.g. 90/10) | server a weight=3; |
| Least connections | Picks the backend with the fewest active connections | Requests of very different lengths | least_conn; |
| IP hash / sticky | The same client always goes to the same backend | Apps that keep sessions in memory (better: store sessions in Redis) | ip_hash; / hash $cookie_x; |
upstream shop_api { least_conn; server 10.0.11.21:5000 max_fails=3 fail_timeout=30s; # 3 failures → skip it for 30 s server 10.0.11.22:5000 max_fails=3 fail_timeout=30s; server 10.0.11.23:5000 backup; # only used if the others are down keepalive 32; }
L4 (TCP/UDP) balancers, such as AWS NLB or HAProxy in mode tcp, forward raw connections. They're very fast and work for any protocol (databases, MQTT), but they can't see URLs. L7 (HTTP) balancers, such as Nginx, AWS ALB or HAProxy in mode http, read each request and can route by path, host or header, add headers and terminate TLS.
5.5 Upstream services and health checks
- Passive checks (open-source Nginx): mark a backend "down" after real requests to it fail (
max_fails/fail_timeout). - Active checks (HAProxy, NGINX Plus, cloud LBs, Kubernetes probes): the balancer calls
GET /healthevery few seconds, even with no user traffic.
frontend web bind *:80 default_backend shop_api backend shop_api balance roundrobin option httpchk GET /health http-check expect status 200 server api1 10.0.11.21:5000 check inter 5s fall 3 rise 2 server api2 10.0.11.22:5000 check inter 5s fall 3 rise 2
Keep it fast and cheap. Return 200 only when the app can really serve traffic (for example, it can reach its database). Return 503 while starting up or shutting down. Never put heavy queries in it: the balancer calls it every few seconds on every instance.
5.6 TLS/SSL certificates and HTTPS
TLS (Transport Layer Security; "SSL" is its outdated name) gives three guarantees:
🔒 Encryption
Nobody on the Wi-Fi, ISP or path can read the traffic.
✅ Integrity
Nobody can change the data in transit without detection.
🔏 Authentication
The certificate proves that you're really talking to shop.example.com.
Free certificates with Let's Encrypt + Certbot
$ sudo apt install -y certbot python3-certbot-nginx $ sudo certbot --nginx -d shop.example.com -d www.shop.example.com # Certbot proves you control the domain (HTTP-01 challenge on port 80), # installs the cert in your nginx config and adds the HTTP→HTTPS redirect. $ sudo certbot renew --dry-run # test auto-renewal $ systemctl list-timers | grep certbot # the renewal timer runs twice a day
| Challenge type | How it proves ownership | Use when |
|---|---|---|
| HTTP-01 | The CA fetches http://domain/.well-known/acme-challenge/… on port 80 | A normal public web server (the most common case) |
| DNS-01 | You create a _acme-challenge TXT record | Wildcard certs (*.example.com), or servers not reachable from the Internet |
$ echo | openssl s_client -connect shop.example.com:443 -servername shop.example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates subject=CN = shop.example.com issuer=C = US, O = Let's Encrypt, CN = R11 notBefore=Oct 1 10:00:00 2026 GMT notAfter=Dec 30 10:00:00 2026 GMT
- Expired: renewal failed silently (port 80 blocked, DNS changed). Monitor expiry dates! Let's Encrypt certificates are short-lived (90 days, and getting shorter), so automation is mandatory.
- Name mismatch: the cert is for
example.combut the user openedwww.example.com. - Incomplete chain: using
cert.peminstead offullchain.pem. Browsers may work while curl and mobile apps fail.
5.7 Ports 80 and 443
Port 80 · HTTP
Keep it open, but use it only to redirect to HTTPS (301) and to answer Let's Encrypt HTTP-01 challenges.
Port 443 · HTTPS
All real traffic. HTTP/1.1 and HTTP/2 use TCP 443. HTTP/3 (QUIC) uses UDP 443, so open both if you enable HTTP/3.
Add HSTS (add_header Strict-Transport-Security "max-age=31536000" always;) once HTTPS works well. It tells browsers to always use HTTPS for your domain.
5.8 WebSockets and persistent connections
Normal HTTP is request → response → done. Chat, live dashboards, notifications and multiplayer games need the server to push data at any time. WebSockets start as an HTTP request with Upgrade: websocket, then turn the TCP connection into a long-lived, two-way channel.
location /ws/ { proxy_pass http://127.0.0.1:6000; proxy_http_version 1.1; # required: WebSocket upgrade needs HTTP/1.1 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 1h; # default 60 s would cut idle sockets }
Even normal HTTP reuses TCP connections (keep-alive) to avoid a new TCP + TLS handshake for every request. HTTP/2 goes further and multiplexes many requests over one connection. That's why keepalive 32; in the upstream block, plus proxy_set_header Connection "";, makes Nginx → backend traffic faster.
Start two tiny "backends" that each identify themselves:
$ mkdir -p ~/b1 ~/b2 && echo "backend 1" > ~/b1/index.html && echo "backend 2" > ~/b2/index.html $ (cd ~/b1 && nohup python3 -m http.server 5001 --bind 127.0.0.1 >/dev/null 2>&1 &) $ (cd ~/b2 && nohup python3 -m http.server 5002 --bind 127.0.0.1 >/dev/null 2>&1 &) $ curl 127.0.0.1:5001 ; curl 127.0.0.1:5002
Create the Nginx site:
$ sudo tee /etc/nginx/sites-available/lab >/dev/null <<'EOF' upstream lab_backends { server 127.0.0.1:5001 max_fails=1 fail_timeout=10s; server 127.0.0.1:5002 max_fails=1 fail_timeout=10s; } server { listen 80 default_server; server_name _; location / { proxy_pass http://lab_backends; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 5s; } location /slow/ { proxy_pass http://127.0.0.1:5003; proxy_read_timeout 5s; } } EOF $ sudo rm -f /etc/nginx/sites-enabled/default $ sudo ln -sf /etc/nginx/sites-available/lab /etc/nginx/sites-enabled/lab $ sudo nginx -t && sudo systemctl reload nginx
Watch round-robin in action:
$ for i in 1 2 3 4; do curl -s localhost; done backend 1 backend 2 backend 1 backend 2
Kill one backend: Nginx fails over. Kill both: you get a 502.
$ pkill -f "http.server 5001" ; for i in 1 2 3; do curl -s localhost; done # all "backend 2" $ pkill -f "http.server 5002" ; curl -sI localhost | head -1 HTTP/1.1 502 Bad Gateway $ sudo tail -n 2 /var/log/nginx/error.log ... connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://127.0.0.1:5002/"
Remember the pattern: 502 + "Connection refused" in error.log = the backend isn't listening.
Create a backend that accepts connections but never answers, which produces a 504:
$ nohup nc -lk 127.0.0.1 5003 >/dev/null 2>&1 & $ time curl -sI localhost/slow/ | head -1 HTTP/1.1 504 Gateway Time-out real 0m5.0s $ sudo tail -n 1 /var/log/nginx/error.log # "upstream timed out (110: Connection timed out)" $ pkill -f "nc -lk"
HTTPS. If you have a domain pointing to this VPS, use Let's Encrypt:
$ sudo ufw allow 80,443/tcp $ sudo sed -i 's/server_name _;/server_name lab.yourdomain.com;/' /etc/nginx/sites-available/lab $ sudo nginx -t && sudo systemctl reload nginx $ sudo certbot --nginx -d lab.yourdomain.com $ curl -I https://lab.yourdomain.com
No domain? Practise with a self-signed certificate (browsers will warn, which is expected):
$ sudo openssl req -x509 -nodes -days 30 -newkey rsa:2048 \ -keyout /etc/ssl/private/lab.key -out /etc/ssl/certs/lab.crt -subj "/CN=lab.local" # add to the server block: listen 443 ssl; ssl_certificate /etc/ssl/certs/lab.crt; # ssl_certificate_key /etc/ssl/private/lab.key; $ sudo nginx -t && sudo systemctl reload nginx $ curl -kI https://localhost # -k = accept the untrusted self-signed cert
5.9 Troubleshooting scenarios
🚑 HTTPS works, but every API call returns 502 Bad Gateway
/api/* returns 502.proxy_pass uses (e.g. the app listens on [::1]:5000 while Nginx calls 127.0.0.1:5000).sudo tail /var/log/nginx/error.log → ss -tlnp | grep 5000 → curl -v 127.0.0.1:5000/health → systemctl status shop-api / journalctl -u shop-api. Make the port/address in proxy_pass match reality.🚑 Report export fails with 504 after exactly 60 seconds
proxy_read_timeout defaults to 60 s, and the export takes 90 s.proxy_read_timeout for that location only. Better: turn the export into a background job and let the user download it when it's ready.🚑 "ERR_TOO_MANY_REDIRECTS" after enabling HTTPS
X-Forwarded-Proto.X-Forwarded-Proto; redirect only once, at the edge.🚑 Live chat disconnects every minute
proxy_read_timeout 60s, or a cloud LB's idle timeout./ws/ and send application-level ping/heartbeat messages every 20–30 s.🔑 Key takeaways
- A reverse proxy is the single public door: TLS, routing, load balancing, and private backends.
- Always run
nginx -tbeforesystemctl reload nginx. - Pass
Host,X-Real-IP,X-Forwarded-ForandX-Forwarded-Prototo backends. - Load-balancing algorithms: round robin, weighted, least connections, IP hash. Health checks remove dead backends.
- TLS = encryption + integrity + authentication. Use Let's Encrypt with auto-renewal, and monitor expiry.
- Port 80 only redirects and answers ACME challenges; 443 carries the traffic (UDP 443 for HTTP/3).
- WebSockets need HTTP/1.1, the Upgrade/Connection headers and long timeouts.
- 502 = backend unreachable/refused; 504 = backend too slow/silent.
📝 Quiz: Module 5
1. What is the difference between a forward proxy and a reverse proxy?
Show answer
2. Your backend logs show every visitor's IP as 127.0.0.1. What do you fix?
Show answer
X-Real-IP/X-Forwarded-For in Nginx, and configure the app to trust the proxy and read the client IP from those headers.3. Which Nginx directive sends each request to the backend with the fewest active connections?
Show answer
least_conn; inside the upstream block.4. Why must port 80 stay open even when the site is HTTPS-only?
Show answer
5. Which file should ssl_certificate point to: cert.pem or fullchain.pem?
Show answer
6. Which two headers make a WebSocket upgrade work through Nginx?
Show answer
Upgrade $http_upgrade and Connection "upgrade", plus proxy_http_version 1.1.7. Nginx error log: upstream timed out (110). Which status code did users get?
Show answer
8. You need a certificate for *.example.com. Which challenge type is required?
Show answer
Docker networking
Containers are isolated little machines. This module shows how they get IPs, find each other by name, get published to the world, and stay private when they should.
- Explain how a container gets its own network stack
- Choose the right network driver
- Publish ports safely
- Connect containers by service name
- Design front-end / back-end networks in Compose
- Avoid the "Docker bypasses UFW" trap
6.1 How container networking works
Every container gets its own network namespace: its own interfaces, IP address, routing table and port numbers. Two containers can both listen on port 80 without clashing. By default Docker connects containers to a virtual switch on the host called a bridge (docker0), using a virtual cable (veth pair) for each one.
The host is an office building and each container is a private office with its own internal phone extension (container IP). The bridge is the building's internal phone system, so offices can call each other. Port publishing is the reception desk: an outside caller dials the building's public number plus an extension (host:8080), and reception forwards the call to the right office (container:80).
6.2 Network drivers: bridge, host, none, overlay
| Driver | How to use it | Notes |
|---|---|---|
default bridge (bridge) | docker run nginx | Containers get IPs, but no DNS by name. Legacy: avoid it for multi-container apps. |
| user-defined bridge | docker network create shopnetdocker run --network shopnet … | ✅ Built-in DNS (container/service names), better isolation. Compose creates one automatically. |
| host | docker run --network host … | No isolation, no port mapping. Useful for monitoring agents or very high-throughput proxies. |
| none | docker run --network none … | Loopback only. |
| overlay | docker network create -d overlay … | Multi-host networking in Docker Swarm. (Kubernetes uses CNI instead; see Module 7.) |
| macvlan / ipvlan | -d macvlan --subnet … | The container appears as a real device on your LAN with its own IP and MAC. Niche (legacy apps, IoT). |
6.3 Container IP addresses
$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web 172.18.0.2 $ docker network inspect shopnet | jq '.[0].IPAM.Config, .[0].Containers'
Container IPs change every time a container is recreated. Always connect by name (Docker DNS, section 6.5). Hard-coded 172.17.0.3 in a config file is a time bomb.
6.4 Port publishing and port mapping
Containers on a bridge are private to the host. To let the outside world in, publish a port with -p HOST_PORT:CONTAINER_PORT.
| Flag | Meaning | Reachable from |
|---|---|---|
-p 8080:80 | Host port 8080 (all interfaces) → container port 80 | 🌍 Anyone who can reach the host, even if UFW says deny |
-p 127.0.0.1:8080:80 | Only on the host's loopback | 🔒 The host itself (e.g. a host Nginx proxying to it) ✅ recommended |
-p 10.0.1.10:5432:5432 | Only on the private interface | 🏢 The private network |
-p 53:53/udp | UDP port | Remember: TCP and UDP are separate |
-P | Publish all EXPOSEd ports on random host ports | Testing only |
EXPOSE 5000 (Dockerfile) | Documentation only. It publishes nothing. | — |
Docker inserts its DNAT rules before UFW's rules, so -p 5432:5432 exposes your database to the entire Internet even when ufw status says 5432 is denied. Many real databases have been wiped by bots this way. Defences, best first:
- Don't publish database or internal ports at all. Let containers talk over a Docker network.
- If the host must reach it, bind to loopback:
-p 127.0.0.1:5432:5432. - Add a cloud firewall / security group in front of the VPS (it filters before traffic reaches the server).
- Advanced: put rules in the
DOCKER-USERiptables chain, which Docker evaluates first.
6.5 Container-to-container communication and Docker DNS
On a user-defined network, Docker runs an embedded DNS server at 127.0.0.11 inside each container. Every container can reach every other container on the same network by its container name, its Compose service name or a network alias.
$ docker network create shopnet $ docker run -d --name db --network shopnet -e POSTGRES_PASSWORD=secret postgres:16-alpine $ docker run -d --name api --network shopnet -e DATABASE_HOST=db myshop/api:1.0 # inside "api", the hostname "db" resolves to the db container's current IP $ docker exec api getent hosts db 172.18.0.2 db
Inside a container, localhost means that container itself, not the host and not other containers. DATABASE_URL=postgres://localhost:5432 in the api container fails with "connection refused". Use the service name: postgres://db:5432. To reach a service running on the host, use host.docker.internal (on Linux, add --add-host=host.docker.internal:host-gateway).
6.6 Isolated networks
Containers can only talk to containers that share a network with them. Use that to build zones, just like subnets in Module 1. A network created with --internal has no route to the outside world at all, which is perfect for databases.
$ docker network create frontend $ docker network create --internal backend # no Internet in or out $ docker network connect backend api # a container can join several networks
6.7 Docker Compose networking: ShopNow in containers
Compose creates a network for each project automatically (named <project>_default), and every service can reach the others by service name. For production you'll usually define networks explicitly:
services: nginx: image: nginx:1.27-alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # proxy_pass http://api:5000; depends_on: [api] networks: [frontend] api: build: ./api environment: DATABASE_URL: postgres://shop:${DB_PASSWORD}@db:5432/shop # service name, not localhost! expose: ["5000"] # documentation; NOT published to the host networks: [frontend, backend] db: image: postgres:16-alpine environment: POSTGRES_USER: shop POSTGRES_PASSWORD: ${DB_PASSWORD} # from .env, never committed POSTGRES_DB: shop volumes: [dbdata:/var/lib/postgresql/data] networks: [backend] # no "ports:" → unreachable from outside networks: frontend: {} backend: internal: true volumes: dbdata: {}
Docker picks networks from 172.17.0.0/16 upward. If your office VPN or cloud VPC also uses 172.17–172.31, the server may lose access to those networks as soon as a Compose project starts. Fix it by choosing Docker's ranges in /etc/docker/daemon.json:
{
"bip": "10.199.0.1/24",
"default-address-pools": [ { "base": "10.200.0.0/16", "size": 24 } ]
}6.8 Overlay networks (multi-host)
When containers run on several hosts, a bridge isn't enough because it only exists on one machine. Overlay networks tunnel container traffic between hosts (using VXLAN over UDP 4789), so containers on different servers share one virtual network. Docker Swarm uses overlays; Kubernetes solves the same problem with CNI plugins (next module).
The default bridge has no name resolution:
$ docker network ls $ docker run -d --name a1 nginx:alpine $ docker run --rm alpine ping -c1 a1 ping: bad address 'a1'
A user-defined network has DNS:
$ docker network create labnet $ docker run -d --name web --network labnet nginx:alpine $ docker run --rm --network labnet alpine sh -c "cat /etc/resolv.conf; ping -c1 web; wget -qO- http://web | head -4" nameserver 127.0.0.11 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.09 ms <!DOCTYPE html> <html>
Publish ports, then look at the NAT rule Docker created:
$ docker run -d --name pub -p 8080:80 nginx:alpine $ docker run -d --name priv -p 127.0.0.1:8081:80 nginx:alpine $ docker port pub ; sudo ss -tlnp | grep -E '808[01]' 80/tcp -> 0.0.0.0:8080 LISTEN 0.0.0.0:8080 ... docker-proxy LISTEN 127.0.0.1:8081 ... docker-proxy $ sudo iptables -t nat -L DOCKER -n | grep -E '808[01]' DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.3:80 DNAT tcp -- 0.0.0.0/0 127.0.0.1 tcp dpt:8081 to:172.17.0.4:80
From your laptop: curl SERVER-IP:8080 works and curl SERVER-IP:8081 fails. 8081 is private. (Optional: run sudo ufw deny 8080 and notice that 8080 still works from the laptop. That's the UFW bypass in action.)
An internal network has no Internet:
$ docker network create --internal vault $ docker run --rm --network vault alpine ping -c1 -W2 1.1.1.1 ping: sendto: Network unreachable
Build a segmented Compose stack. Create the files:
$ mkdir -p ~/lab6 && cd ~/lab6 $ cat > default.conf <<'EOF' server { listen 80; location / { proxy_pass http://api:80; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } EOF $ cat > compose.yaml <<'EOF' services: proxy: image: nginx:alpine ports: ["8088:80"] volumes: ["./default.conf:/etc/nginx/conf.d/default.conf:ro"] networks: [frontend] api: image: traefik/whoami # tiny app that echoes request + its IPs networks: [frontend, backend] db: image: postgres:16-alpine environment: { POSTGRES_PASSWORD: labpass } networks: [backend] networks: frontend: {} backend: { internal: true } EOF $ docker compose up -d && docker compose ps
Test the segmentation:
$ curl -s localhost:8088 | grep -E 'Hostname|IP|X-Forwarded' Hostname: 3f2a9c1d7b20 IP: 172.20.0.3 IP: 172.21.0.2 ← the api has one IP per network X-Forwarded-For: 172.20.0.1 $ docker compose exec proxy ping -c1 -W2 db # fails: proxy is not on "backend" ping: bad address 'db' $ docker run --rm --network lab6_backend postgres:16-alpine pg_isready -h db db:5432 - accepting connections $ docker compose down ; docker rm -f a1 web pub priv ; docker network rm labnet vault
6.9 Troubleshooting scenarios
🚑 "ECONNREFUSED 127.0.0.1:5432" from the api container
npm start on a laptop, but crashes in Docker with a DB connection refused error.localhost inside the container is the container itself, and no Postgres runs there.db:5432) and make sure both services share a network. Check with docker compose exec api getent hosts db.🚑 "Bind for 0.0.0.0:80 failed: port is already allocated"
docker compose up fails for the nginx service.sudo ss -tlnp | grep :80 and docker ps --format '{{.Names}} {{.Ports}}'. Stop the other service or change the host port ("8080:80").🚑 "Our test database was wiped and replaced with a ransom note"
ports: ["5432:5432"] published the DB on 0.0.0.0, and Docker's rules bypass UFW. Bots scan the whole Internet for open DB ports within minutes.ports: entry (or use 127.0.0.1:5432:5432), use strong passwords, add a provider firewall, and restore from backup. Audit with nmap -Pn SERVER-IP from outside.🚑 "The server lost access to the company VPN after deploying Compose"
docker compose up, the server can't reach 172.20.5.10 (an office system).172.20.0.0/16, which overlaps the office range, so the local route wins.default-address-pools in daemon.json to a non-overlapping range, restart Docker, and recreate the networks. Check with ip route.🔑 Key takeaways
- Each container has its own network namespace; bridges and veth pairs connect containers on the host.
- Use user-defined bridge networks (Compose does this for you), which give you DNS by service name.
-p host:containerpublishes a port.EXPOSEdoesn't. Bind to 127.0.0.1 when only the host needs access.- Published ports bypass UFW. Never publish databases.
localhostin a container = that container. Use service names.- Segment with multiple networks; use
internal: truefor data stores. - Plan Docker's address pools so they don't overlap VPN/VPC ranges.
📝 Quiz: Module 6
1. Two containers on the default bridge can ping each other by IP but not by name. Why?
Show answer
2. What does -p 127.0.0.1:9000:3000 do?
Show answer
3. Does EXPOSE 8080 in a Dockerfile make the app reachable from the Internet?
Show answer
-p / ports: publishes.4. In Compose, what hostname should the api use to reach a service named cache (Redis)?
Show answer
cache (e.g. redis://cache:6379), provided both services share a network.5. UFW denies 27017, but MongoDB published with -p 27017:27017 is reachable from the Internet. Why, and how do you fix it?
Show answer
6. What does internal: true do on a Compose network?
Show answer
7. Which network driver lets containers on different hosts share one network in Docker Swarm?
Show answer
Kubernetes networking
Kubernetes runs pods across many machines and keeps moving them around. Its networking gives every pod an IP, gives every app a stable name, and controls who may talk to whom.
- State the Kubernetes networking model
- Explain why Services exist
- Choose ClusterIP, NodePort or LoadBalancer
- Expose HTTP apps with Ingress / Gateway
- Use cluster DNS names correctly
- Write a default-deny NetworkPolicy
- Compare CNI plugins
This module assumes you know what a Pod (one or more containers that run together), a Deployment (keeps N copies of a pod running) and a Node (a machine in the cluster) are. Everything you learned about IPs, ports, DNS, proxies and firewalls applies here, just automated.
7.1 The Kubernetes networking model
Kubernetes sets three simple rules, and a CNI plugin makes them true:
① Every pod gets its own IP
Not shared with other pods. Containers in one pod share it (and talk via localhost).
② Pods reach all pods without NAT
A pod on node 1 can talk directly to a pod IP on node 2.
③ Nodes reach all pods
Node agents (kubelet, monitoring) can reach any pod IP.
All containers in a pod share one network namespace. They have the same IP and talk to each other over localhost, and they can't both use the same port. That's how sidecars work: a log shipper or proxy container next to your app, reaching it on localhost:5000.
7.2 Services: stable addresses for moving pods
Pods are temporary. A new deployment, a crash or a node failure replaces them, and they come back with new IPs. So you never connect to pod IPs. Instead you create a Service: a stable virtual IP and DNS name that load-balances across all pods matching a label selector.
apiVersion: apps/v1 kind: Deployment metadata: { name: api, namespace: shop } spec: replicas: 3 selector: { matchLabels: { app: api } } template: metadata: { labels: { app: api } } # ← label on the pods spec: containers: - name: api image: myshop/api:1.4 ports: [{ containerPort: 5000 }] readinessProbe: # only "ready" pods receive traffic httpGet: { path: /health, port: 5000 } --- apiVersion: v1 kind: Service metadata: { name: api, namespace: shop } spec: type: ClusterIP # the default selector: { app: api } # ← must match the pod labels! ports: - port: 80 # the Service's port (what clients call) targetPort: 5000 # the container's port
Now any pod can call http://api.shop (port 80), and Kubernetes forwards it to one of the three ready pods on port 5000. The component that programs this forwarding on every node is kube-proxy (using iptables or IPVS rules), or the CNI itself with eBPF (Cilium).
Service types
| Type | Reachable from | Use it for |
|---|---|---|
| ClusterIP (default) | Inside the cluster only | Internal services: APIs, databases, caches. Most of your Services. |
| NodePort | <any-node-IP>:30000–32767 | Labs, bare-metal setups, or as the target of an external LB. Rarely exposed directly in production. |
| LoadBalancer | A public (or internal) IP from the cloud provider | Exposing a service to the Internet on EKS/GKE/AKS. Each one usually creates a paid cloud LB. (k3s fakes it with ServiceLB; bare metal uses MetalLB.) |
| ExternalName | — (a DNS CNAME) | Giving an external host, such as a managed DB endpoint, a cluster-local name. |
Headless (clusterIP: None) | DNS returns the pod IPs directly | StatefulSets (databases, Kafka) where clients must reach a specific pod: db-0.db.shop.svc. |
7.3 Ingress and the Gateway API
One LoadBalancer per app gets expensive and messy. For HTTP(S), use a single entry point that routes by hostname and path, exactly like the Nginx reverse proxy in Module 5, but configured with Kubernetes objects.
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: shop namespace: shop spec: ingressClassName: traefik tls: - hosts: [shop.example.com] secretName: shop-tls rules: - host: shop.example.com http: paths: - path: /api pathType: Prefix backend: service: { name: api, port: { number: 80 } } - path: / pathType: Prefix backend: service: { name: frontend, port: { number: 80 } }
apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: shop namespace: shop spec: parentRefs: - name: public-gateway # owned by the platform team namespace: infra hostnames: [shop.example.com] rules: - matches: - path: { type: PathPrefix, value: /api } backendRefs: - name: api port: 80 - backendRefs: - name: frontend port: 80
| Ingress | Gateway API | |
|---|---|---|
| Status | Stable and everywhere, but feature-frozen | The official successor (v1 since 2023); growing fast |
| Objects | One Ingress (+ controller-specific annotations) | GatewayClass → Gateway (infra team) → HTTPRoute/GRPCRoute/TCPRoute (app teams) |
| Features | Host/path routing + TLS; anything else via annotations | Header matching, traffic splitting (canary), cross-namespace routing, built in |
An Ingress object does nothing on its own. A controller must be installed to read it. k3s ships Traefik; managed clouds offer their own (e.g. AWS Load Balancer Controller). Note: the community ingress-nginx controller was retired (its retirement was announced in November 2025), so new clusters should pick another controller or move to the Gateway API.
7.4 Cluster DNS (CoreDNS)
Every cluster runs CoreDNS, and every pod's /etc/resolv.conf points at it. Services automatically get DNS names:
| Name | Works from |
|---|---|
api | Pods in the same namespace |
api.shop | Any namespace (service.namespace) |
api.shop.svc.cluster.local | Anywhere: the fully qualified name |
db-0.db.shop.svc.cluster.local | A specific StatefulSet pod behind a headless service |
$ cat /etc/resolv.conf search shop.svc.cluster.local svc.cluster.local cluster.local nameserver 10.43.0.10 ← the CoreDNS Service (k3s default) options ndots:5
The search list is why short names work. Because of ndots:5, a name like api.stripe.com is first tried with each search suffix (api.stripe.com.shop.svc.cluster.local…) before the real lookup. In busy clusters, adding a trailing dot (api.stripe.com.) or tuning ndots cuts DNS load.
7.5 Network policies
By default, every pod can talk to every other pod in the cluster, across all namespaces. A NetworkPolicy is a firewall for pods: once a pod is selected by any policy, only the traffic you explicitly allow gets through.
# 1) Deny all incoming traffic to every pod in the namespace apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: { name: default-deny-ingress, namespace: shop } spec: podSelector: {} # {} = all pods policyTypes: [Ingress] --- # 2) Allow only api pods to reach db pods on 5432 apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: { name: db-allow-api, namespace: shop } spec: podSelector: { matchLabels: { app: db } } policyTypes: [Ingress] ingress: - from: - podSelector: { matchLabels: { app: api } } ports: - { protocol: TCP, port: 5432 }
- NetworkPolicies are only enforced if your CNI supports them (Calico, Cilium and k3s's built-in controller do; plain Flannel alone does not). Otherwise they're silently ignored.
- If you also default-deny Egress, remember to allow DNS: UDP/TCP 53 to the
kube-systemCoreDNS pods. Otherwise every name lookup fails.
7.6 CNI plugins
The Container Network Interface (CNI) is the plug-in standard that gives pods their network. Kubernetes itself doesn't implement pod networking; the CNI does.
| CNI | How it works | NetworkPolicy | Known for |
|---|---|---|---|
| Flannel | Simple VXLAN overlay | ❌ (k3s adds its own controller) | Simplicity, k3s default |
| Calico | BGP routing or VXLAN; iptables or eBPF | ✅ plus its own extended policies | Very common in self-managed and enterprise clusters |
| Cilium | eBPF in the kernel (can replace kube-proxy) | ✅ L3–L7 (even HTTP-aware) | Performance, observability (Hubble), default on many managed platforms |
| AWS VPC CNI / Azure CNI | Pods get real VPC IPs | ✅ (with policy add-on) | EKS / AKS: pods directly reachable in the VPC (watch subnet IP exhaustion!) |
7.7 Communication between pods and external services
- Pod → Internet (egress): the node SNATs pod traffic to its own IP (or a NAT gateway in the cloud does). External firewalls see the node/NAT IP, so allowlist that IP with partners.
- Pod → managed database (e.g. RDS): connect by its DNS name. Optionally wrap it in an
ExternalNameService so apps usedb.shopeverywhere. - Internet → pod: only through a LoadBalancer Service or an Ingress/Gateway. Never expose pods directly.
- Your laptop → a pod (debugging):
kubectl port-forward svc/api 8080:80, which is the Kubernetes version of an SSH tunnel.
Install k3s (a lightweight, certified Kubernetes):
$ curl -sfL https://get.k3s.io | sh - $ sudo k3s kubectl get nodes -o wide $ mkdir -p ~/.kube && sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config && sudo chown $USER ~/.kube/config $ kubectl get pods -A # coredns, traefik, local-path, metrics-server…
If UFW is enabled, the k3s docs recommend allowing the pod/service networks: sudo ufw allow from 10.42.0.0/16 and sudo ufw allow from 10.43.0.0/16, plus 6443/tcp if other machines will use the API.
Deploy an app with 3 replicas and a ClusterIP Service:
$ kubectl create namespace shop $ kubectl -n shop create deployment web --image=traefik/whoami --replicas=3 $ kubectl -n shop expose deployment web --port=80 --target-port=80 $ kubectl -n shop get pods -o wide ; kubectl -n shop get svc,endpointslices
Notice that each pod has its own 10.42.x.x IP, while the Service has one stable 10.43.x.x IP.
Test DNS and load balancing from a temporary debug pod:
$ kubectl -n shop run tmp --rm -it --image=nicolaka/netshoot -- bash # inside the pod: tmp# cat /etc/resolv.conf tmp# dig +short web.shop.svc.cluster.local tmp# for i in 1 2 3 4 5 6; do curl -s web | grep Hostname; done # different pods answer tmp# exit
Expose it with a NodePort, then with an Ingress (k3s includes Traefik):
$ kubectl -n shop expose deployment web --name=web-np --type=NodePort --port=80 $ kubectl -n shop get svc web-np # e.g. 80:31234/TCP $ curl -s http://NODE-IP:31234 | head -3 $ kubectl -n shop create ingress web --class=traefik --rule="shop.local/*=web:80" $ curl -s -H "Host: shop.local" http://NODE-IP/ | head -3
Apply a default-deny policy and watch traffic stop:
$ cat <<'EOF' | kubectl apply -f - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: { name: default-deny-ingress, namespace: shop } spec: { podSelector: {}, policyTypes: [Ingress] } EOF $ kubectl -n shop run tmp --rm -it --image=nicolaka/netshoot -- curl -s -m 3 web || echo BLOCKED BLOCKED
Allow only pods labelled role=client:
$ cat <<'EOF' | kubectl apply -f - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: { name: web-allow-clients, namespace: shop } spec: podSelector: { matchLabels: { app: web } } ingress: - from: [ { podSelector: { matchLabels: { role: client } } } ] ports: [ { protocol: TCP, port: 80 } ] EOF $ kubectl -n shop run ok --rm -it --labels=role=client --image=nicolaka/netshoot -- curl -s -m 3 web | head -2 $ kubectl delete namespace shop # clean up
Note: the Ingress from step 4 is now blocked too, because Traefik's pods don't carry role=client. In real clusters you add a rule allowing the ingress controller's namespace.
7.8 Troubleshooting scenarios
🚑 "The Service exists, but curl hangs or is refused"
curl api.shop from a pod fails, yet the pods are Running.app: API), the pods aren't Ready (readiness probe failing), or targetPort is wrong.kubectl -n shop get endpointslices -l kubernetes.io/service-name=api. If it's empty, compare kubectl get svc api -o yaml (selector) with kubectl get pods --show-labels. Then check that targetPort matches the container port.🚑 "Ingress returns 404 for my new site"
Host doesn't match the Ingress rule (DNS points elsewhere, or you tested by IP), the ingressClassName is wrong, or the path doesn't match.kubectl describe ingress; test with curl -H "Host: shop.example.com" http://LB-IP/; check the controller's logs.🚑 "After adding NetworkPolicies, nothing can resolve names"
getaddrinfo EAI_AGAIN / "temporary failure in name resolution".k8s-app: kube-dns in kube-system.🔑 Key takeaways
- Every pod gets an IP, and pods talk across nodes without NAT. The CNI plugin makes that happen.
- Never use pod IPs. Use Services (a stable virtual IP and DNS name that load-balances to pods selected by labels).
- ClusterIP (internal) ⊂ NodePort (node ports 30000–32767) ⊂ LoadBalancer (cloud LB).
- Ingress/Gateway = an HTTP reverse proxy for the cluster; it needs a controller.
- DNS:
service.namespace.svc.cluster.local, served by CoreDNS. - NetworkPolicies are pod firewalls: default allow-all until you add policies, and they need CNI support. Don't forget DNS egress.
📝 Quiz: Module 7
1. Why shouldn't an app connect to another app's pod IP?
Show answer
2. Which Service type gives an internal-only virtual IP?
Show answer
3. A pod in namespace web needs the redis Service in namespace cache. What hostname does it use?
Show answer
redis.cache (or the full form redis.cache.svc.cluster.local).4. What is the default NodePort range?
Show answer
5. You created an Ingress, but nothing happens at all. What's most likely missing?
Show answer
ingressClassName for the installed one).6. With no NetworkPolicies, can a pod in dev reach the database pod in prod?
Show answer
7. Name two CNI plugins that enforce NetworkPolicies.
Show answer
Cloud & production networking
In the cloud you design the whole network yourself, in software: address ranges, subnets, routes, gateways, firewalls and private links. Same concepts, bigger blast radius.
- Design a VPC with public and private subnets
- Explain what makes a subnet "public"
- Give private servers outbound-only Internet
- Choose between security groups and NACLs
- Connect offices, VPCs and services privately
- Build a highly available, segmented 3-tier network
Every cloud has the same building blocks under different names. This module uses the generic idea first, then the AWS name (the most common in job ads). Section 8.9 maps everything to Azure and GCP.
8.1 Virtual Private Cloud (VPC) and subnets
A VPC is your own isolated, private network inside a cloud provider. Think of it as a virtual data centre network. You choose its CIDR (e.g. 10.0.0.0/16) and carve it into subnets, each living in one Availability Zone (AZ), which is a physically separate data centre within a region.
The VPC is a gated society with its own private street numbers. Subnets are its blocks: the public block faces the main road and has a gate (internet gateway), while the private blocks are inside, reachable only through internal streets. Security guards at each house (security groups) and at each block entrance (NACLs) check who may enter.
- Give each VPC a unique, non-overlapping range (prod
10.0.0.0/16, staging10.1.0.0/16, office10.50.0.0/16…). You'll want to connect them one day. - Leave room: make Kubernetes subnets larger (/20 or /19), because some CNIs give every pod a VPC IP.
- Cloud providers reserve some addresses in every subnet (AWS and Azure: 5; GCP: 4).
8.2 Route tables and internet gateways
Every subnet is associated with a route table, exactly like ip route on Linux. What makes a subnet "public" is one thing only: its route table sends 0.0.0.0/0 to an Internet Gateway (IGW). Instances there also need a public IP to be reachable.
| Public route table | |
|---|---|
10.0.0.0/16 | local (inside the VPC) |
0.0.0.0/0 | igw-… (Internet Gateway) |
| Private app route table (AZ A) | |
|---|---|
10.0.0.0/16 | local |
0.0.0.0/0 | nat-… (NAT Gateway A) |
A data subnet often has only the local route, so it has no path to the Internet at all. The local route is automatic and means every subnet in the VPC can route to every other subnet. Firewalls (next section) decide what's actually allowed.
8.3 NAT and NAT gateways
Private servers still need outbound Internet access for OS updates, Docker image pulls and calls to payment APIs, but nothing on the Internet should be able to start a connection to them. A NAT Gateway sits in a public subnet, swaps the private source IP for its own public IP (just like your home router in Module 1) and lets only the replies back in.
- Managed NAT gateways charge per hour and per GB. Big Docker pulls through NAT can be a surprise on the bill. VPC endpoints for S3/ECR, or a NAT instance in dev, reduce it.
- All outbound traffic leaves from the NAT's Elastic/static IP. That's the IP you give a partner or payment provider to allowlist.
- For high availability, use one NAT gateway per AZ. If it's shared, losing one AZ takes away outbound access for the other.
8.4 Security groups and network ACLs
| Security group (SG) | Network ACL (NACL) | |
|---|---|---|
| Applies to | Each instance / network interface / load balancer | A whole subnet |
| State | Stateful: replies are allowed automatically | Stateless: you must allow return traffic (ephemeral ports 1024–65535) explicitly |
| Rules | Allow only (implicit deny) | Allow and deny, evaluated in number order |
| Sources | CIDRs or other security groups ✨ | CIDRs only |
| Typical use | Main, everyday firewall | Coarse guardrails, e.g. block a malicious IP range for a whole subnet |
Security groups that reference each other
Instead of IPs, an SG rule can say "allow from members of another SG". IPs change as instances scale; group membership doesn't.
| Security group | Inbound rule | Source |
|---|---|---|
sg-alb | TCP 443, TCP 80 | 0.0.0.0/0 (the Internet) |
sg-app | TCP 5000 | sg-alb (only the load balancer) |
sg-db | TCP 5432 | sg-app (only app servers) |
sg-app | TCP 22 | No rule! Use SSM Session Manager or a bastion/VPN instead |
It's the same "expose the minimum" chain from the ShopNow introduction, now enforced by the cloud: Internet → ALB → app → DB, each hop allowed only from the previous one.
8.5 VPNs and private connectivity
| Option | Connects | Notes |
|---|---|---|
| VPC peering | VPC ↔ VPC | Simple and cheap. Not transitive (A–B and B–C doesn't mean A–C). CIDRs must not overlap. |
| Transit gateway / hub | Many VPCs + VPNs | Hub-and-spoke with central route tables. |
| Site-to-site VPN | Office/DC ↔ cloud | Encrypted IPsec tunnel over the Internet. Quick to set up. |
| Client VPN | A person's laptop ↔ cloud | WireGuard, OpenVPN, Tailscale, or the cloud's client VPN. Gives engineers access to private resources. |
| Dedicated link | DC ↔ cloud over a private circuit | Predictable latency and high bandwidth. Takes weeks to provision. |
| Private endpoints / PrivateLink | VPC ↔ a cloud service or SaaS | Reach S3, ECR or a vendor's API over private IPs, without the Internet or NAT. |
8.6 High availability and load balancers
- Spread across at least 2 AZs: load balancer nodes, app instances (an auto-scaling group), NAT gateways and a database standby.
- The load balancer health-checks targets (like Module 5) and only sends traffic to healthy ones. If an entire AZ fails, it keeps serving from the other.
- L7 (ALB / Application Gateway / HTTP LB): HTTPS termination with managed certificates, path/host routing, WAF integration. L4 (NLB): TCP/UDP, static IPs, extreme throughput, client IP preserved.
- DNS points at the LB's hostname (an ALIAS record at the apex, as in Module 3), never at an individual server.
8.7 Private database access and network segmentation
✅ Do
• Put databases in private data subnets with no public IP
• Use an SG that allows 5432/3306 only from the app SG
• Use a managed DB with Multi-AZ
• Give humans access through SSM Session Manager / a bastion / VPN with port forwarding
• Separate prod and non-prod into different VPCs/accounts
❌ Don't
• Tick "publicly accessible" on the database
• Allow 0.0.0.0/0 on 22, 3306, 5432, 6379 or 27017
• Share one flat subnet for everything
• Hard-code IPs: use DNS endpoints
• Let staging reach the prod database
$ aws ssm start-session --target i-0abc123def4567890 \ --document-name AWS-StartPortForwardingSessionToRemoteHost \ --parameters '{"host":["shop-db.xxxx.eu-central-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["15432"]}' # then: psql -h localhost -p 15432 -U shop shop
8.8 Doing it without a hyperscaler
The same design works on a simple VPS provider. Most offer private networks (Hetzner vSwitch/Networks, DigitalOcean VPC, Linode VLAN), cloud firewalls (filtered before traffic reaches the VPS, so they also cover the Docker/UFW gap) and load balancers. A typical small production setup: 1 load balancer + 2 app VPSs + 1 DB VPS on a private network, with only the LB public.
8.9 Concept mapping: AWS · Azure · GCP
| Generic concept | AWS | Azure | Google Cloud |
|---|---|---|---|
| Private network | VPC (regional) | Virtual Network (VNet) | VPC network (global) |
| Subnet | Subnet (one AZ) | Subnet (regional) | Subnet (regional) |
| Route table | Route table | Route table (UDR) | Routes |
| Internet access | Internet Gateway | System route to Internet | Default Internet gateway route |
| Outbound NAT | NAT Gateway | NAT Gateway | Cloud NAT |
| Instance-level firewall | Security Group | Network Security Group (NIC) | VPC firewall rules (by tag / service account) |
| Subnet-level firewall | Network ACL | NSG on subnet | Firewall rules / hierarchical policies |
| L7 load balancer | Application LB (ALB) | Application Gateway / Front Door | Application Load Balancer (HTTP(S)) |
| L4 load balancer | Network LB (NLB) | Azure Load Balancer | Network Load Balancer |
| Network-to-network | VPC Peering / Transit Gateway | VNet Peering / Virtual WAN | VPC Peering / Network Connectivity Center |
| Site-to-site VPN | Site-to-Site VPN | VPN Gateway | Cloud VPN |
| Dedicated link | Direct Connect | ExpressRoute | Cloud Interconnect |
| Private service access | VPC Endpoints / PrivateLink | Private Endpoint / Private Link | Private Service Connect |
| Managed DNS | Route 53 | Azure DNS | Cloud DNS |
| Keyless admin access | SSM Session Manager | Azure Bastion | IAP TCP forwarding |
Design on paper. Fill in this table for ShopNow in one region with 2 AZs (use Figure 8.1 as a guide):
| Item | Your answer |
|---|---|
| VPC CIDR (must not overlap office 10.50.0.0/16) | … |
| 6 subnet CIDRs (public / app / data × 2 AZs) | … |
| Route tables and their 0.0.0.0/0 targets | … |
| Security groups and inbound rules (source = CIDR or SG) | … |
| How engineers reach the DB for maintenance | … |
Check it against these questions: Can the Internet reach an app server directly? (It shouldn't.) Can app servers download updates? (They should.) Can the DB reach the Internet? (It shouldn't need to.) What happens if AZ A disappears?
Optional build (AWS). In the VPC console, use "Create VPC → VPC and more", which draws a live diagram while you choose 2 AZs, 2 public + 2 private subnets and NAT gateways ("None" to stay free). Then create sg-alb, sg-app and sg-db with the referencing rules from section 8.4.
NAT gateways, load balancers, Elastic IPs that aren't attached, and RDS instances cost money even when idle. Set a billing alarm, and delete everything at the end of the lab.
Verify with the CLI (read-only):
$ aws ec2 describe-vpcs --query 'Vpcs[].{id:VpcId,cidr:CidrBlock}' --output table $ aws ec2 describe-subnets --filters Name=vpc-id,Values=vpc-XXXX \ --query 'Subnets[].{az:AvailabilityZone,cidr:CidrBlock,publicIp:MapPublicIpOnLaunch}' --output table $ aws ec2 describe-route-tables --filters Name=vpc-id,Values=vpc-XXXX \ --query 'RouteTables[].Routes[].{dest:DestinationCidrBlock,gw:GatewayId,nat:NatGatewayId}' --output table
8.10 Troubleshooting scenarios
🚑 "Private instance can't run apt update or docker pull"
0.0.0.0/0 → NAT gateway route, the NAT gateway is in a private subnet (it must be public), or the SG/NACL blocks outbound traffic.🚑 "Load balancer says targets are unhealthy → users get 502/503"
sg-app doesn't allow the health-check port from sg-alb, the health-check path returns 301/404, or the app listens on 127.0.0.1./health (returning 200), and bind the app to 0.0.0.0. Test from inside the VPC with curl -v http://10.0.11.25:5000/health.🚑 "Web server has a public IP and SG allows 443, but it still times out"
0.0.0.0/0 → igw to the subnet's route table. In the NACL, allow outbound 1024–65535, or revert to the default NACL. VPC Flow Logs show REJECTs clearly.🚑 "VPC peering is active, but nothing gets through"
pcx-… in each VPC's relevant route tables, then allow the peer CIDR in the SGs.🔑 Key takeaways
- VPC = your private network in the cloud. Plan non-overlapping CIDRs and spread across ≥ 2 AZs.
- A subnet is public only if its route table sends 0.0.0.0/0 to an Internet Gateway.
- NAT gateways give private subnets outbound-only Internet access, with a fixed egress IP.
- Security groups are stateful, allow-only and can reference each other. NACLs are stateless, ordered, allow+deny, per subnet.
- Connect networks with peering, a transit hub, VPN or a dedicated link. Reach cloud services privately with endpoints.
- Databases go in private subnets with no public IP, reachable only from the app tier. Humans get in via SSM/bastion/VPN.
📝 Quiz: Module 8
1. What single setting makes a subnet "public"?
Show answer
2. Where must a NAT gateway be placed, and what route do private subnets need?
Show answer
0.0.0.0/0 → nat-gateway.3. A NACL allows inbound 443 but has a "deny all" outbound rule. Will HTTPS work? Why?
Show answer
4. Write the inbound rule for the DB security group in a 3-tier app.
Show answer
5. VPC A is peered with B, and B with C. Can A reach C?
Show answer
6. A payment provider must allowlist your servers' IP. Your app servers are private behind NAT. Which IP do you give them?
Show answer
7. Name the Azure and GCP equivalents of an AWS Security Group.
Show answer
Network security & monitoring
Protect what you built and watch it. You need to know something is wrong before your users tell you, and to see exactly what is happening on the wire.
- Audit and minimise exposed ports
- Harden SSH with keys
- Encrypt internal traffic too
- Explain defence in depth and zero trust
- Set up a WireGuard VPN
- Capture and filter packets
- Measure where request time is spent
- Choose what to monitor and alert on
9.1 Firewall rules and least-privilege access
Least privilege means every user, server and service gets only the access it needs, and nothing more. For networks that means: deny by default, then open the fewest ports, to the fewest sources, for the shortest time.
| ShopNow port | Service | Who should reach it | How it's enforced |
|---|---|---|---|
443, 80 | Nginx / load balancer | 🌍 Everyone | SG/UFW allow 0.0.0.0/0 |
22 | SSH | 👩💻 Admins only | Allow from office/VPN IP only, keys only, or no SSH at all (SSM) |
5000 | Backend API | ⚙️ Nginx / LB only | Bind to 127.0.0.1, or SG source = LB SG |
5432 | PostgreSQL | 🔒 Backend only | Private subnet / internal Docker network, SG source = app SG |
9100, 3000 | node_exporter, Grafana | 📊 Monitoring server / VPN | Allow from the monitoring IP only. Never public. |
$ sudo ss -tulnp | grep -v '127.0.0.1\|::1' # what listens on non-loopback addresses $ sudo ufw status verbose $ docker ps --format '{{.Names}}\t{{.Ports}}' # remember: published ports bypass UFW $ nmap -Pn -p- --min-rate 1000 YOUR-SERVER-IP # run from OUTSIDE, only on servers you own
9.2 TLS certificates and encryption
- Public traffic: HTTPS only, TLS 1.2 and 1.3 (disable 1.0/1.1), HTTP→HTTPS redirect, HSTS. Test with SSL Labs or
testssl.sh. - Internal traffic: "it's on a private network" is not encryption. Use TLS to databases (
sslmode=require/verify-fullfor PostgreSQL), to Redis and to internal APIs, especially across VPCs or data centres. - Service-to-service: mTLS (mutual TLS: both sides present certificates) proves identity in both directions. Service meshes (Istio, Linkerd, Cilium) automate it in Kubernetes.
- Certificate lifecycle: automate issuance and renewal (certbot, cert-manager), and alert 14+ days before expiry.
ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
9.3 SSH keys and secure remote access
$ ssh-keygen -t ed25519 -C "you@laptop" # creates id_ed25519 + id_ed25519.pub $ ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@203.0.113.10 $ ssh deploy@203.0.113.10 # should log in without a password
Host shop-prod HostName 203.0.113.10 User deploy IdentityFile ~/.ssh/id_ed25519 IdentitiesOnly yes Host shop-db HostName 10.0.21.15 User deploy ProxyJump shop-prod # reach the private host through the public one # now just: ssh shop-prod / ssh shop-db / scp file shop-prod:/tmp/
PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no # or prohibit-password if root must use keys PubkeyAuthentication yes AllowUsers deploy # only these users may log in MaxAuthTries 3
$ sudo sshd -t && sudo systemctl reload ssh # test the config, then reload # KEEP your current session open and test a NEW login in another terminal before logging out! $ sudo apt install -y fail2ban # bans IPs with repeated failed logins $ sudo fail2ban-client status sshd
Before disabling passwords, make sure key login works for a user with sudo. Keep the existing session open while testing. Changing the SSH port only reduces log noise; it isn't real security. Keys, allow-listing and fail2ban are.
9.4 Network segmentation
Segmentation splits a network into zones with controlled crossings, so a compromised web server can't directly reach the database, the CI server or the office laptops. You've already seen it at every level:
Linux
Bind internal services to 127.0.0.1; UFW rules per source subnet.
Docker
Separate frontend / backend networks; internal: true for data.
Kubernetes
Namespaces + default-deny NetworkPolicies.
Cloud
Public / app / data subnets, SG chaining, separate VPCs or accounts for prod.
9.5 VPNs and zero-trust principles
Zero trust principles: never trust, always verify; least-privilege access per application; assume breach (segment and log everything); strong identity (SSO + MFA) and device checks. Tools include Cloudflare Access, Tailscale ACLs, Teleport, Google BeyondCorp/IAP, and service-mesh mTLS inside clusters. A VPN is still useful, but as one layer, not the whole strategy.
A minimal WireGuard VPN
[Interface] Address = 10.8.0.1/24 ListenPort = 51820 # UDP! PrivateKey = <server-private-key> [Peer] # your laptop PublicKey = <laptop-public-key> AllowedIPs = 10.8.0.2/32
[Interface] Address = 10.8.0.2/24 PrivateKey = <laptop-private-key> [Peer] # the server PublicKey = <server-public-key> Endpoint = 203.0.113.10:51820 AllowedIPs = 10.8.0.0/24 # only VPN traffic goes through the tunnel PersistentKeepalive = 25
$ sudo apt install -y wireguard $ wg genkey | tee server.key | wg pubkey > server.pub # keep *.key files secret, chmod 600 $ sudo ufw allow 51820/udp $ sudo systemctl enable --now wg-quick@wg0 ; sudo wg show # Now restrict admin ports to the VPN: sudo ufw allow from 10.8.0.0/24 to any port 22 proto tcp
9.6 Packet capture with tcpdump and Wireshark
When logs aren't enough, look at the actual packets. tcpdump captures on servers (CLI); Wireshark analyses captures with a GUI on your laptop. Together they answer "did the packet even arrive?", which is the ultimate ground truth.
| Goal | Command |
|---|---|
| All DNS traffic | sudo tcpdump -i any -n port 53 |
| Backend ↔ database | sudo tcpdump -i any -n host 10.0.21.15 and port 5432 |
| Only new connection attempts (SYN) | sudo tcpdump -i any -n 'tcp[tcpflags] & tcp-syn != 0' |
| Connection resets | sudo tcpdump -i any -n 'tcp[tcpflags] & tcp-rst != 0' |
| Readable HTTP (plain text only) | sudo tcpdump -i any -nn -A port 80 |
| Container traffic | sudo tcpdump -i docker0 -n (or a br-… interface) |
| Save for Wireshark | sudo tcpdump -i any -w /tmp/cap.pcap -c 2000 port 443 |
$ scp shop-prod:/tmp/cap.pcap . # Wireshark display filters: tcp.flags.reset==1 · dns · http.response.code >= 500 · tcp.analysis.retransmission
HTTPS payloads are encrypted, so a capture shows IPs, ports, timing, the handshake and the SNI hostname, but not the content. That's usually enough to prove "the packet never arrived", "the server sent RST", or "there were 3-second retransmission gaps". Capture only on systems you're authorised to monitor, and treat pcaps as sensitive data.
9.7 Monitoring latency, traffic and connection errors
Watch the four golden signals for every service: Latency (how long requests take; track p95/p99, not just the average), Traffic (requests/s, bandwidth), Errors (5xx rate, connection failures, timeouts) and Saturation (CPU, connections, bandwidth limits).
Where does the time go? Break down one request
$ curl -o /dev/null -s -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total} code:%{http_code}\n' https://shop.example.com/api/products dns:0.020 connect:0.055 tls:0.115 ttfb:0.295 total:0.310 code:200
| Slow part | Points to |
|---|---|
| DNS (> 100 ms) | Slow or distant resolver, no caching, ndots issues in Kubernetes |
| TCP connect | Network distance (choose a closer region/CDN), packet loss, overloaded LB |
| TLS | Extra round trips (old TLS, no session resumption), a long certificate chain, a CPU-starved proxy |
| TTFB (server processing) | Application code, database queries, upstream API calls |
| Download | Large responses: enable compression, a CDN, pagination |
Monitoring stack and alerts
Common tools
Prometheus + node_exporter (host network stats) + blackbox_exporter (probe HTTP, TCP, DNS, ICMP from outside) + Grafana dashboards · Uptime Kuma (simple self-hosted uptime checks) · Nginx access logs → Loki/ELK · cloud: CloudWatch, VPC Flow Logs, Azure Monitor, Cloud Monitoring.
Alerts worth having
🚨 Site/API down from outside for 2+ min
🚨 5xx rate > 1–2% for 5 min
🚨 p95 latency above your target
🚨 TLS cert expires in < 14 days
🚨 Bandwidth or connection count near the limit
🚨 Spikes in failed SSH logins or firewall drops
$ ss -s # summary: established, time-wait, orphaned… $ nstat -az | grep -Ei 'retrans|resets|listendrops' # TCP retransmissions = packet loss/congestion $ ip -s link show ens3 # RX/TX errors and drops on the NIC $ awk '$9 ~ /^5/' /var/log/nginx/access.log | tail # recent 5xx responses $ awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn # status code counts
Set up key login, then disable passwords (keep a session open!):
laptop$ ssh-keygen -t ed25519 ; ssh-copy-id user@VM-IP ; ssh user@VM-IP vm$ echo -e "PasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin no" | sudo tee /etc/ssh/sshd_config.d/99-hardening.conf vm$ sudo sshd -t && sudo systemctl reload ssh laptop$ ssh -o PubkeyAuthentication=no user@VM-IP user@VM-IP: Permission denied (publickey). ← passwords are now refused ✔
Look at who's been knocking (on a public VPS this list is usually long):
$ sudo journalctl -u ssh --since today | grep -Ei 'invalid user|failed' | tail $ sudo apt install -y fail2ban && sudo systemctl enable --now fail2ban $ sudo fail2ban-client status sshd
Capture a DNS lookup and a plain HTTP request:
$ sudo tcpdump -i any -n port 53 -c 4 & $ dig +short example.org @1.1.1.1 IP 192.168.1.50.41234 > 1.1.1.1.53: 12345+ [1au] A? example.org. (40) IP 1.1.1.1.53 > 192.168.1.50.41234: 12345 1/0/1 A 93.184.215.14 (56) $ sudo tcpdump -i any -nn -A -c 20 port 80 & $ curl -s http://example.org > /dev/null # you'll see "GET / HTTP/1.1" and "Host: example.org" in clear text
Save a capture and open it in Wireshark on your laptop:
$ sudo tcpdump -i any -w /tmp/lab9.pcap -c 200 'port 443 or port 53' & $ curl -s https://example.org > /dev/null laptop$ scp user@VM-IP:/tmp/lab9.pcap . # open it, filter "tls.handshake" and find the SNI
Measure the timing of three different sites and compare where the time goes:
$ for u in https://example.org https://github.com https://www.google.com; do
curl -o /dev/null -s -w "$u dns:%{time_namelookup} conn:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer}\n" $u; done(Optional) Run an uptime monitor in Docker and add HTTP and TCP checks for your lab services:
$ docker run -d --name kuma -p 127.0.0.1:3001:3001 -v kuma:/app/data louislam/uptime-kuma:1 laptop$ ssh -N -L 3001:127.0.0.1:3001 user@VM-IP # then browse http://localhost:3001
Notice that you used an SSH tunnel to reach an admin UI without exposing it. That's least privilege in practice.
9.8 Troubleshooting scenarios
🚑 "Thousands of failed SSH logins per hour"
auth.log fills with Invalid user admin from 45.x.x.x.🚑 "Random requests take exactly 1, 3 or 7 seconds longer"
nstat retransmits, ip -s link drops and dmesg | grep conntrack. Capture with tcpdump and look for retransmissions in Wireshark. Use curl -w to see whether the delay is in DNS or connect.🚑 "Monday morning: NET::ERR_CERT_DATE_INVALID"
sudo certbot renew and read the error. Fix port 80 or switch to the DNS-01 challenge. Add a cert-expiry alert (blackbox_exporter / Uptime Kuma) so it never surprises you again.🔑 Key takeaways
- Least privilege: deny by default, minimal ports, minimal sources. Audit with
ss,ufw status,docker psand an externalnmap. - Encrypt everything in transit, including internal traffic. Automate certificates and alert before expiry.
- SSH: ed25519 keys, no passwords, no root login,
AllowUsers, fail2ban, admin ports only via VPN. - Segment at every level; layer your defences.
- Zero trust = verify identity, device and policy for every request, not network location.
tcpdumpshows the truth on the wire; Wireshark helps you analyse it.- Monitor latency (p95), traffic, errors and saturation.
curl -wshows where time is spent.
📝 Quiz: Module 9
1. Which three sshd settings most improve SSH security?
Show answer
PasswordAuthentication no, PermitRootLogin no and AllowUsers <user> (plus key auth and fail2ban).2. What does "zero trust" mean in one sentence?
Show answer
3. Write a tcpdump command that shows traffic between this host and 10.0.21.15 on port 5432.
Show answer
sudo tcpdump -i any -n host 10.0.21.15 and port 54324. curl -w shows dns 0.01, connect 0.02, tls 0.05, ttfb 2.40. Where is the problem?
Show answer
5. Can tcpdump show the JSON body of an HTTPS API request?
Show answer
6. Name the four golden signals.
Show answer
7. Which UDP port does WireGuard use by default?
Show answer
A request's full journey
Everything from Modules 1–9 in one story: what happens, step by step, when a customer opens ShopNow, and what can break at each step.
A customer opens https://shop.example.com and the product list loads. In about 300 milliseconds, more than a dozen systems cooperated. If you can explain this page from memory, you understand DevOps networking.
| # | What happens | Concepts (module) | If it breaks, you see… | Check with |
|---|---|---|---|---|
| 1 | The user enters https://shop.example.com. The browser parses scheme, host, port (443 implied) and path. | URL anatomy (3) | — | — |
| 2 | The browser/OS cache, then /etc/hosts, then the recursive resolver: root → .com → authoritative → A 203.0.113.10, cached for the TTL. | DNS, TTL (3) | DNS_PROBE_FINISHED_NXDOMAIN, old IP | dig, dig +trace |
| 3 | 203.0.113.10 isn't in the local subnet, so the packet goes to the default gateway. The router NATs the private source IP to its public IP, and ISP routers forward it hop by hop. | IP, subnet, gateway, NAT, routing (1) | No Internet at all; high latency/loss | ip route, mtr |
| 4 | The cloud firewall/security group and UFW check: TCP 443 from anywhere → allow. | Firewalls, SG (4, 8) | Timeout | nc -vz host 443 |
| 5 | TCP handshake SYN → SYN-ACK → ACK with Nginx/LB on port 443. | TCP, ports (2) | Connection refused (nothing listening) | ss -tlnp |
| 6 | TLS handshake: SNI = shop.example.com, the server sends its certificate chain, the browser verifies it, and keys are agreed. | TLS, certificates (5) | Certificate expired / name mismatch | openssl s_client, curl -v |
| 7 | Encrypted GET /api/products arrives. Nginx matches server_name and location /api/. | HTTP, reverse proxy (2, 5) | 404, redirect loops | Nginx access/error logs |
| 8 | Nginx proxies to the upstream (127.0.0.1:5000, Docker api:5000 or a Kubernetes Service) and adds X-Forwarded-* headers. A load balancer picks a healthy instance. | Proxying, LB, Docker/K8s networking (5–7) | 502 (backend down) / 504 (slow) | curl 127.0.0.1:5000/health |
| 9 | The backend resolves db (Docker DNS, CoreDNS or a cloud endpoint) and opens TCP 5432 over the private network/subnet. The SG allows it only from the app tier. | Service discovery, segmentation (6–8) | ECONNREFUSED / ETIMEDOUT | getent hosts db, nc -vz db 5432 |
| 10 | PostgreSQL runs the query and returns rows to the backend, which builds JSON. | (application) | Slow TTFB | curl -w, DB slow-query log |
| 11 | The response flows back: backend → Nginx (compression, headers) → TLS encryption → Internet → the router reverses NAT → browser. | Encapsulation, NAT (1, 2) | Partial loads, resets | tcpdump |
| 12 | The browser renders the page. The TCP/TLS connection is kept alive for the next requests, and DNS stays cached until the TTL expires. Monitoring records latency and status. | Keep-alive, caching, monitoring (5, 9) | — | Dashboards, alerts |
The questions every DevOps engineer must be able to answer
Why is the domain not opening?
Go bottom-up: does DNS resolve to the right IP (dig)? Is 443 reachable (nc), or does it time out (firewall) or get refused (service)? Is the certificate valid? → Modules 3, 4, 5.
Why does HTTPS work but the API returns 502?
Nginx is fine; its upstream isn't. The backend is down, on the wrong port/address, or crashed. Read the Nginx error log. → Module 5.
Why can't the backend connect to PostgreSQL?
Wrong host (localhost in a container), DB listening on 127.0.0.1 only, a firewall/SG blocking 5432, or no shared Docker network. Refused vs timeout tells you which. → Modules 2, 6, 8.
Why can't a container reach another container?
Not on the same user-defined network, using the default bridge (no DNS), using an IP instead of the service name, or an internal/policy restriction. → Modules 6, 7.
Why does it work internally but not from the Internet?
Bound to 127.0.0.1, no public IP or IGW route, SG/UFW/NACL blocking, missing port publishing, or DNS pointing elsewhere. → Modules 1, 4, 8.
Which ports should be exposed?
Publicly: only 80 and 443 (on the LB/proxy). Admin (22) only from a VPN/office, or not at all (SSM). Everything else stays private. → Module 9.
The troubleshooting playbook
A calm, repeatable method for "it's down!", plus ten real incidents and how they were solved.
The method
1 · Define
What exactly fails? For whom (everyone, one region, one user)? Since when? What changed (deploy, DNS, firewall, certificate)?
2 · Locate
Walk the path from Figure 10.1 and test each hop: DNS → reachability → port → TLS → HTTP → upstream → DB.
3 · Read the clue
Timeout = dropped (firewall/route). Refused = no listener. 502 = upstream down. 504 = upstream slow. NXDOMAIN = DNS.
4 · Fix one thing
Change one variable at a time, and back up configs first. Validate (nginx -t, sshd -t, netplan try) before you reload.
5 · Verify
Test from the user's point of view (outside the network), not just from the server.
6 · Prevent
Add the missing alert, health check or documentation. Write a short post-incident note.
Ten real incidents
1 · 🌐 "The new domain doesn't open"
dig +short NS domain → add the record at the real DNS host → dig @1.1.1.1 to confirm.2 · 🧱 "502 Bad Gateway after a deploy"
/api call returns 502.PORT=8000 from a changed env file; Nginx still proxies to 5000.ss -tlnp shows :8000 → align the env and proxy_pass → add a post-deploy health check.3 · ⏳ "504 on checkout only"
curl -v https://api.payment.example from the backend times out → allow egress (or route via the NAT with the allowlisted IP) → add a short client timeout so users get a clear error, not a 504.4 · 💾 "Backend can't connect to PostgreSQL"
ETIMEDOUT 10.0.21.15:5432 after moving the DB to a new subnet.nc -vz 10.0.21.15 5432 from an app host.5 · 🐳 "Container can't reach another container"
getaddrinfo ENOTFOUND redis in the worker container.docker run on the default bridge, outside the Compose network.compose.yaml (or --network project_default). Check with docker network inspect.6 · 🏠 "Works internally but not from the Internet"
curl from another VPC server works; external users time out.0.0.0.0/0 → IGW (it was launched in the "private" subnet by mistake).7 · 🔐 "Mobile app fails, browsers work"
cert.pem instead of fullchain.pem. Browsers fetch the missing intermediate themselves; apps don't.fullchain.pem. Check with openssl s_client -showcerts or SSL Labs.8 · ☸️ "Kubernetes service times out after a label refactor"
curl api.shop hangs.app: api to app.kubernetes.io/name: api; the Service selector wasn't updated, so there are no endpoints.kubectl get endpointslices is empty → align the selector → endpoints appear.9 · 🐢 "Site slow for users in another continent"
curl -w from a remote VPS.10 · 🔥 "After 'tightening' the firewall, DNS broke everywhere"
apt, curl and app API calls fail with name-resolution errors; IP-based tests work.Post-incident note template
## Incident: API 502 errors – 2026-10-24 19:05–19:20 (15 min) **Impact:** 100% of /api requests failed; checkout unavailable. **Detection:** Uptime alert at 19:07 (2 min after deploy). **Root cause:** New release listened on :8000; nginx proxy_pass still :5000. **Fix:** Aligned PORT env + proxy_pass, reloaded nginx at 19:20. **Prevention:** Post-deploy smoke test hits /api/health through nginx; port defined in one place.
Cheat sheets
Everything you'll look up again and again, on a few pages. Print them and keep them next to your keyboard.
🔌 Ports
| 22/tcp | SSH, SFTP, SCP |
| 53/udp+tcp | DNS |
| 80/tcp | HTTP (redirect + ACME) |
| 443/tcp (+udp) | HTTPS (HTTP/3 = UDP) |
| 25 · 587 · 993 | SMTP · submission · IMAPS |
| 123/udp | NTP |
| 3306 · 5432 | MySQL · PostgreSQL |
| 6379 · 27017 | Redis · MongoDB |
| 6443 · 10250 | Kubernetes API · kubelet |
| 30000–32767 | Kubernetes NodePorts |
| 9090 · 9100 · 3000 | Prometheus · node_exporter · Grafana |
| 51820/udp | WireGuard |
| 4789/udp | VXLAN (overlays) |
🌐 DNS records
| A / AAAA | name → IPv4 / IPv6 |
| CNAME | name → another name (not at apex) |
| ALIAS/ANAME | CNAME-like at the apex (provider feature) |
| MX | mail servers + priority |
| TXT | SPF, DKIM, DMARC, verification |
| NS | authoritative nameservers |
| SOA | zone metadata |
| PTR | IP → name (set by the IP owner) |
| CAA | which CAs may issue certs |
| SRV | service host + port |
dig +short x | quick answer |
dig +trace x | root → TLD → authoritative |
dig @1.1.1.1 x | ask a specific resolver |
dig -x IP | reverse lookup |
🐧 Linux network commands by task
| My IPs | ip -br addr · hostname -I |
| Public IP | curl ifconfig.me |
| Interfaces | ip -br link · ip -s link |
| Routes | ip route · ip route get IP |
| ARP | ip neigh |
| Listening | sudo ss -tulnp |
| Connections | ss -tnp state established |
| Reachability | ping -c4 IP · mtr -rwc20 host |
| Port test | nc -vz -w5 host port |
| HTTP test | curl -vI https://host |
| Timing | curl -w '%{time_total}' |
| DNS config | resolvectl status |
| Capture | sudo tcpdump -i any -n port 443 |
| Scan (own hosts) | nmap -Pn host |
🧮 CIDR quick table
| /32 | 1 address · single host |
| /30 | 4 · 2 usable |
| /29 | 8 · 6 usable |
| /28 | 16 · 14 usable · mask .240 |
| /27 | 32 · 30 usable · mask .224 |
| /26 | 64 · 62 usable · mask .192 |
| /25 | 128 · 126 usable · mask .128 |
| /24 | 256 · 254 usable · 255.255.255.0 |
| /20 | 4,096 |
| /16 | 65,536 · 255.255.0.0 |
| /8 | 16.7 M · 255.0.0.0 |
| Private | 10/8 · 172.16/12 · 192.168/16 |
| Special | 127/8 loopback · 169.254/16 link-local · 100.64/10 CGNAT |
| Formula | 2(32−n) addresses; usable −2 (cloud −5) |
🚦 HTTP status codes
| 200 / 201 / 204 | OK / Created / No Content |
| 301 / 308 | Permanent redirect |
| 302 / 307 | Temporary redirect |
| 304 | Not modified (cache) |
| 400 | Bad request |
| 401 / 403 | Not authenticated / Forbidden |
| 404 | Not found (path/location/Ingress) |
| 413 | Body too large (client_max_body_size) |
| 429 | Rate limited |
| 500 | App error → app logs |
| 502 | Upstream down / refused → backend |
| 503 | No healthy upstream / maintenance |
| 504 | Upstream timeout → slow backend / blocked |
🧱 UFW
| Defaults | ufw default deny incoming |
| SSH first! | ufw allow OpenSSH |
| Web | ufw allow 80,443/tcp |
| From subnet | ufw allow from 10.0.1.0/24 to any port 5432 proto tcp |
| Rate limit | ufw limit OpenSSH |
| Enable | ufw enable |
| Status | ufw status numbered |
| Delete | ufw delete 3 |
| Logs | ufw logging on · /var/log/ufw.log |
| ⚠ Docker | Published ports bypass UFW |
🐳 Docker networking
| List | docker network ls |
| Create | docker network create appnet |
| Internal | docker network create --internal db |
| Run on net | docker run --network appnet … |
| Attach | docker network connect appnet c1 |
| Publish | -p 8080:80 · -p 127.0.0.1:8080:80 |
| Ports of c1 | docker port c1 |
| IP of c1 | docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' c1 |
| Inspect net | docker network inspect appnet |
| Debug shell | docker run --rm -it --network appnet nicolaka/netshoot |
| Remember | localhost = the container itself · use service names |
☸️ Kubernetes networking
| Pod IPs | kubectl get pods -o wide |
| Services | kubectl get svc -A |
| Endpoints | kubectl get endpointslices -n ns |
| Expose | kubectl expose deploy web --port 80 |
| Ingress | kubectl get ingress -A · describe |
| Policies | kubectl get networkpolicy -A |
| Debug pod | kubectl run t --rm -it --image=nicolaka/netshoot -- bash |
| Port-forward | kubectl port-forward svc/api 8080:80 |
| DNS name | svc.ns.svc.cluster.local |
| CoreDNS logs | kubectl -n kube-system logs -l k8s-app=kube-dns |
🟩 Nginx essentials
| Test config | sudo nginx -t |
| Reload | sudo systemctl reload nginx |
| Logs | /var/log/nginx/{access,error}.log |
| Proxy | proxy_pass http://127.0.0.1:5000; |
| Headers | Host, X-Real-IP, X-Forwarded-For/Proto |
| WebSocket | proxy_http_version 1.1 + Upgrade/Connection |
| Timeouts | proxy_connect_timeout · proxy_read_timeout |
| Uploads | client_max_body_size 10m; |
| LB | upstream {…} + least_conn / ip_hash |
| HTTPS | certbot --nginx -d host |
☁️ Cloud networking rules of thumb
| VPC CIDR | /16, unique per environment |
| Public subnet | route 0.0.0.0/0 → IGW |
| Private subnet | route 0.0.0.0/0 → NAT GW |
| Data subnet | local route only |
| SG | stateful, allow-only, reference other SGs |
| NACL | stateless, allow+deny, ephemeral ports! |
| HA | ≥ 2 AZs, NAT per AZ, LB health checks |
| Admin | SSM / bastion / VPN, never 0.0.0.0/0:22 |
| Peering | not transitive, no overlapping CIDRs |
🔍 Error → meaning
| Connection refused | reached host, nothing listening on port |
| Timed out | dropped: firewall/SG/route/host down |
| No route to host | routing/ARP failure or REJECT rule |
| NXDOMAIN | name doesn't exist in DNS |
| SERVFAIL | DNS server failure / DNSSEC |
| EAI_AGAIN | resolver unreachable (temporary) |
| Address in use | port already taken: ss -tlnp |
| Cert expired / mismatch | renewal failed / wrong hostname |
| 502 / 504 | upstream down / upstream slow |
🧭 Troubleshooting order
| 1 Link | ip -br link |
| 2 IP | ip -br addr |
| 3 Gateway | ip route · ping GW |
| 4 Internet | ping 1.1.1.1 · mtr |
| 5 DNS | dig +short name |
| 6 Port | nc -vz host port |
| 7 TLS | curl -vI https://… |
| 8 App | status code + logs |
| 9 Upstream | curl 127.0.0.1:PORT/health |
| 10 Wire | tcpdump |
Learning roadmap & glossary
The order to practise in, projects that prove your skills, and every term from these notes in plain English.
Your practical learning order
Don't study networking theory on its own for months. Learn just enough fundamentals, then deploy something real on a VPS and break it on purpose. You don't need to become a network engineer, but you should be able to deploy, secure and troubleshoot applications independently.
Portfolio projects that prove these skills
🥉 Project 1: Secure VPS web stack
Your own domain → DNS A record → Ubuntu VPS with UFW (22 from your IP only, 80/443 public) → Nginx reverse proxy → a small API on 127.0.0.1 → Let's Encrypt HTTPS with auto-renewal → Uptime Kuma monitoring. Write up the architecture with a diagram.
🥈 Project 2: Dockerised 3-tier app
Compose with nginx, api, db and redis on separate frontend/backend (internal) networks; only Nginx published; health checks; prove with nmap that only 80/443 are open.
🥇 Project 3: Kubernetes with policies
k3s (or a managed cluster) running the same app: Deployments, ClusterIP Services, Ingress/Gateway with cert-manager TLS, and default-deny NetworkPolicies allowing only frontend → api → db.
🏆 Project 4: Cloud VPC with IaC
Terraform a 2-AZ VPC (public/app/data subnets, NAT, SG chaining), an ALB, an auto-scaling app tier and a private DB. Access it via SSM only. Add VPC Flow Logs and an architecture diagram to your README.
If you want structured goals: CompTIA Network+ (fundamentals), CKA (Kubernetes, including networking), and a cloud associate cert such as AWS Solutions Architect – Associate, later the AWS Advanced Networking – Specialty. Projects matter more than certificates in interviews.
Glossary
- ACL / NACL
- Access control list. In the cloud, a stateless subnet-level firewall with ordered allow/deny rules.
- ALB / NLB
- AWS Application (L7, HTTP) and Network (L4, TCP/UDP) Load Balancers.
- ARP
- Address Resolution Protocol: finds the MAC address for an IP on the local network.
- Availability Zone (AZ)
- A physically separate data centre within a cloud region.
- Bastion / jump host
- A hardened server used as the only SSH entry point into a private network.
- Bridge
- A virtual switch connecting interfaces, e.g. Docker's
docker0. - CA (Certificate Authority)
- A trusted organisation that signs TLS certificates (e.g. Let's Encrypt).
- CDN
- Content Delivery Network: servers worldwide that cache content close to users.
- CIDR
- The
address/prefixnotation for IP ranges, e.g.10.0.0.0/16. - Client / server
- The client starts a connection; the server listens and responds.
- CNAME
- A DNS alias from one name to another name.
- CNI
- Container Network Interface: the plugin system that gives Kubernetes pods networking (Flannel, Calico, Cilium).
- Conntrack
- The kernel's connection-tracking table that makes Linux firewalls stateful.
- CoreDNS
- The DNS server inside Kubernetes clusters.
- DHCP
- Automatically assigns IP, mask, gateway and DNS to devices (Discover-Offer-Request-Ack).
- DNAT / SNAT
- Destination/Source NAT: rewriting the destination (port publishing) or the source (outbound) address.
- DNS
- Domain Name System: translates names into IP addresses and other records.
- Egress / ingress
- Outbound / inbound traffic (also the name of the Kubernetes Ingress resource).
- Encapsulation
- Each layer wrapping data in its own header on the way down the stack.
- Ephemeral port
- A temporary, random source port used by a client for one connection.
- Firewall
- Filters traffic by rules (IP, port, protocol, state).
- Gateway (default)
- The router a host sends traffic to when the destination isn't in its subnet.
- Gateway API
- The successor to Kubernetes Ingress: GatewayClass, Gateway and HTTPRoute objects.
- Handshake (TCP / TLS)
- The setup exchange before data: SYN, SYN-ACK, ACK for TCP; hello and key exchange for TLS.
- Health check
- A periodic probe a load balancer uses to decide whether a backend gets traffic.
- HSTS
- A header telling browsers to always use HTTPS for a domain.
- HTTP / HTTPS
- The web's request/response protocol; HTTPS is HTTP inside TLS.
- ICMP
- A control/diagnostic protocol with no ports, used by ping and traceroute.
- Internet Gateway (IGW)
- The cloud component that connects a VPC to the Internet.
- IP address
- The logical address of a network interface (IPv4: 32-bit, IPv6: 128-bit).
- Keep-alive
- Reusing one TCP/TLS connection for multiple HTTP requests.
- kube-proxy
- Programs Service forwarding rules (iptables/IPVS) on every Kubernetes node.
- LAN / WAN
- Local network in one site / a network spanning large distances.
- Latency
- The time for data to travel; RTT is the round-trip time.
- Load balancer
- Spreads traffic across multiple backends and removes unhealthy ones.
- Loopback
127.0.0.1/::1, the machine talking to itself.- MAC address
- The 48-bit hardware address used for delivery within a LAN.
- MTU
- Maximum Transmission Unit: the largest frame payload (usually 1500 bytes).
- mTLS
- Mutual TLS: both client and server present certificates.
- NAT / NAT gateway
- Translating private addresses to public ones; the managed cloud service that does it for private subnets.
- Netfilter
- The Linux kernel packet-filtering framework behind iptables, nftables and UFW.
- NetworkPolicy
- A Kubernetes firewall rule set for pods.
- NodePort
- A Kubernetes Service type exposing a port (30000–32767) on every node.
- OSI model
- The 7-layer reference model: Physical, Data link, Network, Transport, Session, Presentation, Application.
- Overlay network
- A virtual network tunnelled over another (e.g. VXLAN between hosts).
- Packet / frame / segment
- The data units at L3 / L2 / L4.
- Peering
- A private, non-transitive connection between two VPCs/VNets.
- Port
- A 16-bit number (0–65535) identifying a program/service on a host.
- PTR record
- A reverse DNS record (IP → name).
- Recursive resolver
- The DNS server that looks up answers on your behalf and caches them (1.1.1.1, 8.8.8.8).
- Reverse proxy
- A server in front of backends that receives client requests for them (Nginx, HAProxy).
- Route table
- The list of destination → next-hop rules; the most specific match wins.
- Security group
- A stateful, allow-only cloud firewall attached to instances/interfaces.
- Service (Kubernetes)
- A stable virtual IP and DNS name that load-balances to pods selected by labels.
- SNI
- Server Name Indication: the hostname sent in the TLS hello so one IP can serve many certificates.
- Socket
- An IP:port endpoint; a connection is identified by its 5-tuple.
- Subnet / subnet mask
- A sub-range of a network; the mask marks the network vs host bits.
- TCP / UDP
- Reliable, connection-oriented transport / fast, connectionless transport.
- TLS
- Transport Layer Security: encryption, integrity and server authentication.
- TTL
- DNS: how long an answer may be cached. IP: the hop limit before a packet is dropped.
- VPC / VNet
- A private, isolated virtual network in the cloud.
- VPN
- An encrypted tunnel connecting networks or a device to a network (WireGuard, IPsec).
- WebSocket
- A long-lived, two-way connection upgraded from HTTP.
- Zero trust
- A security model that verifies every request by identity, device and policy, not network location.