🎓 Neptabolt Free DevOps Course · Networking track

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.

📚 13 modules🧪 Hands-on Ubuntu labs🖼️ 40+ diagrams📝 Quizzes with answers🚑 Real troubleshooting scenarios📄 Cheat sheets
You DNS HTTPS Nginx Docker / K8s Database
★
Start here

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.

🌍
Real life

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:

💡
AnalogyAn everyday comparison that makes the idea click.
🌍
Real lifeHow the idea shows up in actual production work.
🛠️
DevOps tipPractical advice from the field.
⚠️
WarningA common mistake that breaks things or creates a security hole.
ℹ️
Good to knowExtra detail. Skip it on a first read if you like.
🧠
RememberThe one-line idea to memorise.

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

⚙
Before you begin

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)

  1. Install VirtualBox (or VMware Workstation Player / Hyper-V / UTM on Mac).
  2. Download the Ubuntu Server 24.04 LTS ISO (22.04 also works).
  3. 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.
  4. 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.

⚠️
Warning: don't lock yourself out

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
ℹ️
Good to know

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.

🛠️
DevOps tip

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.

🛒
Running case study

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.

YOUR SERVER (VPS · public IP 203.0.113.10) Customer browser DNS ① DNS lookup ② HTTPS · port 443 firewall Nginx :80 / :443 ③ /api → Backend API 127.0.0.1:5000 ④ SQL PostgreSQL private :5432 React files /var/www/shop / →
Figure 0.1 ShopNow's architecture. Only Nginx is public. The backend and the database sit behind it and stay private.

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.

🧠
Remember

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.

01
Module 1

Networking fundamentals

The alphabet of networking: what a network is, how devices are addressed, and how traffic finds its way out of your network.

LAN / WAN / InternetClient-serverIPv4 & IPv6Public vs privateMAC & ARPSubnets & CIDRGateway & routingNAT
🎯 After this module you can:
  • 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.

💡
Analogy: the postal system

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

VPN tunnel across the WAN LAN · HOME WI-FI Laptop Phone Smart TV Router 192.168.1.1 ISP link INTERNET the biggest WAN LAN · OFFICE Router File server HR portal(intranet)
Figure 1.1 Two LANs connected through the Internet. The office HR portal is part of an intranet: it is only reachable from inside the office or through a VPN.
TypeWhat it isReal-life exampleWhy 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.
InternetThe 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.
IntranetA 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, psql and your SSH app.
MANY CLIENTS → ONE SERVER request response Server always on, listening on a port A SERVER CAN ALSO BE A CLIENT client→server client→server Browser client Backend API server AND client PostgreSQL server
Figure 1.2 Left: many clients share one server. Right: the backend is a server to the browser and a client to the database.
🛠️
DevOps tip

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.

ℹ️
Good to know: peer-to-peer

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.

octet 1octet 2octet 3octet 4 192 . 168 . 1 . 10 11000000 10101000 00000001 00001010 Network part (with /24): 192.168.1 → "which street" Host part: .10 → "which house" 4 octets × 8 bits = 32 bits · each octet is 0–255 · about 4.3 billion possible addresses
Figure 1.3 An IPv4 address in decimal and in binary. The subnet mask (section 1.6) decides where the network part ends and the host part begins.

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
IPv4IPv6
Size32 bits128 bits
Format203.0.113.102001:db8::10
Number of addresses~4.3 billion~340 undecillion (3.4 × 10³⁸), effectively unlimited
Loopback (this machine)127.0.0.1::1
Private / local10.x, 172.16–31.x, 192.168.xfd00::/8 (unique local), fe80::/10 (link-local)
NAT needed?Usually yesUsually no: every device can get a global address
DNS recordAAAAA

Special IPv4 addresses every DevOps engineer must know

AddressMeaningWhere 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.0When 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.xLink-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.255Broadcast to everyone on the local network.DHCP discovery.
⚠️
Warning: the #1 beginner deployment bug

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 rangeCIDRAddressesTypically used by
10.0.0.0 – 10.255.255.25510.0.0.0/816.7 millionCloud 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.255172.16.0.0/121 millionDocker (172.17.0.0/16 is the default bridge), AWS default VPCs
192.168.0.0 – 192.168.255.255192.168.0.0/1665,536Home 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.

PRIVATE NETWORK (LAN) Laptop192.168.1.10 Phone192.168.1.11 Smart TV192.168.1.12 Router (NAT) public IP 203.0.113.25 Internet shop.example.com 203.0.113.10 NAT TABLE (inside → outside) 192.168.1.10:51000 → 203.0.113.25:62001 192.168.1.11:49152 → 203.0.113.25:62002
Figure 1.4 Three devices share one public IP. The router's NAT table remembers which reply belongs to which device.
🌍
Real life

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.

ℹ️
Good to know: CGNAT

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):

Laptop 192.168.1.10 3c:22:fb:1a:2b:3c Router 192.168.1.1 aa:bb:cc:dd:ee:01 ① BROADCAST to ff:ff:ff:ff:ff:ff (everyone on the LAN) "Who has 192.168.1.1? Tell 192.168.1.10" ② UNICAST reply, only to the laptop "192.168.1.1 is at aa:bb:cc:dd:ee:01" Phone hears it too, but ignores it (not its IP) The laptop caches the answer. You can see the cache with ip neigh
Figure 1.5 ARP turns a known IP address into the MAC address needed to deliver frames on the local network.
🧠
Remember

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 maskWritten as CIDRMeaning
255.0.0.0/8First 8 bits are network; 24 bits for hosts
255.255.0.0/16First 16 bits are network; 16 bits for hosts
255.255.255.0/24First 24 bits are network; 8 bits for hosts
255.255.255.192/26First 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.

🧠
The only formula you need

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.)

CIDRAddressesUsable hostsTypical use
/3211 (a single host)Firewall rule for exactly one IP, e.g. "allow SSH from 198.51.100.7/32"
/3042Point-to-point router links
/281614A tiny subnet for a few servers
/266462A small department or application tier
/24256254Home network; a typical cloud subnet
/204,0964,094A large cloud subnet (e.g. AWS default subnets) for big EKS clusters
/1665,53665,534A whole VPC; Docker's default bridge
/0all 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:

192.168.10.0/24 → 256 addresses (.0 – .255) split into 4 × /26 Subnet A · Web192.168.10.0/26 Subnet B · App192.168.10.64/26 Subnet C · DB192.168.10.128/26 Subnet D · Spare192.168.10.192/26 network .0 · broadcast .63hosts .1 – .62 network .64 · broadcast .127hosts .65 – .126 network .128 · broadcast .191hosts .129 – .190 network .192 · broadcast .255hosts .193 – .254 Each subnet can now have its own firewall rules, e.g. only Subnet B may reach port 5432 in Subnet C.
Figure 1.6 One /24 split into four /26 subnets, each with its own network address, broadcast address and host range.

Worked example: the "block size" trick

Question: Which subnet does 192.168.1.130/26 belong to, and what is its usable range?

1

Find which octet the prefix falls in. /26 sits in the 4th octet (24 < 26 ≤ 32).

2

Work out the block size: 2(32−26) = 64. So subnets start at .0, .64, .128 and .192.

3

130 falls between 128 and 191, so the subnet is 192.168.1.128/26.

4

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).

🌍
Real life: CIDR shows up everywhere

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".

⚠️
Warning: overlapping CIDRs

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.
Is 8.8.8.8 inside 192.168.1.0/24? No → send it to the default gateway Laptop 192.168.1.10/24 Default gateway 192.168.1.1 ISP router … more hops … 8.8.8.8 Google DNS laptop's routing table: 192.168.1.0/24 → deliver directly | default (0.0.0.0/0) → via 192.168.1.1
Figure 1.7 Anything not in the local subnet goes to the default gateway. Every router along the way repeats the same "where next?" decision.

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).

💡
Analogy

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.

ℹ️
Good to know: how devices get an IP (DHCP)

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.

🧪 Lab 1: Explore your own network⏱ 20 min · 🖥 Ubuntu VM/VPS
Goal: find your machine's IP, subnet, MAC, gateway and public IP, then practise subnet calculations.
1

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.

2

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
3

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.

4

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
5

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.

6

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"

Symptom
curl localhost:5000 on the server returns the page, but http://SERVER-IP:5000 from a laptop times out or is refused.
Cause
The app is bound to 127.0.0.1, or a firewall blocks port 5000.
Fix
Check with 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"

Symptom
The VPN shows "connected", but ping 192.168.1.20 (an office server) gets no reply.
Cause
Your home network also uses 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.
Fix
Use non-overlapping ranges, for example move the office to 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 to 0.0.0.0 = all interfaces.

📝 Quiz: Module 1

1. Which of these is a private IP address?

  1. 8.8.8.8
  2. 172.20.5.4
  3. 172.32.1.1
  4. 203.0.113.9
Show answer
B: 172.20.5.4. The private 172 range is 172.16.0.0 – 172.31.255.255, so 172.32.1.1 is just outside it.

2. How many usable host addresses does a /27 subnet have?

Show answer
2(32−27) = 32 addresses, minus network and broadcast = 30 usable. In AWS it's 27, because AWS reserves 5.

3. What is the network address and broadcast address of 10.0.0.77/28?

Show answer
The block size is 16, so the blocks are .0, .16, .32, .48, .64, .80… 77 falls in the .64 block. Network 10.0.0.64, broadcast 10.0.0.79, usable .65–.78.

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
No. 127.0.0.1 is loopback, so only processes on the same machine can connect. Use 0.0.0.0 to listen on all interfaces.

5. What does ARP do?

  1. Translates domain names to IPs
  2. Finds the MAC address for a known IP on the local network
  3. Assigns IP addresses automatically
  4. Encrypts traffic
