In Module 3 you learned to look at the network: ip a, ping, dig, ss -tulpn, curl -v and a layered methodology for working out why something does not answer. That was diagnosis, and diagnosis is reading. This lesson takes the next and considerably more committing step: writing. You are going to decide the address of srv-tramontana, its gateway, its DNS servers and its routes, and you are going to leave it written down in a way that survives a reboot.
Committing is exactly the right word: configuring the network of a machine you reach over the network is the one administration task where a one-letter mistake locks you out instantly and without warning. That is why, on top of netplan's syntax, this lesson teaches you the procedure: validate before applying, have an automatic rollback and never let go of the VM console. srv-tramontana is still running the configuration the installer left — pure DHCP — so its IP depends on a server you do not control. A bookings application whose service name points at 10.0.2.15 cannot afford that address changing on a Tuesday night.
Contents
- An operational refresher: IPv4, netmask, CIDR and private networks
- IPv6 today: link-local, SLAAC and why you do not disable it
- Ephemeral configuration with
ipand why it is lost - Netplan: the renderer model in Ubuntu 24.04
- Netplan's YAML, field by field
netplan tryand mode 600- Name resolution:
systemd-resolvedand the 127.0.0.53 stub - The machine's name:
hostnamectland/etc/hosts - Interfaces: predictable names, aliases, VLANs, bonding and bridges
- Routes, metrics and
ip route get - Packet forwarding and NAT as concepts
- The Tramontana case: from DHCP to a static IP, verified
- An operational refresher: IPv4, netmask, CIDR and private networks
An IPv4 address is 32 bits in four octets. The netmask splits them in two: the network part (bits set to 1) and the host part (bits set to 0). CIDR notation writes the mask as the number of bits set to 1.
| CIDR | Netmask | Usable hosts | Typical use |
|---|---|---|---|
/32 |
255.255.255.255 | 1 | One specific IP (firewall rules) |
/30 |
255.255.255.252 | 2 | A point-to-point link between routers |
/24 |
255.255.255.0 | 254 | The classic LAN: your lab's |
/16 |
255.255.0.0 | 65,534 | A large network or a container range |
Two addresses in every network cannot be assigned: the network address (all host bits at 0, 10.0.2.0) and the broadcast address (all at 1, 10.0.2.255). Hence the "−2".
The private networks of RFC 1918 are not routed on the Internet: 10.0.0.0/8 (companies, VirtualBox NAT, clouds), 172.16.0.0/12 (Docker uses 172.17.0.0/16) and 192.168.0.0/16 (home routers). The gateway is the router's address inside your own network, the one everything non-local is sent to: srv-tramontana sits at 10.0.2.15/24, talks directly to any 10.0.2.x and hands the rest over to 10.0.2.2.
For external IPs we always use RFC 5737's documentation ranges: 192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24. Never use somebody else's real addresses in examples or in tests.
- IPv6 today: link-local, SLAAC and why you do not disable it
IPv6 stopped being "the future" more than a decade ago, and Ubuntu 24.04 ships it enabled.
- Every operational interface has a link-local address in
fe80::/10. It is not routed off the segment, but neighbour discovery (NDP, the equivalent of ARP) and router advertisements use it. - SLAAC (stateless autoconfiguration) lets a host build its own global address from the prefix the router advertises, with no DHCP. That is the norm in IPv6; DHCPv6 exists and coexists with it.
$ ip -6 addr show dev enp0s3
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
inet6 fe80::a00:27ff:fe4b:1c93/64 scope linkThere is only a link-local address because VirtualBox's NAT network advertises no IPv6 prefix. Disabling IPv6 "just in case" is a bad idea, even though half the Internet recommends it: you do not reduce the attack surface, you reduce it badly. If a service listens on :: and you half-disable IPv6, you end up with firewall rules that do not cover a path which still exists. On top of that you break software that assumes ::1 — systemd-resolved included — with timeouts that are hard to diagnose. The correct stance is the same one as with IPv4: if it is enabled, protect it; that is why the firewall in 06-03 will use table inet, which covers both families. And if you genuinely do not need it on a segment, disable it explicitly in netplan (accept-ra: false, link-local: []), not with a half-finished patch.
- Ephemeral configuration with
ip and why it is lost
ip and why it is lostThe ip command does not only read: it also writes. And what it writes lives only in the kernel, in memory.
$ sudo ip addr add 10.0.2.99/24 dev enp0s3
$ ip -brief addr show dev enp0s3
enp0s3 UP 10.0.2.15/24 10.0.2.99/24 fe80::a00:27ff:fe4b:1c93/64
$ sudo ip route add 198.51.100.0/24 via 10.0.2.2 dev enp0s3
$ sudo ip link set enp0s3 mtu 1400
$ sudo ip addr del 10.0.2.99/24 dev enp0s3Notice that ip addr add adds, it does not replace: an interface can hold several addresses at once. And all of this disappears on reboot, and also as soon as systemd-networkd reconfigures the interface after a netplan apply. No file is involved: the kernel's network state is volatile by design. That makes ip the perfect tool for testing a hypothesis and the worst possible one for configuring a server.
- Netplan: the renderer model in Ubuntu 24.04
Ubuntu unified network configuration in netplan: you write declarative YAML and netplan generates the configuration for whichever backend applies.
flowchart LR
A["/etc/netplan/*.yaml"] --> B["netplan generate"]
B --> C["/run/systemd/network/*"]
B --> D["NetworkManager<br/>(desktop)"]
C --> E["systemd-networkd<br/>(server)"]
E --> F["Kernel:<br/>addresses, routes, DNS"]
D --> F
The networkd renderer is the Ubuntu Server one — fixed machines, servers, cloud — and NetworkManager is the Ubuntu Desktop one, designed for laptops with wifi, roaming and user VPNs. On srv-tramontana it is networkd.
The files live in /etc/netplan/ with a .yaml extension, are read in alphabetical order and the later ones win over the earlier ones. That is why they are numbered:
That file was written by cloud-init during the installation and it may rewrite it on the next boot. The clean way to take control is to disable that part:
$ echo 'network: {config: disabled}' | sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
- Netplan's YAML, field by field
network:
version: 2
renderer: networkd
ethernets:
enp0s3:
dhcp4: false
dhcp6: false
addresses:
- 10.0.2.15/24
routes:
- to: default
via: 10.0.2.2
metric: 100
nameservers:
addresses: [10.0.2.2, 192.0.2.53]
search: [tramontana.example]
mtu: 1500
optional: false| Field | What it does | The detail that matters |
|---|---|---|
version: 2 |
Format version | Always 2; there is no other in use |
renderer |
Backend | networkd on a server |
ethernets |
Device type | Also wifis, bridges, bonds, vlans |
enp0s3 |
The interface name | It must match ip -brief link |
dhcp4/dhcp6 |
DHCP client | Set them to false explicitly with a fixed IP |
addresses |
Addresses with CIDR | 10.0.2.15 without /24 is a frequent mistake |
routes |
Static routes | to: default replaces the old gateway4 |
metric |
The route's cost | The lowest wins among several default routes |
nameservers |
DNS and search domains | They are handed over to systemd-resolved |
mtu |
Maximum transmission unit | 1500 is standard; 1450 is typical inside a tunnel |
optional: true |
Do not wait for the interface at boot | Avoids a 2-minute wait if there is no cable |
The YAML syntax that breaks everybody: indentation uses spaces, never tabs, and two spaces per level is the convention. A tab produces an error as cryptic as found character '\t' that cannot start any token. Three other classics: colons need a space after them (dhcp4: false, not dhcp4:false); lists are written with - or in square brackets, but not half and half; and yes/no are interpreted as booleans, so always use true/false.
netplan generate translates the YAML into systemd-networkd files under /run without activating them. If it fails, you have not touched the network:
$ sudo netplan generate && ls /run/systemd/network/
10-netplan-enp0s3.link 10-netplan-enp0s3.network
netplan try and mode 600
netplan try and mode 600This is the command that separates the people who have lost a server from the people who have not.
$ sudo netplan try
Do you want to keep these settings?
Press ENTER before the timeout to accept the new configuration
Changes will revert in 120 seconds
Configuration accepted.netplan try applies the configuration, starts a 120-second countdown and waits for your ENTER. If you have broken the network and your SSH session is cut off, you cannot press anything: when the deadline runs out, netplan restores the previous configuration. Adjust the deadline with --timeout 300 if the change is a delicate one.
| Command | What it does | Reversible |
|---|---|---|
netplan generate |
Validates and generates files under /run |
It applies nothing |
netplan try |
Applies with an automatic rollback | Yes, automatically |
netplan apply |
Applies, full stop | No |
netplan status --all |
The effective network and DNS state | Read only |
netplan try does not cover every case: it reverts properly if the process is still alive waiting for your ENTER, but not if the system hangs. That is why the complete rule is try plus the VM console open in another window; in VirtualBox that console does not go over the network.
Since 24.04, netplan warns if its files are readable by others: Permissions for /etc/netplan/50-cloud-init.yaml are too open. The reason is straightforward: a netplan file can contain a wifi network's password or 802.1X credentials in the clear, and with 644 any user could read them, intern included. Apply umask 027 and leave them at 600 root:root.
- Name resolution:
systemd-resolved and the 127.0.0.53 stub
systemd-resolved and the 127.0.0.53 stub$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 39 Aug 18 09:14 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
$ cat /etc/resolv.conf
nameserver 127.0.0.53
options edns0 trust-ad
search tramontana.example/etc/resolv.conf is a symbolic link to a generated file, and the only server that appears is 127.0.0.53: systemd-resolved's local stub. Applications ask the stub and the stub forwards to the real DNS servers netplan handed it. Editing it by hand achieves nothing: it is regenerated on the next boot or netplan apply.
$ resolvectl status
Link 2 (enp0s3)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS
Current DNS Server: 10.0.2.2
DNS Servers: 10.0.2.2 192.0.2.53
DNS Domain: tramontana.example
$ resolvectl query bookings.tramontana.example
bookings.tramontana.example: 10.0.2.15 -- link: enp0s3
-- Information acquired via protocol DNS in 1.2ms.Three things matter in that output:
- DNS is configured per interface. A machine with two interfaces can have different DNS servers on each one, and
+DefaultRoutemarks which one is used for anything that does not match a specific domain. DNS Domainis the search domain: with it,resolvectl query bookingscompletes tobookings.tramontana.example.resolvectltakes/etc/hostsinto account;digdoes not, because it talks straight to the server. That discrepancy explains 90% of the "butdigresolves it fine" cases. Andresolvectl flush-cachesempties the cache when you have just changed a record and are still seeing the old one.
- The machine's name:
hostnamectl and /etc/hosts
hostnamectl and /etc/hosts$ hostnamectl
Static hostname: srv-tramontana
Chassis: vm
Virtualization: oracle
Operating System: Ubuntu 24.04.1 LTS
Kernel: Linux 6.8.0-41-generic
$ sudo hostnamectl set-hostname srv-tramontanaThere are three names: the static one (the one in /etc/hostname, the one that persists), the transient one (the one DHCP can impose) and the pretty one (--pretty, free text). /etc/hosts has to be consistent, because many programs resolve their own name at start-up:
127.0.0.1 localhost 127.0.1.1 srv-tramontana 10.0.2.15 srv-tramontana bookings.tramontana.example ::1 localhost ip6-localhost ip6-loopback
The 127.0.1.1 line is a Debian/Ubuntu convention so that the name resolves even with no network.
- Interfaces: predictable names, aliases, VLANs, bonding and bridges
enp0s3 is not arbitrary: it is a predictable name generated by systemd from the device's physical position — en (ethernet) + p0 (PCI bus 0) + s3 (slot 3). The old scheme (eth0, eth1) numbered interfaces by the kernel's detection order, which could change between boots and leave your management cable on the wrong interface.
An IP alias is simply a second address on the same interface: it is enough to add it to addresses. It is useful for migrating a service from one IP to another without a cut. And this is the general view of the virtual interfaces you will meet as soon as you leave the lab, without building them here:
| Type | What it is for | Netplan key | When you need it |
|---|---|---|---|
| VLAN | Separating logical networks over one cable (802.1Q) | vlans: with id and link |
A switch delivers several networks over a trunk port |
| Bonding | Aggregating several NICs: redundancy or more bandwidth | bonds: with interfaces and parameters.mode |
A physical server with two cards and two switches |
| Bridge | A software switch in the same broadcast domain | bridges: with interfaces |
VMs or containers on the physical LAN |
| Tunnel / VPN | A private network over a public one | tunnels: |
Joining sites or remote working → project 08-04 |
The VPN appears here only as a networking concept: the complete WireGuard server is built in project 08-04.
- Routes, metrics and
ip route get
ip route get$ ip route
default via 10.0.2.2 dev enp0s3 proto static metric 100
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15Two entries: the default route and the network's own link route, which the kernel creates by itself when the address is assigned. The kernel always picks the most specific route (the longest prefix) and, where those tie, the one with the lowest metric. A persistent static route is added to the YAML's routes block with to: 198.51.100.0/24 and via: 10.0.2.9.
The golden tool for ending an argument is to ask the kernel what it will do with one specific packet, without sending anything:
$ ip route get 198.51.100.7
198.51.100.7 via 10.0.2.9 dev enp0s3 src 10.0.2.15 uid 1000
$ ip route get 10.0.2.77
10.0.2.77 dev enp0s3 src 10.0.2.15 uid 1000The first one leaves via router 10.0.2.9 following the static route; the second is local and goes through no router at all. When somebody claims "the traffic isn't going the way it should", ip route get closes the conversation in a second.
- Packet forwarding and NAT as concepts
By default, Linux does not forward packets between interfaces: if one arrives that is not addressed to it, it drops it. A kernel parameter controls this, and enabling it turns the machine into a router:
$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
$ echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-forward.conf
$ sudo sysctl --systemIt is made persistent under /etc/sysctl.d/, never by editing /etc/sysctl.conf by hand. NAT is what your home router does and what VirtualBox does with the 10.0.2.0/24 network: it rewrites the source address of outgoing packets so that they all appear to come from a single IP, and it keeps a table to return the replies. NAT is not security, however much it looks like it: it is a workaround for the shortage of IPv4 which, as a bonus, means incoming connections reach nobody by default.
srv-tramontana needs neither forwarding nor NAT: it is an end server, and leaving ip_forward = 0 is the right stance. You will enable it with the VPN in project 08-04. Filtering and NAT rules are written with ufw/nftables, and that is lesson 06-03.
- The Tramontana case: from DHCP to a static IP, verified
Step 0 — measure first and take a copy, the course's convention:
$ { ip -brief a; echo; ip r; echo; resolvectl status; echo; ss -tulpn; } \
| sudo tee /root/network-reference.txt > /dev/null
$ sudo cp /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.bak-$(date +%F)Step 1 — open the VM console in VirtualBox and leave a second SSH session open. Never close the door you are coming in through.
Step 2 — write the configuration:
$ sudo install -m 600 -o root -g root /dev/null /etc/netplan/60-tramontana.yaml
$ sudo tee /etc/netplan/60-tramontana.yaml > /dev/null <<'YAML'
network:
version: 2
renderer: networkd
ethernets:
enp0s3:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 10.0.2.15/24
routes:
- to: default
via: 10.0.2.2
metric: 100
nameservers:
addresses: [10.0.2.2]
search: [tramontana.example]
optional: false
YAML
$ sudo mv /etc/netplan/50-cloud-init.yaml /root/50-cloud-init.yaml.retiredThe cloud-init file is moved out of /etc/netplan/: renaming it inside the directory is not enough, because netplan reads any .yaml.
Step 3 — validate without applying: sudo netplan generate and check that it returns 0.
Step 4 — apply with a safety net: sudo netplan try. Before pressing ENTER, check from the other session that you still have access and that the application answers. Only then do you accept.
Step 5 — measure afterwards and compare:
$ { ip -brief a; echo; ip r; echo; resolvectl status; echo; ss -tulpn; } > /tmp/network-current.txt
$ sudo diff -u /root/network-reference.txt /tmp/network-current.txt
-default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100
+default via 10.0.2.2 dev enp0s3 proto static metric 100
- DNS Domain: ~.
+ DNS Domain: tramontana.exampleThe only thing that has changed is what we wanted: the route goes from proto dhcp to proto static and the search domain appears. The IP is the same and the services listen just as before. If the diff showed anything else, you would have a problem to investigate now, not in three weeks' time.
Step 6 — the acid test, rebooting:
$ sudo reboot
$ ssh [email protected] 'ip -brief a show enp0s3; curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8080/health'
enp0s3 UP 10.0.2.15/24 fe80::a00:27ff:fe4b:1c93/64
200A network configuration is not finished until it survives a reboot. Document the change in /opt/tramontana/HISTORY, like everything else.
Common Mistakes and Tips
- Tabs in the YAML. It is, by a distance, mistake number one. Configure your editor so that it inserts spaces in
.yamlfiles (:set expandtabin vim) and always runnetplan generatebefore applying. - Forgetting the CIDR in
addresses.- 10.0.2.15without/24gives an unclear validation error. The address always carries its mask. - Using
netplan applyon a remote machine. If you get it wrong, the only way back is the physical or hypervisor console.netplan trycosts the same and forgives. - Editing
/etc/resolv.conf. It is a link to a generated file: your change lasts until the next boot. DNS is changed in netplan and checked withresolvectl status. - Leaving the
cloud-initfile active. It may rewrite your configuration at boot; disable it with99-disable-network-config.cfg. - Assigning a static IP inside the DHCP range. Sooner or later the DHCP server hands it to somebody else and you have an intermittent, maddening conflict. Ask for a reservation or an excluded range.
- Tip: keep
/etc/netplan/in git alongside your scripts. It is production configuration and it deserves a history, just like the systemd units.
Security and compliance warning: an addressing change on a server that processes guests' personal data affects firewall rules, access control lists and audit records. In a real environment it is planned with a maintenance window, communicated to the people responsible and reviewed by the security officer. Never reconfigure or scan networks that are not yours: everything here is done on srv-tramontana, your own VM.
Exercises
- Double-checking before applying. Write the exact sequence of commands to change
srv-tramontana's DNS from10.0.2.2to192.0.2.53, from an SSH session, without risking the loss of name resolution. Include the subsequent verification. - A second address and a static route. Add the address
10.0.2.16/24and a route towards198.51.100.0/24through10.0.2.9toenp0s3, persistently. Then prove, without generating any real traffic, that a packet to198.51.100.7will leave by that route and not by the default one. - Diagnosing a broken YAML. When you apply your configuration you get
Error in network definition: enp0s3: unknown key 'gateway4'. Explain what has happened, fix it and say why netplan has not left the machine without a network.
Solutions
1. A DNS server that does not answer leaves the machine "working and mute", so it is handled with the same procedure as the IP:
$ sudo cp /etc/netplan/60-tramontana.yaml /root/60-tramontana.yaml.bak-$(date +%F)
$ sudo sed -i 's/addresses: \[10\.0\.2\.2\]/addresses: [192.0.2.53]/' /etc/netplan/60-tramontana.yaml
$ sudo diff -u /root/60-tramontana.yaml.bak-$(date +%F) /etc/netplan/60-tramontana.yaml
- addresses: [10.0.2.2]
+ addresses: [192.0.2.53]
$ sudo netplan generate && sudo netplan tryVerification from the safety session, before pressing ENTER:
$ resolvectl status | grep 'Current DNS'
Current DNS Server: 192.0.2.53
$ resolvectl flush-caches && resolvectl query bookings.tramontana.example
bookings.tramontana.example: 10.0.2.15 -- link: enp0s3Two details: the copy goes to /root and not to /etc/netplan/, because any .yaml file in that directory is read (with .bak-date it would not be, but it is better not to depend on that); and if the new DNS server did not answer, netplan try reverts on its own after 120 seconds.
2. The address is added to the list and the route to the routes block:
addresses:
- 10.0.2.15/24
- 10.0.2.16/24
routes:
- to: default
via: 10.0.2.2
metric: 100
- to: 198.51.100.0/24
via: 10.0.2.9$ sudo netplan generate && sudo netplan try
$ ip -brief a show enp0s3
enp0s3 UP 10.0.2.15/24 10.0.2.16/24 fe80::a00:27ff:fe4b:1c93/64
$ ip route get 198.51.100.7
198.51.100.7 via 10.0.2.9 dev enp0s3 src 10.0.2.15 uid 1000ip route get interrogates the kernel's routing table without sending a single packet: it is exactly the demonstration the exercise asked for. Had it returned via 10.0.2.2, the static route would not have been applied.
3. gateway4 was the classic key for the gateway and it has been removed in the netplan versions shipped with Ubuntu 24.04. Its replacement is an entry in routes:
The change was made because gateway4 allowed only one gateway and supported neither metrics nor alternative tables; routes covers every case with the same syntax. And the machine has not been left without a network because netplan's parser caught the error in the generate phase, before touching anything: the configuration only reaches the kernel if the YAML is valid in its entirety. That is exactly why netplan generate always comes before netplan try: it separates syntax errors (cheap) from content errors (expensive).
Conclusion
srv-tramontana no longer depends on a DHCP server for the address its own service name advertises. You have left the network written, versioned and verified: a netplan file with mode 600, static IP 10.0.2.15/24, gateway 10.0.2.2, DNS servers and a search domain declared, resolution through systemd-resolved and a procedure — measure, generate, try, compare with diff -u, reboot — that you can repeat on any machine without breaking a sweat. Along the way you have seen why ip is for testing and not for configuring, why IPv6 is protected instead of disabled, and how to ask the kernel which way a specific packet will go.
But look at what you have just done: you have fixed a stable, well-known address for a server that accepts SSH connections with passwords, allows root logins and has already racked up 47 failed attempts from 203.0.113.44 in /var/log/btmp. You have put a permanent letterbox on a door with no lock. In the next lesson, 06-02: SSH and Remote Access, you fix precisely that: you will understand the protocol's three phases and what the fingerprint you are asked to accept the first time really means, you will generate ed25519 keys, you will banish password authentication, you will harden sshd_config directive by directive without locking yourself out of the machine, you will build tunnels to reach Tramontana's database without exposing it, and you will read with lastb who has been knocking on your door 47 times.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