Show answer
B. A is DNS, C is DHCP, and D is TLS/VPN.

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
No. 192.168.1.200 is in the same /24, so the laptop delivers it directly using ARP. Only off-subnet destinations go to the gateway.

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
10.1.0.0/16, because it's the longest (most specific) matching prefix.

8. Why doesn't ip addr on an AWS EC2 instance show its public IP?

Show answer
AWS performs 1:1 NAT at its network edge. The instance only has a private IP on its interface, and the public/Elastic IP is mapped to it outside the VM.
02
Module 2

Networking models & protocols

How the work of networking is split into layers, and the protocols (rules of conversation) you will use every single day.

OSI modelTCP/IP modelEncapsulationTCP vs UDP3-way handshakePorts & socketsHTTP/HTTPSSSH · SFTP · ICMP
🎯 After this module you can:
  • 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.

💡
Analogy: sending a parcel

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.

OSI MODEL (7 LAYERS) TCP/IP (4) EXAMPLES · DATA UNIT · DEVICES 7 · Application 6 · Presentation 5 · Session 4 · Transport 3 · Network 2 · Data link 1 · Physical Application Transport Internet Link HTTP, HTTPS, DNS, SSH, SMTP, FTPwhat the application "speaks" · L7 load balancer TLS encryption, JSON, UTF-8, gzipformat & encrypt the data Sessions, keep-alive, WebSocketopen, manage and close conversations TCP, UDP · PORTS · data unit: segmentL4 load balancer, firewall port rules IP, ICMP · data unit: packetrouters, IP addressing, routing tables Ethernet, Wi-Fi, ARP, MAC · frameswitches, network cards Cables, fibre, radio · bits"is the cable plugged in / link up?" DevOps lives mostly in L7 (HTTP, DNS), L4 (TCP/UDP ports) and L3 (IP, routing)
Figure 2.1 The 7 OSI layers mapped to the 4 TCP/IP layers, with the protocols and devices that work at each one.
🧠
Memory trick

From layer 1 up to layer 7: Please Do Not Throw Sausage Pizza Away: Physical, Data link, Network, Transport, Session, Presentation, Application.

🌍
Real life: layers in job language
  • "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 /api and /images to 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.

DataApplication GET /api/products SegmentTransport (L4) TCP header51000 → 443 data PacketNetwork (L3) IP headersrc → dst IP TCP data FrameData link (L2) Ethernet headersrc → dst MAC IP TCP data FCS BitsPhysical (L1) 0101101001110100101011010010110100101100101101001011010 Sender wraps going DOWN ↓ · receiver unwraps going UP ↑ (decapsulation)
Figure 2.2 Each layer adds a header. Routers look at the IP header; switches look at the Ethernet header; only the destination app reads the data.
ℹ️
Good to know: what changes at each hop

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.

TCP · reliable, ordered, connection-based 1 2 3 ACK: "got 1, 2, 3 ✓" ✔ handshake first · ✔ lost data is re-sent · ✔ arrives in order Used by: HTTP/HTTPS, SSH, databases, SMTP, most APIs UDP · fast, fire-and-forget 1 ✗ 3 packet 2 lost → nobody re-sends it ✔ no handshake · ✔ tiny overhead · ✘ no delivery guarantee Used by: DNS, video calls, gaming, streaming, VPN (WireGuard)
Figure 2.3 TCP confirms and re-sends; UDP just sends. A missing video frame is better than a frozen call, which is why real-time apps prefer UDP.
FeatureTCPUDP
ConnectionConnection-oriented (3-way handshake first)Connectionless: just send
ReliabilityGuaranteed delivery, re-transmits lost dataBest effort, no retries
OrderData arrives in orderMay arrive out of order
Speed / overheadSlower, bigger header (20+ bytes)Faster, 8-byte header
Analogy📞 A phone call: "Hello?" "Hi!" … "Bye."📮 Throwing postcards in a mailbox
DevOps examplesWeb (80/443), SSH (22), PostgreSQL (5432), MySQL (3306), Redis (6379)DNS (53), NTP (123), syslog (514), WireGuard (51820), QUIC/HTTP/3 (443/udp)
🛠️
DevOps tip

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.

Client 192.168.1.10:51000 Server 203.0.113.10:443 LISTEN ① SYN · "Can we talk? My sequence number starts at x" SYN_SENT ② SYN-ACK · "Yes! Got x. Mine starts at y" SYN_RECEIVED ③ ACK · "Got y. Let's go!" ESTABLISHED ESTABLISHED ⇄ data flows both ways (TLS handshake, then HTTP request/response) FIN · "I'm done" ACK + FIN · "OK, me too" ACK TIME_WAIT CLOSED
Figure 2.4 Open with SYN, SYN-ACK, ACK; exchange data; close with FIN/ACK. You'll see these states in ss output.

"Connection refused" vs "connection timed out": the most useful clue in networking

✔ Connected ✘ Connection refused ⏳ Connection timed out SYN SYN-ACK A program is listeningand the path is open. SYN RST (reset), instantly The host is reachable, but nothinglistens on that port (or a firewallREJECTs it). Check the service/port. SYN … SYN … SYN silence… then give up Packets are dropped somewhere:firewall / security group DROP,wrong IP, host down, bad route.
Figure 2.5 Learn to read these two errors and you'll know straight away whether to look at the service or at the network path.
🧠
Remember

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.

💡
Analogy

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).

Admin (ssh) Browser Server · 203.0.113.10 :22sshd (SSH) :80nginx (HTTP → redirects) :443nginx (HTTPS) :5432postgres, bound to127.0.0.1 only 🔒 ONE TCP CONNECTION = A UNIQUE 5-TUPLE protocol: TCP src IP: 198.51.100.7 src port: 51000 (random) dst IP: 203.0.113.10 dst port: 443 That's how one port 443 serves thousands of users at once.
Figure 2.6 One IP, many ports, each owned by one listening program. The client's source port is a random "ephemeral" port.
RangeNameNotes
0 – 1023Well-known portsStandard services (22, 80, 443). On Linux, binding to them needs root or CAP_NET_BIND_SERVICE.
1024 – 49151Registered portsApplications: 3000, 3306, 5432, 6379, 8080, 9090…
49152 – 65535Ephemeral / dynamicRandom source ports for outgoing client connections. (Linux actually uses 32768–60999 by default.)

Ports you should know by heart

PortProtocolServicePortProtocolService
22TCPSSH / SFTP / SCP3306TCPMySQL / MariaDB
25 / 587TCPSMTP (mail sending)5432TCPPostgreSQL
53UDP + TCPDNS6379TCPRedis
80TCPHTTP27017TCPMongoDB
443TCP (+UDP for HTTP/3)HTTPS6443TCPKubernetes API server
20 / 21TCPFTP (insecure)3000 / 8080TCPCommon dev/app ports (Grafana uses 3000)
123UDPNTP (time sync)9090 / 9100TCPPrometheus / 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

CodeMeaningWhat it usually means for a DevOps engineer
200 OK / 201 CreatedSuccessAll good.
301 / 302 / 308RedirectHTTP → HTTPS redirects, or www → non-www. Loops here mean misconfigured proxies.
400 Bad RequestMalformed requestClient bug, or a header too large for the proxy.
401 / 403Unauthenticated / ForbiddenA missing/expired token (401) or no permission (403). It can also be a Nginx deny rule or file permissions.
404 Not FoundNo such pathWrong URL, wrong Nginx location/root, or a wrong Ingress path.
413 Payload Too LargeBody too bigRaise client_max_body_size in Nginx for uploads.
500 Internal Server ErrorApp crashedRead the application logs: it's a code or config bug.
502 Bad GatewayProxy got a bad/no response from upstreamThe backend is down, on the wrong port, or crashed. The most common networking 5xx.
503 Service UnavailableTemporarily can't serveNo healthy backends, maintenance, or rate limiting.
504 Gateway TimeoutUpstream too slowThe 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
Your laptop pgAdmin → localhost:5432 encrypted SSH tunnel over port 22 only port 22 is open on the firewall VPS 203.0.113.10 sshd :22 postgres127.0.0.1:5432
Figure 2.7 SSH local port forwarding: reach a database that is not exposed to the Internet, safely through 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
🌍
Real life

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

ToolPortEncrypted?Use it?
FTP21 (control) + 20/random (data)❌ No. Passwords travel in plain text.Avoid. Legacy only (FTPS adds TLS).
SFTP22 (over SSH)✅ Yes✅ Standard for file transfer to servers.
SCP / rsync over SSH22✅ 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
⚠️
Warning: ping is not the whole truth

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.

🧪 Lab 2: Watch TCP, UDP and HTTP in action⏱ 30 min · 🖥 Ubuntu VM (two terminals)
Goal: create your own server and client with nc, see the handshake with tcpdump, and read real HTTP headers.
1

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.

2

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)
3

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 14
4

Produce 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
5

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
6

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"

Symptom
ping shop.example.com shows 100% packet loss, yet the browser loads the site fine.
Cause
ICMP is blocked by the server firewall or cloud security group. HTTP (TCP 443) is allowed.
Fix
Nothing is broken. Test the real service with 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"

Symptom
The backend logs ECONNREFUSED 10.0.11.20:5432 on one day and ETIMEDOUT on another.
Cause
Refused: PostgreSQL is stopped or only listens on 127.0.0.1. Timed out: a firewall or security group blocks 5432, or the IP is wrong.
Fix
Refused → on the DB host run 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 -tulnp shows 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
Layer 4, Transport. It adds source and destination ports (plus sequence numbers and ACKs for TCP).

2. Put the handshake in order: ACK, SYN, SYN-ACK.

Show answer
SYN → SYN-ACK → ACK.

3. Which protocol does DNS mainly use, and why?

  1. TCP, for reliability
  2. UDP, because queries are tiny and need to be fast
  3. ICMP
  4. HTTP
Show answer
B. DNS uses UDP 53 for normal queries and falls back to TCP 53 for large responses and zone transfers.

4. curl http://10.0.0.5:8080 instantly returns "Connection refused". What does this tell you?

Show answer
The host was reached and replied with RST, so nothing is listening on port 8080 (or a firewall is set to REJECT). Check the service, its port and its bind address.

5. Nginx returns 502 Bad Gateway. Which component do you check first?

Show answer
The upstream/backend that Nginx proxies to. Is it running? Is it listening on the port in 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
SFTP, which runs over SSH on port 22 and is encrypted.

7. What does this command do: ssh -N -L 6379:127.0.0.1:6379 deploy@203.0.113.10?

Show answer
It opens an SSH tunnel so that localhost:6379 on your laptop forwards to Redis on 127.0.0.1:6379 on the server, without exposing Redis to the Internet.

8. How can thousands of users connect to the same server port 443 at once?

Show answer
Each connection is identified by a unique 5-tuple (protocol, src IP, src port, dst IP, dst port). Clients use different source IPs and ports, so every connection is distinct.
03
Module 3

DNS & domain management

DNS is the Internet's phonebook. It turns shop.example.com into an IP address. When DNS breaks, everything looks broken.

Domain anatomyResolution flowA · AAAA · CNAME · MX · TXTTTL & cachingPropagationNameserversReverse DNSTroubleshooting
🎯 After this module you can:
  • 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?").

💡
Analogy: your phone's contacts

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

https:// shop. example .com :443 /products?id=7 schemeprotocol subdomainyou create it domain nameyou buy it TLDtop-level portusually hidden path + queryhandled by the app/Nginx only this "hostname" part is looked up in DNS
Figure 3.1 DNS only resolves the hostname. The port and path never touch DNS.

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.

. (root) .org .com .io .pk / .uk ← TLDs example.com google.com ← domains (bought from a registrar) shop.example.com api.example.com ← subdomains (free; you create them)
Figure 3.2 The DNS hierarchy. Each level only knows who is responsible for the level below it.

3.2 How DNS resolution works, step by step

① shop.example.com ? ⑧ 203.0.113.10 (cached) 2 3 4 5 6 7 Your computer checks locally first: • browser DNS cache • /etc/hosts file • OS cache (systemd-resolved) Recursive resolver ISP · 1.1.1.1 · 8.8.8.8 does the legwork + caches Root (.) "ask .com" TLD (.com) "ask ns1.dns-host.net" Authoritative NS has the real record
Figure 3.3 A full DNS lookup. In practice most answers come straight from a cache at step ① or ⑧, so lookups usually take only milliseconds.
  1. 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?"
  2. The resolver (if it has nothing cached) asks a root server.
  3. The root replies: "I don't know, but here are the .com TLD servers."
  4. The resolver asks a .com TLD server.
  5. The TLD server replies: "example.com's authoritative nameservers are ns1.dns-host.net and ns2.dns-host.net."
  6. The resolver asks the authoritative nameserver.
  7. The authoritative server answers: "shop.example.com A 203.0.113.10, TTL 300."
  8. 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.
🧠
Remember

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

RecordPurposeExampleReal-life use
AName → IPv4 addressshop.example.com → 203.0.113.10Point your domain at a VPS or load balancer.
AAAAName → IPv6 addressshop.example.com → 2001:db8::10Same as A, for IPv6.
CNAMEName → another name (an alias)www.example.com → example.com
app.example.com → myapp.herokuapp.com
Point to a cloud service whose IP changes (load balancers, CDNs, SaaS).
MXWhich servers receive email (with priority)example.com MX 10 mail.example.comGoogle Workspace / Microsoft 365 mail setup.
TXTFree 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.
NSWhich nameservers are authoritative for the zoneexample.com NS ns1.dns-host.netDelegating your domain to Cloudflare/Route 53.
SOAZone metadata: primary NS, admin, serial, timersCreated automaticallyRarely edited by hand.
PTRIP → name (reverse DNS)10.113.0.203.in-addr.arpa → mail.example.comMail server reputation; set in your VPS/IP provider's panel.
CAAWhich CAs may issue certificates for the domainexample.com CAA 0 issue "letsencrypt.org"Extra protection against mis-issued certificates.
SRVService location (host + port)_sip._tcp.example.comVoIP, 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"
⚠️
Warning: CNAME rules

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.

Resolver cached the record with TTL 300 (5 min) TTL 3600 (1 hour) TTL 86400 (24 hours) some users still reach the OLD server all day 10:00 record changed 10:0511:00next day 10:00 old IP new IP
Figure 3.4 Worst case: a resolver cached the old answer just before you changed it. The longer the TTL, the longer some users see the old server. (Not to scale.)
🛠️
DevOps tip: the safe server migration recipe
  1. A day before, lower the record's TTL from 3600/86400 to 300.
  2. Wait for the old TTL to pass, so every cache now holds the short TTL.
  3. Switch the A record to the new server. Keep the old server running for a while.
  4. Verify with dig @1.1.1.1 and dig @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.

Registrar Namecheap, GoDaddy, PKNIC… You buy/renew the domain here and choose its nameservers TLD registry (.com) stores the delegation: example.com NS ns1.dns-host.net DNS hosting Cloudflare, Route 53, or the registrar Your A, CNAME, MX, TXT records actually live here ⚠ Editing records at the registrar does nothing if the NS records point to Cloudflare!
Figure 3.5 The registrar tells the TLD registry which nameservers are authoritative. Only those nameservers' records count.

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.

🌍
Real life

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 / toolWhat it does
/etc/hostsStatic name → IP entries, checked before DNS. Great for testing a new server before switching real DNS.
/etc/resolv.confWhich DNS server the system asks. On Ubuntu it points to 127.0.0.53, the local systemd-resolved stub.
resolvectl statusShows the real upstream DNS servers that systemd-resolved uses on each interface.
resolvectl flush-cachesClears the local DNS cache.
/etc/nsswitch.confThe hosts: line sets the lookup order (files = /etc/hosts first, then dns).
🛠️
DevOps tip: test before you switch 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.

🧪 Lab 3: Become fluent with dig⏱ 30 min · 🖥 Ubuntu VM/VPS
Goal: query every record type, follow a lookup from the root, compare resolvers and override DNS locally. Use any real domain (google.com, github.com) or your own.
1

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.)

2

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
3

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)
4

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
5

Reverse lookup, and nslookup (also available on Windows):

$ dig -x 8.8.8.8 +short
dns.google.
$ nslookup github.com 1.1.1.1
6

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
ℹ️
Good to know

dig and nslookup bypass /etc/hosts. getent hosts, ping and curl use it. That difference explains many "but dig says…" confusions.

7

(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 seeLikely causeCheck with
NXDOMAIN / browser: DNS_PROBE_FINISHED_NXDOMAINThe record doesn't exist, there's a typo, records were added at the wrong provider, or the domain expireddig +short NS domain, whois domain, dig @authoritative-ns name
SERVFAILAuthoritative servers are unreachable or broken, or there's a DNSSEC validation failuredig +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.confresolvectl status, dig @1.1.1.1 name
Some users get the old IPCaching, because the TTL hasn't expiredCompare dig @1.1.1.1 vs @8.8.8.8 vs the authoritative server
Works with IP, fails with nameDNS problem on that client onlygetent hosts name, check /etc/hosts, flush the cache

🚑 "I updated the A record two hours ago, but the site still shows the old server"

Symptom
Your colleague sees the new site; you see the old one.
Cause
The record had a TTL of 86400 (24 h), and your ISP's resolver or your OS/browser cached the old answer.
Fix
Confirm the authoritative server has the new IP (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"

Symptom
dig shop.example.com returns NXDOMAIN, even though the record is visible in the registrar's DNS panel.
Cause
The domain's NS records point to Cloudflare (set up months ago), but the records were added in the registrar's panel.
Fix
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"

Symptom
Order-confirmation emails from ShopNow land in customers' spam folders.
Cause
The SPF/DKIM/DMARC TXT records are missing or wrong, or there's no matching PTR for the sending IP.
Fix
Add SPF (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 @resolver and dig -x are your DNS toolkit. dig ignores /etc/hosts.

📝 Quiz: Module 3

1. Which record would you create to point blog.example.com to example.ghost.io?

Show answer
A CNAME record, because the target is a name, not an IP.

2. You want example.com (the apex) to point to an AWS load balancer hostname. Can you use a CNAME?

Show answer
No, a CNAME isn't allowed at the apex. Use an ALIAS record (Route 53 Alias, Cloudflare CNAME flattening).

3. What does a TTL of 300 mean?

Show answer
Resolvers may cache the answer for 300 seconds (5 minutes) before asking again.

4. Which server owns the final answer for shop.example.com?

  1. The root server
  2. The .com TLD server
  3. The authoritative nameserver for example.com
  4. 8.8.8.8
Show answer
C. 8.8.8.8 is a recursive resolver that only caches and forwards answers.

5. ping shop.test works, but dig shop.test returns nothing. Why?

Show answer
The name is in /etc/hosts. ping uses the system resolver (hosts file first), but dig queries DNS servers directly.

6. Which record type proves domain ownership to Google, or holds an SPF policy?

Show answer
TXT.

7. Who sets the PTR (reverse DNS) record for your VPS IP?

Show answer
The owner of the IP address, i.e. your VPS/cloud provider, usually through their control panel. Your domain's DNS host can't set it.

8. Which command shows every step from the root servers down to the final answer?

Show answer
dig +trace name
04
Module 4

Linux 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.

InterfacesNetplanRouting tablesListening portsUFW · nftables · iptablesConfig filesLatency & packet lossCommand toolkit
🎯 After this module you can:
  • 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.

UBUNTU SERVER (one Linux kernel) Internet private LAN / VPC eth0 / ens3203.0.113.10/24 eth1 / ens410.0.1.10/24 lo (loopback)127.0.0.1/8 wg0 (VPN)10.8.0.1/24 docker0 (bridge)172.17.0.1/16 veth…veth… web172.17.0.2 db172.17.0.3
Figure 4.1 A typical server: a public NIC, a private NIC, loopback, a VPN interface, and Docker's bridge with one virtual cable (veth pair) per container.
Name patternWhat it is
loLoopback, 127.0.0.1. Always present.
eth0, ens3, enp0s3, eno1Wired/virtual NICs. Modern Ubuntu uses "predictable" names based on the hardware slot, so the name varies by machine.
wlan0 / wlp2s0Wi-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, tun0VPN 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
⚠️
Warning

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
🛠️
DevOps tip

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

FilePurpose
/etc/netplan/*.yamlInterface IPs, routes and DNS (Ubuntu)
/etc/hostnameThe machine's hostname (change it with hostnamectl set-hostname web-01)
/etc/hostsStatic name → IP mappings
/etc/resolv.confDNS resolver (managed by systemd-resolved on Ubuntu)
/etc/servicesThe 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_configThe 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}]
🧠
Remember

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…MeaningReachable from
0.0.0.0:80 / *:80All IPv4 interfacesAnywhere the firewall allows
[::]:80All IPv6 interfaces (often IPv4 too)Anywhere the firewall allows
127.0.0.1:5432Loopback onlyThis machine only ✅ (perfect for a DB behind an app on the same host)
10.0.1.10:5432One specific interfaceOnly 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?
⚠️
Warning: "address already in use"

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.

INCOMING PACKETS 198.51.100.7 → :443 10.0.1.20 → :5432 198.51.100.7 → :5432 45.33.1.9 → :3306 UFW rules (top → bottom) 1 ALLOW 22/tcp from anywhere 2 ALLOW 80,443/tcp from anywhere 3 ALLOW 5432/tcp from 10.0.1.0/24 4 (established replies allowed) DEFAULT: deny incoming DEFAULT: allow outgoing first matching rule wins; no match → default policy ✔ to nginx (rule 2) ✔ to postgres (rule 3) ✘ dropped: not from app subnet ✘ dropped: no rule
Figure 4.2 A least-privilege firewall: deny everything by default, then allow only what's needed, from only where it's needed.

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
ℹ️
Good to know: stateful firewalls

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.

⚠️
Warning

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.

1 · Interface up?ip -br link 2 · Has an IP?ip -br addr 3 · Gateway reachable?ip route; ping GW 4 · Internet by IP?ping 1.1.1.1 5 · DNS resolves?dig +short name 6 · Port open?nc -vz host 443 7 · App answers?curl -v URL Stop at the first failing step: that's where the problem is. Steps 1–4 = server/network Step 5 = DNS · Steps 6–7 = firewall/app
Figure 4.3 The troubleshooting ladder. Memorise the seven commands in order.
$ 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
🛠️
Reading mtr like a pro

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.

SymptomMost likely causeCommand that proves it
Can't reach anything, not even the gatewayInterface down, wrong IP/subnet, cable/VM networkip -br addr, ip -s link
Gateway OK, Internet IPs failMissing default route, upstream/NAT problemip route, traceroute -n 1.1.1.1
IPs work, names failDNS (resolver unreachable, bad resolv.conf)resolvectl status, dig @1.1.1.1 name
Name resolves, port times outFirewall / security group / service not publicnc -vz host port, sudo ufw status
Slow or intermittentPacket loss, congestion, DNS slowness, overloaded servermtr, curl -w timings (Module 9), ip -s link errors

4.7 The Linux networking command toolkit

CommandPurposeExample
ip addrView IP addressesip -br addr show
ip linkInterface state / MACip -br link
ip routeRouting table / path for an IPip route get 8.8.8.8
ip neighARP / neighbour cacheip neigh show
pingReachability and latency (ICMP)ping -c 4 8.8.8.8
traceroute / mtrPath, plus per-hop loss and latencymtr -rwc 20 google.com
ssListening ports and connectionssudo ss -tulnp
curlTest HTTP/API endpointscurl -I https://example.com
dig / nslookupTroubleshoot DNSdig +short example.com
ncTest TCP/UDP port connectivitync -vz 10.0.0.5 5432
tcpdumpCapture packetssudo tcpdump -i any port 443
ufwManage the firewallsudo ufw status verbose
hostnameHostname / IPshostname -I
nmapScan which ports are open (only on your own hosts!)nmap -Pn -p 1-1000 203.0.113.10
resolvectlLocal DNS settings and cacheresolvectl status
🧪 Lab 4: Lock down a server with a least-privilege firewall⏱ 30 min · 🖥 Ubuntu VM/VPS + your laptop
Goal: run three "services", decide which should be public, enforce that with UFW, and verify from outside.
1

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'
2

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
3

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).

4

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.

5

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"

Symptom
Right after sudo ufw enable, the SSH session freezes, and new connections time out.
Cause
The default policy denies incoming traffic, and no rule allowed port 22, or SSH runs on a custom port such as 2222.
Fix
Log in through the provider's web console, then run 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'"

Symptom
The server can't install packages, yet ping 1.1.1.1 works.
Cause
A DNS problem: there are no nameservers in Netplan or systemd-resolved, or outbound UDP/TCP 53 is blocked.
Fix
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"

Symptom
Page loads take 3–8 s from 7–11 pm, and are fine at other times.
Cause
Possibly an upstream congestion/packet-loss path, or the server's network or CPU is saturated at peak.
Fix
Run 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

  • ip is the modern tool for interfaces, addresses, routes and neighbours (ifconfig/route are legacy).
  • ip addr/route add is temporary; Netplan YAML is permanent. Use netplan try remotely.
  • ss -tulnp shows 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
It automatically rolls back after a timeout unless you confirm, so a broken network config can't lock you out.

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
DNS. IP connectivity is fine, but name resolution fails.

5. mtr shows 60% loss at hop 4 but 0% at hops 5–9 (the destination). Is there a problem?

Show answer
Probably not. Hop 4 is rate-limiting ICMP replies to itself. Real loss would continue through to the destination.

6. What does the "established,related" rule allow?

Show answer
Return traffic for connections that are already open (for example, replies to your server's outgoing requests), so you don't need inbound rules for them.

7. ip route get 10.0.2.15 shows it would leave via the public interface. What is missing?

Show answer
A more specific route for 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).
05
Module 5

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.

Reverse proxyNginx configHeaders & forwardingLoad balancingHealth checksTLS / HTTPSLet's EncryptWebSocketsPorts 80 & 443
🎯 After this module you can:
  • 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.

HTTPS :443 Nginx TLS · routing · LB compression · cache / /api/ /ws/ React static files/var/www/shop (disk) Backend APIhttp://127.0.0.1:5000 Live-chat (WebSocket)http://127.0.0.1:6000
Figure 5.1 One public entry point (ports 80/443) routes to many private services by URL path or hostname.

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)

FORWARD PROXY · works for the CLIENTS Company proxyfilters, caches, hides users any website on the Internet REVERSE PROXY · works for the SERVERS any user Nginx / ALBTLS, routing, hides servers your private backends
Figure 5.2 A forward proxy acts for clients going out; a reverse proxy acts for servers receiving traffic.

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)
⚠️
Warning: the proxy_pass trailing-slash trap

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.

Customer198.51.100.7 HTTPS, src 198.51.100.7 Nginxadds headers HTTP, src 127.0.0.1 Backend receives: TCP peer: 127.0.0.1 ← Nginx Host:shop.example.com X-Real-IP:198.51.100.7 X-Forwarded-For:198.51.100.7 X-Forwarded-Proto:https
Figure 5.3 Forwarding headers tell the backend who the real client is and whether they used HTTPS.
HeaderPurpose
HostWhich site was requested. Needed when one backend serves several domains. Without it, the backend sees "shop_api".
X-Real-IPThe client's IP as Nginx saw it.
X-Forwarded-ForThe chain of client and proxy IPs (client, proxy1, proxy2). Used behind CDNs and load balancers.
X-Forwarded-ProtoThe original scheme (http/https). Apps use it to build correct links and to avoid redirect loops.
Upgrade / ConnectionNeeded to switch a connection to WebSocket (section 5.8).
⚠️
Security note

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.

Load balancer round robin ✗ api-1 10.0.11.21:5000✔ healthy · gets ½ of traffic api-2 10.0.11.22:5000✔ healthy · gets ½ of traffic api-3 10.0.11.23:5000✘ GET /health failed → removed Users never notice that api-3 is down. When it recovers, the health check adds it back automatically.
Figure 5.4 Load balancing plus health checks give you scale and high availability.
AlgorithmHow it choosesGood forNginx directive
Round robinTakes turns: 1, 2, 3, 1, 2, 3…Identical, stateless backends (the default)(default)
WeightedBigger servers get more requestsMixed server sizes, canary releases (e.g. 90/10)server a weight=3;
Least connectionsPicks the backend with the fewest active connectionsRequests of very different lengthsleast_conn;
IP hash / stickyThe same client always goes to the same backendApps 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 vs L7 load balancing

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 /health every 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
🛠️
DevOps tip: designing a good /health endpoint

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.

Browser Nginx :443 (after the TCP handshake) ① ClientHello: TLS versions, ciphers, key share, SNI ② ServerHello + key share + 📜 certificate + Finished ③ Browser verifies the certificate: trusted CA? name matches? not expired? → derive keys ④ Finished 🔐 encrypted HTTP (GET /api/products) CHAIN OF TRUST Root CApre-installed in OS/browser signs Intermediate CAe.g. Let's Encrypt R11 signs Your certificateCN=shop.example.comvalid 90 days · has public key fullchain.pem = your cert + intermediate privkey.pem = secret key (never share!)
Figure 5.5 A simplified TLS 1.3 handshake (one round trip) and the chain of trust the browser checks.

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 typeHow it proves ownershipUse when
HTTP-01The CA fetches http://domain/.well-known/acme-challenge/… on port 80A normal public web server (the most common case)
DNS-01You create a _acme-challenge TXT recordWildcard 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
⚠️
Common certificate failures
  • 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.com but the user opened www.example.com.
  • Incomplete chain: using cert.pem instead of fullchain.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.

HTTP polling: "any news?" every 2 s clientserver "nothing""nothing""new message!" WebSocket: one connection stays open clientserver GET /ws Upgrade: websocket 101 Switching Protocols ⇄ messages both ways, any time for minutes or hours
Figure 5.6 Polling wastes requests. A WebSocket upgrades one connection and keeps it open.
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
}
ℹ️
Keep-alive

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.

🧪 Lab 5: Build a load-balanced reverse proxy, break it, and add HTTPS⏱ 45 min · 🖥 Ubuntu VM/VPS
Goal: put Nginx in front of two backends, watch the load balancing, cause a 502 and a 504 on purpose, then enable HTTPS.
1

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
2

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
3

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
4

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.

5

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"
6

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

Symptom
The homepage loads (static files), but /api/* returns 502.
Cause
The backend crashed, was restarted on another port, or listens on a different address than proxy_pass uses (e.g. the app listens on [::1]:5000 while Nginx calls 127.0.0.1:5000).
Fix
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

Symptom
Short requests work; "Export CSV" always fails at 60 s.
Cause
proxy_read_timeout defaults to 60 s, and the export takes 90 s.
Fix
Raise 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

Symptom
The browser loops between http and https.
Cause
Something in front (Cloudflare in "Flexible" mode, or a cloud LB) talks HTTP to Nginx, and Nginx/the app redirects to HTTPS on every request. Or the app ignores X-Forwarded-Proto.
Fix
Use Cloudflare "Full (strict)" with a real cert on the origin; pass and trust X-Forwarded-Proto; redirect only once, at the edge.

🚑 Live chat disconnects every minute

Symptom
WebSocket connects (101), then drops after ~60 s of silence.
Cause
The default proxy_read_timeout 60s, or a cloud LB's idle timeout.
Fix
Raise the timeout for /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 -t before systemctl reload nginx.
  • Pass Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto to 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
A forward proxy acts for clients going out to the Internet (filtering, hiding users). A reverse proxy acts for servers, receiving traffic on their behalf (TLS, routing, load balancing).

2. Your backend logs show every visitor's IP as 127.0.0.1. What do you fix?

Show answer
Set 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
To redirect users who type http:// and to let Let's Encrypt HTTP-01 challenges succeed during renewal.

5. Which file should ssl_certificate point to: cert.pem or fullchain.pem?

Show answer
fullchain.pem (your cert + the intermediate). Otherwise some clients fail with "unable to verify".

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
504 Gateway Timeout.

8. You need a certificate for *.example.com. Which challenge type is required?

Show answer
DNS-01 (a TXT record). Wildcards can't use HTTP-01.
06
Module 6

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.

Network namespacesbridge · host · none · overlayContainer IPsPort publishingDocker DNSIsolated networksDocker ComposeUFW gotcha
🎯 After this module you can:
  • 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.

💡
Analogy: an office building

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).

Internet DOCKER HOST (Ubuntu) · eth0 203.0.113.10 iptables / nftables (written by Docker) DNAT (incoming) host :8080 → 172.17.0.2:80 MASQUERADE (out) 172.17.0.0/16 → 203.0.113.10 :8080 docker0 bridge172.17.0.1/16 vethveth web 172.17.0.2 :80 api 172.17.0.3 :5000
Figure 6.1 Behind the scenes: published ports are DNAT rules, and outgoing container traffic is NATed (MASQUERADE) to the host's IP.

6.2 Network drivers: bridge, host, none, overlay

bridge (default) virtual switch own IP per container private 172.x network -p to publish ports ✔ most apps host = host's network shares the host's IP and ports directly no NAT, no -p needed ✔ max performance none only "lo" no network at all fully isolated ✔ batch jobs, security overlay host A host B one network spanning many hosts (VXLAN) ✔ Docker Swarm
Figure 6.2 The four drivers you'll meet. Use user-defined bridge networks for almost everything on a single host.
DriverHow to use itNotes
default bridge (bridge)docker run nginxContainers get IPs, but no DNS by name. Legacy: avoid it for multi-container apps.
user-defined bridgedocker network create shopnet
docker run --network shopnet …
✅ Built-in DNS (container/service names), better isolation. Compose creates one automatically.
hostdocker run --network host …No isolation, no port mapping. Useful for monitoring agents or very high-throughput proxies.
nonedocker run --network none …Loopback only.
overlaydocker 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'
⚠️
Never hard-code container IPs

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.

FlagMeaningReachable from
-p 8080:80Host port 8080 (all interfaces) → container port 80🌍 Anyone who can reach the host, even if UFW says deny
-p 127.0.0.1:8080:80Only on the host's loopback🔒 The host itself (e.g. a host Nginx proxying to it) ✅ recommended
-p 10.0.1.10:5432:5432Only on the private interface🏢 The private network
-p 53:53/udpUDP portRemember: TCP and UDP are separate
-PPublish all EXPOSEd ports on random host portsTesting only
EXPOSE 5000 (Dockerfile)Documentation only. It publishes nothing.—
⚠️
Warning: Docker bypasses UFW

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:

  1. Don't publish database or internal ports at all. Let containers talk over a Docker network.
  2. If the host must reach it, bind to loopback: -p 127.0.0.1:5432:5432.
  3. Add a cloud firewall / security group in front of the VPS (it filters before traffic reaches the server).
  4. Advanced: put rules in the DOCKER-USER iptables 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
⚠️
The "localhost inside a container" trap

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:

users :80 / :443 only publishedports NETWORK: frontend NETWORK: backend (internal) nginxports 80,443 api:5000 apion BOTH nets db:5432 dbno ports ✘ nginx → db impossible:no shared network
Figure 6.3 Network segmentation in Compose. Users can reach only Nginx, Nginx only the API, and only the API reaches the database.
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: {}
🌍
Real life: Docker subnet collisions

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).

🧪 Lab 6: Docker networks from scratch to Compose⏱ 45 min · 🖥 Ubuntu + Docker
Goal: prove the default bridge has no DNS, use a user-defined network, publish ports safely, see the NAT rules, and build a segmented Compose stack.
1

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'
2

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>
3

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.)

4

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
5

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
6

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

Symptom
The API works with npm start on a laptop, but crashes in Docker with a DB connection refused error.
Cause
localhost inside the container is the container itself, and no Postgres runs there.
Fix
Use the Compose service name (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"

Symptom
docker compose up fails for the nginx service.
Cause
The host's own Nginx/Apache, or another container, already listens on port 80.
Fix
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"

Symptom
A Postgres/MongoDB/Redis container on a VPS lost its data. UFW was enabled and denied the port.
Cause
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.
Fix
Remove the 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"

Symptom
Right after docker compose up, the server can't reach 172.20.5.10 (an office system).
Cause
Compose created a network in 172.20.0.0/16, which overlaps the office range, so the local route wins.
Fix
Set 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:container publishes a port. EXPOSE doesn't. Bind to 127.0.0.1 when only the host needs access.
  • Published ports bypass UFW. Never publish databases.
  • localhost in a container = that container. Use service names.
  • Segment with multiple networks; use internal: true for 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
The default bridge has no embedded DNS. Put both containers on a user-defined network.

2. What does -p 127.0.0.1:9000:3000 do?

Show answer
It publishes container port 3000 on host port 9000, only on the host's loopback, so only processes on the host (e.g. a host Nginx) can reach it.

3. Does EXPOSE 8080 in a Dockerfile make the app reachable from the Internet?

Show answer
No. It's metadata/documentation. Only -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
Docker's iptables DNAT rules are evaluated before UFW. Don't publish the port (or bind to 127.0.0.1), and use a cloud firewall or DOCKER-USER rules.

6. What does internal: true do on a Compose network?

Show answer
Containers on it can talk to each other but have no external connectivity: no Internet access, and nothing can be published from it.

7. Which network driver lets containers on different hosts share one network in Docker Swarm?

Show answer
overlay (VXLAN).
07
Module 7

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.

Pod networkingServicesClusterIP · NodePort · LoadBalancerIngress & Gateway APICoreDNSNetworkPoliciesCNI pluginsEgress
🎯 After this module you can:
  • 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
ℹ️
Before you start

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.

Node 1 · 192.168.1.21pod CIDR 10.42.0.0/24 Node 2 · 192.168.1.22pod CIDR 10.42.1.0/24 web-7d9f10.42.0.5 api-5c2b10.42.0.6 web-x81k10.42.1.4 db-010.42.1.7 cni0 / flannelroutes pods cni0 / flannelroutes pods VXLAN api → db: 10.42.0.6 → 10.42.1.7 directly, no NAT
Figure 7.1 Each node owns a slice of the pod CIDR (k3s default 10.42.0.0/16). The CNI plugin carries pod-to-pod traffic between nodes, through a tunnel (VXLAN) or plain routing depending on the CNI.
ℹ️
Containers in the same pod

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

LoadBalancer = NodePort + an external load balancer NodePort = ClusterIP + a port on every node ClusterIP = stable IP inside the cluster only Internet user Cloud LB203.0.113.50:80 any nodeNodeIP:30080(30000–32767) Service "web"10.43.12.7:80web.shop.svc :8080:8080:8080 pods with labelapp=web port 80 (Service) → targetPort 8080 (container)
Figure 7.2 The three main Service types build on each other.
TypeReachable fromUse it for
ClusterIP (default)Inside the cluster onlyInternal services: APIs, databases, caches. Most of your Services.
NodePort<any-node-IP>:30000–32767Labs, bare-metal setups, or as the target of an external LB. Rarely exposed directly in production.
LoadBalancerA public (or internal) IP from the cloud providerExposing 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 directlyStatefulSets (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.

Internet LB :443 Ingress / Gateway controller Traefik · NGINX · HAProxy · Envoy · Cilium Rules (from your YAML): shop.example.com / → frontend shop.example.com /api → api grafana.example.com → grafana 🔒 TLS terminated here (cert-manager + Let's Encrypt) svc frontend svc api svc grafana
Figure 7.3 One load balancer and one controller route every HTTP(S) app in the cluster by host and path.
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
IngressGateway API
StatusStable and everywhere, but feature-frozenThe official successor (v1 since 2023); growing fast
ObjectsOne Ingress (+ controller-specific annotations)GatewayClass → Gateway (infra team) → HTTPRoute/GRPCRoute/TCPRoute (app teams)
FeaturesHost/path routing + TLS; anything else via annotationsHeader matching, traffic splitting (canary), cross-namespace routing, built in
⚠️
Ingress needs a controller

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:

NameWorks from
apiPods in the same namespace
api.shopAny namespace (service.namespace)
api.shop.svc.cluster.localAnywhere: the fully qualified name
db-0.db.shop.svc.cluster.localA 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
🛠️
DevOps tip

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.

NAMESPACE: shop · policy = default-deny + explicit allows frontend api db ✔ :5000 ✔ :5432 ✘ frontend → db blocked other ns(e.g. "dev") ✘ blocked
Figure 7.4 Zero-trust inside the cluster: deny by default, then allow only the arrows your architecture needs.
# 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 }
⚠️
Two classic gotchas
  • 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-system CoreDNS 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.

CNIHow it worksNetworkPolicyKnown for
FlannelSimple VXLAN overlay❌ (k3s adds its own controller)Simplicity, k3s default
CalicoBGP routing or VXLAN; iptables or eBPF✅ plus its own extended policiesVery common in self-managed and enterprise clusters
CiliumeBPF in the kernel (can replace kube-proxy)✅ L3–L7 (even HTTP-aware)Performance, observability (Hubble), default on many managed platforms
AWS VPC CNI / Azure CNIPods 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 ExternalName Service so apps use db.shop everywhere.
  • 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.
🧪 Lab 7: Kubernetes networking hands-on with k3s⏱ 60 min · 🖥 Ubuntu VM (2 CPU / 4 GB recommended)
Goal: install a one-node cluster, deploy an app, reach it via ClusterIP, DNS, NodePort and Ingress, then lock it down with a NetworkPolicy.
1

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.

2

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.

3

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
4

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
5

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
6

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"

Symptom
curl api.shop from a pod fails, yet the pods are Running.
Cause
The Service selector doesn't match the pod labels (a typo like app: API), the pods aren't Ready (readiness probe failing), or targetPort is wrong.
Fix
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"

Symptom
The controller answers, but returns 404 page not found.
Cause
The request's 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.
Fix
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"

Symptom
Apps log getaddrinfo EAI_AGAIN / "temporary failure in name resolution".
Cause
A default-deny Egress policy also blocks traffic to CoreDNS on port 53.
Fix
Add an egress rule allowing UDP+TCP 53 to pods labelled 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
Pods are ephemeral, and their IPs change when they're rescheduled. Use the Service name, which is stable and load-balanced.

2. Which Service type gives an internal-only virtual IP?

Show answer
ClusterIP (the default).

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
30000–32767.

5. You created an Ingress, but nothing happens at all. What's most likely missing?

Show answer
An Ingress controller (or the correct ingressClassName for the installed one).

6. With no NetworkPolicies, can a pod in dev reach the database pod in prod?

Show answer
Yes. Kubernetes allows all pod-to-pod traffic by default. Add default-deny policies (and ideally separate clusters for prod).

7. Name two CNI plugins that enforce NetworkPolicies.

Show answer
Calico and Cilium (also cloud CNIs with policy add-ons, and k3s's built-in controller).
08
Module 8

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.

VPC & subnetsRoute tablesInternet gatewayNAT gatewaySecurity groups vs NACLsVPN & private linksHigh availabilityPrivate databases
🎯 After this module you can:
  • 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
ℹ️
Generic concepts, AWS names

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.

💡
Analogy: a gated housing society

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.

Internet REGION (e.g. eu-central-1) VPC 10.0.0.0/16 Internet Gateway AVAILABILITY ZONE A AVAILABILITY ZONE B 🌐 Public subnet 10.0.1.0/24 🌐 Public subnet 10.0.2.0/24 NAT GW A NAT GW B Application Load Balancer (nodes in both AZs) · :443 🔒 Private app subnet 10.0.11.0/24 🔒 Private app subnet 10.0.12.0/24 app-1 app-2 app-3 app-4 updates 💾 Private data subnet 10.0.21.0/24 💾 Private data subnet 10.0.22.0/24 DB primary:5432 DB standby(replica) replication
Figure 8.1 A production-grade "3-tier" VPC across two AZs. Only the load balancer is public. App servers reach the Internet outbound through NAT gateways. Databases are fully private.
🛠️
DevOps tip: choosing CIDRs
  • Give each VPC a unique, non-overlapping range (prod 10.0.0.0/16, staging 10.1.0.0/16, office 10.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/16local (inside the VPC)
0.0.0.0/0igw-… (Internet Gateway)
Private app route table (AZ A)
10.0.0.0/16local
0.0.0.0/0nat-… (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.

🌍
Real life: NAT costs and egress IPs
  • 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

client Network ACL (subnet boundary) stateless · numbered rules · allow AND deny · in + out Security group (instance / ENI) stateful · allow rules only app-110.0.11.25 ① NACL checks the subnet edge ② SG checks at the instance both must allow it
Figure 8.2 Two layers of cloud firewall. Most teams rely on security groups and leave NACLs at their default (allow all).
Security group (SG)Network ACL (NACL)
Applies toEach instance / network interface / load balancerA whole subnet
StateStateful: replies are allowed automaticallyStateless: you must allow return traffic (ephemeral ports 1024–65535) explicitly
RulesAllow only (implicit deny)Allow and deny, evaluated in number order
SourcesCIDRs or other security groups ✨CIDRs only
Typical useMain, everyday firewallCoarse 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 groupInbound ruleSource
sg-albTCP 443, TCP 800.0.0.0/0 (the Internet)
sg-appTCP 5000sg-alb (only the load balancer)
sg-dbTCP 5432sg-app (only app servers)
sg-appTCP 22No rule! Use SSM Session Manager or a bastion/VPN instead
🧠
Remember

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

Transithub VPC prod10.0.0.0/16 VPC staging10.1.0.0/16 Head office10.50.0.0/16 On-prem data centre10.60.0.0/16 attachment attachment site-to-site VPN(IPsec) dedicated link(Direct Connect) remote engineer → client VPN (WireGuard)
Figure 8.3 Private connectivity options. With only two VPCs, a simple peering link is enough. With many networks, a transit hub avoids a tangle of point-to-point links.
OptionConnectsNotes
VPC peeringVPC ↔ VPCSimple and cheap. Not transitive (A–B and B–C doesn't mean A–C). CIDRs must not overlap.
Transit gateway / hubMany VPCs + VPNsHub-and-spoke with central route tables.
Site-to-site VPNOffice/DC ↔ cloudEncrypted IPsec tunnel over the Internet. Quick to set up.
Client VPNA person's laptop ↔ cloudWireGuard, OpenVPN, Tailscale, or the cloud's client VPN. Gives engineers access to private resources.
Dedicated linkDC ↔ cloud over a private circuitPredictable latency and high bandwidth. Takes weeks to provision.
Private endpoints / PrivateLinkVPC ↔ a cloud service or SaaSReach 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 conceptAWSAzureGoogle Cloud
Private networkVPC (regional)Virtual Network (VNet)VPC network (global)
SubnetSubnet (one AZ)Subnet (regional)Subnet (regional)
Route tableRoute tableRoute table (UDR)Routes
Internet accessInternet GatewaySystem route to InternetDefault Internet gateway route
Outbound NATNAT GatewayNAT GatewayCloud NAT
Instance-level firewallSecurity GroupNetwork Security Group (NIC)VPC firewall rules (by tag / service account)
Subnet-level firewallNetwork ACLNSG on subnetFirewall rules / hierarchical policies
L7 load balancerApplication LB (ALB)Application Gateway / Front DoorApplication Load Balancer (HTTP(S))
L4 load balancerNetwork LB (NLB)Azure Load BalancerNetwork Load Balancer
Network-to-networkVPC Peering / Transit GatewayVNet Peering / Virtual WANVPC Peering / Network Connectivity Center
Site-to-site VPNSite-to-Site VPNVPN GatewayCloud VPN
Dedicated linkDirect ConnectExpressRouteCloud Interconnect
Private service accessVPC Endpoints / PrivateLinkPrivate Endpoint / Private LinkPrivate Service Connect
Managed DNSRoute 53Azure DNSCloud DNS
Keyless admin accessSSM Session ManagerAzure BastionIAP TCP forwarding
🧪 Lab 8: Design ShopNow's cloud network (+ optional build)⏱ 45–90 min · 📝 paper + optional AWS free tier
Goal: produce a complete network design, then (optionally) build its skeleton in a cloud console.
1

Design on paper. Fill in this table for ShopNow in one region with 2 AZs (use Figure 8.1 as a guide):

ItemYour 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…
2

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?

3

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.

⚠️
Cost warning

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.

4

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"

Symptom
Commands hang and time out from instances in a private subnet.
Cause
The private route table has no 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.
Fix
Check the subnet's route table, the NAT gateway's subnet (and that subnet's IGW route), and the outbound rules. Consider VPC endpoints for S3/ECR.

🚑 "Load balancer says targets are unhealthy → users get 502/503"

Symptom
The ALB target group shows every instance as unhealthy, while the app runs fine on each instance.
Cause
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.
Fix
Allow the port from the ALB's SG, point the health check at /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"

Symptom
Browser timeouts; the instance itself works.
Cause
The subnet's route table has no IGW route (it isn't really public), or a custom NACL allows inbound 443 but not outbound ephemeral ports for the replies (NACLs are stateless!).
Fix
Add 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"

Symptom
The peering connection is "Active", yet ping and nc between the VPCs time out.
Cause
Routes weren't added on both sides, SGs don't allow the other VPC's CIDR, or the CIDRs overlap (in which case peering can't even be created).
Fix
Add a route for the peer CIDR → 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
Its route table has 0.0.0.0/0 → Internet Gateway. Instances also need public IPs to be reachable.

2. Where must a NAT gateway be placed, and what route do private subnets need?

Show answer
In a public subnet. Private route tables need 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
No. NACLs are stateless, so the replies (to the client's ephemeral ports) are blocked outbound. Security groups wouldn't have this problem.

4. Write the inbound rule for the DB security group in a 3-tier app.

Show answer
TCP 5432 (or 3306) with source = the app tier's security group, and nothing else.

5. VPC A is peered with B, and B with C. Can A reach C?

Show answer
No. Peering isn't transitive. Use a transit gateway/hub, or peer A–C directly.

6. A payment provider must allowlist your servers' IP. Your app servers are private behind NAT. Which IP do you give them?

Show answer
The NAT gateway's public (Elastic) IP, one per AZ if you run several.

7. Name the Azure and GCP equivalents of an AWS Security Group.

Show answer
Azure: Network Security Group (NSG). GCP: VPC firewall rules.
09
Module 9

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.

Least privilegeTLS everywhereSSH keys & hardeningSegmentationVPN & zero trusttcpdump & WiresharkLatency · traffic · errors
🎯 After this module you can:
  • 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 portServiceWho should reach itHow it's enforced
443, 80Nginx / load balancer🌍 EveryoneSG/UFW allow 0.0.0.0/0
22SSH👩‍💻 Admins onlyAllow from office/VPN IP only, keys only, or no SSH at all (SSM)
5000Backend API⚙️ Nginx / LB onlyBind to 127.0.0.1, or SG source = LB SG
5432PostgreSQL🔒 Backend onlyPrivate subnet / internal Docker network, SG source = app SG
9100, 3000node_exporter, Grafana📊 Monitoring server / VPNAllow 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
EDGE NETWORK HOST APPLICATION DATA Edge:CDN, DDoS protection, WAF, rate limits Network:VPC, subnets, SGs, NACLs, VPN Host:UFW, SSH keys, patches, fail2ban App:TLS, auth, input validation, secrets Data:encryption at rest, backups, access If one layer fails,the next one still protects you.
Figure 9.1 Defence in depth: never rely on a single control.

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-full for 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

Your laptop 🔑 ~/.ssh/id_ed25519private key: never leavesthis machine; protect itwith a passphrase Server 📄 ~/.ssh/authorized_keyscontains your PUBLIC key(safe to share, like apadlock anyone can see) ① "I'm deploy, here's my public key ID" ② "Prove it: sign this random challenge" ③ signature made with the private key ④ verified with public key → ✔ logged in
Figure 9.2 Key-based SSH: no password ever crosses the network, and stolen server files can't be used to log in elsewhere.
$ 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
⚠️
Warning

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

🏰 Traditional: castle & moat VPN in inside = trusted → can reach everything 🛡️ Zero trust: verify every request Access proxy ✔ who? (SSO+MFA)✔ device healthy?✔ allowed app? Grafana ✔ prod DB ✘ access per app, per user, every time
Figure 9.3 Perimeter security trusts everything inside the network. Zero trust trusts no network location; identity and policy decide each access.

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.

GoalCommand
All DNS trafficsudo tcpdump -i any -n port 53
Backend ↔ databasesudo 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 resetssudo tcpdump -i any -n 'tcp[tcpflags] & tcp-rst != 0'
Readable HTTP (plain text only)sudo tcpdump -i any -nn -A port 80
Container trafficsudo tcpdump -i docker0 -n (or a br-… interface)
Save for Wiresharksudo 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
ℹ️
Good to know

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
DNS lookup20 ms TCP connect35 ms TLS handshake60 ms Server processing180 ms ← waiting for the backend/DB Download 0 ms115 ms310 ms
Figure 9.4 A request waterfall. Here, network setup is fast and most of the time is spent in the application, so the fix belongs there, not in the network.
Slow partPoints to
DNS (> 100 ms)Slow or distant resolver, no caching, ndots issues in Kubernetes
TCP connectNetwork distance (choose a closer region/CDN), packet loss, overloaded LB
TLSExtra 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
DownloadLarge 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
🧪 Lab 9: Harden, observe and measure⏱ 45 min · 🖥 Ubuntu VM (with console access!) + laptop
Goal: switch SSH to key-only, watch brute-force attempts, capture DNS and HTTP packets, and break down request timing.
1

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 ✔
2

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
3

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
4

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
5

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
6

(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"

Symptom
auth.log fills with Invalid user admin from 45.x.x.x.
Cause
Normal Internet background noise: bots scan every public IP on port 22.
Fix
Use key-only auth and fail2ban, and restrict port 22 to your VPN/office IP (UFW or SG). Better still, use SSM or Tailscale SSH with no public port 22 at all.

🚑 "Random requests take exactly 1, 3 or 7 seconds longer"

Symptom
Most API calls take 50 ms, but some take +1 s or +3 s.
Cause
Those numbers are TCP retransmission/SYN-retry timers, a sign of packet loss (an overloaded NIC, a conntrack table full, a flaky link) or DNS timeouts falling back to the second resolver.
Fix
Check 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"

Symptom
Browsers block the site with a certificate-expired error.
Cause
Auto-renewal had been failing for weeks: port 80 was closed in a firewall change, or DNS moved to a CDN.
Fix
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 ps and an external nmap.
  • 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.
  • tcpdump shows the truth on the wire; Wireshark helps you analyse it.
  • Monitor latency (p95), traffic, errors and saturation. curl -w shows 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
No user or device is trusted because of its network location: every access request is authenticated, authorised and checked against policy.

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 5432

4. curl -w shows dns 0.01, connect 0.02, tls 0.05, ttfb 2.40. Where is the problem?

Show answer
In the server/application (TTFB): the network and TLS are fast, and the backend takes ~2.3 s to respond.

5. Can tcpdump show the JSON body of an HTTPS API request?

Show answer
No. It's encrypted by TLS. You see IPs, ports, timing, the handshake and the SNI hostname only.

6. Name the four golden signals.

Show answer
Latency, traffic, errors, saturation.

7. Which UDP port does WireGuard use by default?

Show answer
51820/udp.
10
Module 10 · Putting it together

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.

Browser192.168.1.10 DNS→ 203.0.113.10 RouterNAT Internet FirewallSG / UFW Nginx / LB:443 Backend:5000 PostgreSQLprivate :5432 ⑪ response travels back: Nginx → TLS → Internet → NAT reversed → browser 1 2 3 4 5 6 7 8 9 10 12
Figure 10.1 The full round trip. Each number matches a row in the table below.
#What happensConcepts (module)If it breaks, you see…Check with
1The user enters https://shop.example.com. The browser parses scheme, host, port (443 implied) and path.URL anatomy (3)——
2The 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 IPdig, dig +trace
3203.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/lossip route, mtr
4The cloud firewall/security group and UFW check: TCP 443 from anywhere → allow.Firewalls, SG (4, 8)Timeoutnc -vz host 443
5TCP handshake SYN → SYN-ACK → ACK with Nginx/LB on port 443.TCP, ports (2)Connection refused (nothing listening)ss -tlnp
6TLS 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 mismatchopenssl s_client, curl -v
7Encrypted GET /api/products arrives. Nginx matches server_name and location /api/.HTTP, reverse proxy (2, 5)404, redirect loopsNginx access/error logs
8Nginx 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
9The 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 / ETIMEDOUTgetent hosts db, nc -vz db 5432
10PostgreSQL runs the query and returns rows to the backend, which builds JSON.(application)Slow TTFBcurl -w, DB slow-query log
11The response flows back: backend → Nginx (compression, headers) → TLS encryption → Internet → the router reverses NAT → browser.Encapsulation, NAT (1, 2)Partial loads, resetstcpdump
12The 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.

11
Module 11 · Putting it together

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.

🚨 "The site is not working" Q1 · Does the name resolve to the right IP?dig +short shop.example.com NO DNS: missing record, wrong NS, expireddomain, TTL caching → Module 3 YES Q2 · Can you connect to port 443?nc -vz -w5 shop.example.com 443 TIMEOUT REFUSED Firewall / SG / NACL / route / server down→ Modules 4, 8 Nginx/LB not running or not on 443systemctl status nginx · ss -tlnp → Module 5 CONNECTS Q3 · Is the TLS certificate valid?curl -vI https://shop.example.com NO Expired, name mismatch, incomplete chaincertbot renew · fullchain.pem → Module 5 YES Q4 · What HTTP status comes back?curl -s -o /dev/null -w '%{http_code}' URL 502upstream down / wrong portnginx error.log 504upstream slow / blockedcurl -w · timeouts 4xxpath / auth / proxy configlocation, Ingress rules 500 / 200-but-wrongapplication / DB issueapp logs · DB connectivity
Figure 11.1 The "site down" decision tree. Run it from outside your network (your laptop or phone hotspot) and again from the server.

Ten real incidents

1 · 🌐 "The new domain doesn't open"

Symptom
Browser: "This site can't be reached / NXDOMAIN".
Cause
An A record was added at the registrar, but the NS records point to Cloudflare.
Fix
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"

Symptom
Static pages load; every /api call returns 502.
Cause
The new release reads PORT=8000 from a changed env file; Nginx still proxies to 5000.
Fix
Error log shows "connect() failed (111)" → ss -tlnp shows :8000 → align the env and proxy_pass → add a post-deploy health check.

3 · ⏳ "504 on checkout only"

Symptom
Most pages are fast; checkout gives a 504 after 60 s.
Cause
The backend calls the payment API, but a new egress firewall rule silently drops outbound 443 to the provider's IP range.
Fix
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"

Symptom
ETIMEDOUT 10.0.21.15:5432 after moving the DB to a new subnet.
Cause
The new DB's security group allows 5432 from the old app subnet CIDR.
Fix
Change the rule source to the app security group (not a CIDR), so future moves don't break it. Verify with nc -vz 10.0.21.15 5432 from an app host.

5 · 🐳 "Container can't reach another container"

Symptom
getaddrinfo ENOTFOUND redis in the worker container.
Cause
The worker was started with plain docker run on the default bridge, outside the Compose network.
Fix
Add the worker to compose.yaml (or --network project_default). Check with docker network inspect.

6 · 🏠 "Works internally but not from the Internet"

Symptom
curl from another VPC server works; external users time out.
Cause
The instance is in a subnet whose route table has no 0.0.0.0/0 → IGW (it was launched in the "private" subnet by mistake).
Fix
Put it behind the public ALB (better) or in a public subnet. Never just open everything in the SG.

7 · 🔐 "Mobile app fails, browsers work"

Symptom
Android/iOS clients report SSL handshake errors; desktop browsers are fine.
Cause
Nginx points at cert.pem instead of fullchain.pem. Browsers fetch the missing intermediate themselves; apps don't.
Fix
Use fullchain.pem. Check with openssl s_client -showcerts or SSL Labs.

8 · ☸️ "Kubernetes service times out after a label refactor"

Symptom
Pods are Running, but curl api.shop hangs.
Cause
Deployment labels changed from app: api to app.kubernetes.io/name: api; the Service selector wasn't updated, so there are no endpoints.
Fix
kubectl get endpointslices is empty → align the selector → endpoints appear.

9 · 🐢 "Site slow for users in another continent"

Symptom
Local users see 200 ms loads; overseas users see 2 s.
Cause
~250 ms RTT × several round trips (DNS, TCP, TLS, many asset requests).
Fix
Use a CDN for static assets, HTTP/2 or HTTP/3, TLS 1.3 and keep-alive, cache headers, and possibly a second region. Measure with curl -w from a remote VPS.

10 · 🔥 "After 'tightening' the firewall, DNS broke everywhere"

Symptom
apt, curl and app API calls fail with name-resolution errors; IP-based tests work.
Cause
The new egress policy allowed TCP 443 only, blocking UDP/TCP 53 to the resolver.
Fix
Allow outbound 53/udp+tcp to the resolver only. Then document the egress rule set.

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.
12
Module 12 · Reference

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/tcpSSH, SFTP, SCP
53/udp+tcpDNS
80/tcpHTTP (redirect + ACME)
443/tcp (+udp)HTTPS (HTTP/3 = UDP)
25 · 587 · 993SMTP · submission · IMAPS
123/udpNTP
3306 · 5432MySQL · PostgreSQL
6379 · 27017Redis · MongoDB
6443 · 10250Kubernetes API · kubelet
30000–32767Kubernetes NodePorts
9090 · 9100 · 3000Prometheus · node_exporter · Grafana
51820/udpWireGuard
4789/udpVXLAN (overlays)

🌐 DNS records

A / AAAAname → IPv4 / IPv6
CNAMEname → another name (not at apex)
ALIAS/ANAMECNAME-like at the apex (provider feature)
MXmail servers + priority
TXTSPF, DKIM, DMARC, verification
NSauthoritative nameservers
SOAzone metadata
PTRIP → name (set by the IP owner)
CAAwhich CAs may issue certs
SRVservice host + port
dig +short xquick answer
dig +trace xroot → TLD → authoritative
dig @1.1.1.1 xask a specific resolver
dig -x IPreverse lookup

🐧 Linux network commands by task

My IPsip -br addr · hostname -I
Public IPcurl ifconfig.me
Interfacesip -br link · ip -s link
Routesip route · ip route get IP
ARPip neigh
Listeningsudo ss -tulnp
Connectionsss -tnp state established
Reachabilityping -c4 IP · mtr -rwc20 host
Port testnc -vz -w5 host port
HTTP testcurl -vI https://host
Timingcurl -w '%{time_total}'
DNS configresolvectl status
Capturesudo tcpdump -i any -n port 443
Scan (own hosts)nmap -Pn host

🧮 CIDR quick table

/321 address · single host
/304 · 2 usable
/298 · 6 usable
/2816 · 14 usable · mask .240
/2732 · 30 usable · mask .224
/2664 · 62 usable · mask .192
/25128 · 126 usable · mask .128
/24256 · 254 usable · 255.255.255.0
/204,096
/1665,536 · 255.255.0.0
/816.7 M · 255.0.0.0
Private10/8 · 172.16/12 · 192.168/16
Special127/8 loopback · 169.254/16 link-local · 100.64/10 CGNAT
Formula2(32−n) addresses; usable −2 (cloud −5)

🚦 HTTP status codes

200 / 201 / 204OK / Created / No Content
301 / 308Permanent redirect
302 / 307Temporary redirect
304Not modified (cache)
400Bad request
401 / 403Not authenticated / Forbidden
404Not found (path/location/Ingress)
413Body too large (client_max_body_size)
429Rate limited
500App error → app logs
502Upstream down / refused → backend
503No healthy upstream / maintenance
504Upstream timeout → slow backend / blocked

🧱 UFW

Defaultsufw default deny incoming
SSH first!ufw allow OpenSSH
Webufw allow 80,443/tcp
From subnetufw allow from 10.0.1.0/24 to any port 5432 proto tcp
Rate limitufw limit OpenSSH
Enableufw enable
Statusufw status numbered
Deleteufw delete 3
Logsufw logging on · /var/log/ufw.log
⚠ DockerPublished ports bypass UFW

🐳 Docker networking

Listdocker network ls
Createdocker network create appnet
Internaldocker network create --internal db
Run on netdocker run --network appnet …
Attachdocker network connect appnet c1
Publish-p 8080:80 · -p 127.0.0.1:8080:80
Ports of c1docker port c1
IP of c1docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' c1
Inspect netdocker network inspect appnet
Debug shelldocker run --rm -it --network appnet nicolaka/netshoot
Rememberlocalhost = the container itself · use service names

☸️ Kubernetes networking

Pod IPskubectl get pods -o wide
Serviceskubectl get svc -A
Endpointskubectl get endpointslices -n ns
Exposekubectl expose deploy web --port 80
Ingresskubectl get ingress -A · describe
Policieskubectl get networkpolicy -A
Debug podkubectl run t --rm -it --image=nicolaka/netshoot -- bash
Port-forwardkubectl port-forward svc/api 8080:80
DNS namesvc.ns.svc.cluster.local
CoreDNS logskubectl -n kube-system logs -l k8s-app=kube-dns

🟩 Nginx essentials

Test configsudo nginx -t
Reloadsudo systemctl reload nginx
Logs/var/log/nginx/{access,error}.log
Proxyproxy_pass http://127.0.0.1:5000;
HeadersHost, X-Real-IP, X-Forwarded-For/Proto
WebSocketproxy_http_version 1.1 + Upgrade/Connection
Timeoutsproxy_connect_timeout · proxy_read_timeout
Uploadsclient_max_body_size 10m;
LBupstream {…} + least_conn / ip_hash
HTTPScertbot --nginx -d host

☁️ Cloud networking rules of thumb

VPC CIDR/16, unique per environment
Public subnetroute 0.0.0.0/0 → IGW
Private subnetroute 0.0.0.0/0 → NAT GW
Data subnetlocal route only
SGstateful, allow-only, reference other SGs
NACLstateless, allow+deny, ephemeral ports!
HA≥ 2 AZs, NAT per AZ, LB health checks
AdminSSM / bastion / VPN, never 0.0.0.0/0:22
Peeringnot transitive, no overlapping CIDRs

🔍 Error → meaning

Connection refusedreached host, nothing listening on port
Timed outdropped: firewall/SG/route/host down
No route to hostrouting/ARP failure or REJECT rule
NXDOMAINname doesn't exist in DNS
SERVFAILDNS server failure / DNSSEC
EAI_AGAINresolver unreachable (temporary)
Address in useport already taken: ss -tlnp
Cert expired / mismatchrenewal failed / wrong hostname
502 / 504upstream down / upstream slow

🧭 Troubleshooting order

1 Linkip -br link
2 IPip -br addr
3 Gatewayip route · ping GW
4 Internetping 1.1.1.1 · mtr
5 DNSdig +short name
6 Portnc -vz host port
7 TLScurl -vI https://…
8 Appstatus code + logs
9 Upstreamcurl 127.0.0.1:PORT/health
10 Wiretcpdump
13
Module 13 · What next

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

1FundamentalsIP, subnets, TCP/UDP 2DNS + HTTP/HTTPSTLS, status codes 3Linux networkingip, ss, dig, ufw 4VPS deploymentSSH, Nginx, HTTPS 5Docker networkingCompose, networks 6Cloud networkingVPC, NAT, SGs, LBs 7Kubernetes networkingServices, Ingress, policies 8Monitor & troubleshoottcpdump, alerts
Figure 13.1 Learn the fundamentals, then build on a real VPS. Practise Docker networking before moving to Kubernetes.
🛠️
Recommendation

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.

ℹ️
Optional certifications

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/prefix notation 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.

Networking for DevOps · Neptabolt Free DevOps Course · Edition October 2026

All diagrams are original illustrations. Example addresses use the reserved documentation ranges (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32) and example.com domains.
Sample command output is illustrative; your IPs, names and timings will differ.