In the previous lesson you put a lock on the front door. But srv-tramontana still has all its windows open: anybody who reaches the machine can talk to the application on 8080, and if tomorrow PostgreSQL listened on 0.0.0.0 instead of on localhost, to 5432 as well. The bot at 203.0.113.44 keeps calling 47 times a night and nobody is stopping it.

A firewall answers a question that comes before authentication: who is even entitled to speak to this server, and on which port? By the end of this lesson you will have a written and applied policy — only rate-limited SSH and HTTPS from outside, PostgreSQL only from the internal network, 8080 closed to the outside world — you will understand what lies underneath ufw, you will know how to write an nftables ruleset from scratch and you will have fail2ban automatically blocking whoever persists. And all of it without losing your SSH session, because the golden rule does not change: never close the door you are coming in through.

Contents

  1. What a firewall is and what it is not
  2. The Linux model: netfilter and its front ends
  3. Tables, chains, hooks and connection state
  4. ufw in practice
  5. Application profiles
  6. When ufw is not enough: nftables directly
  7. iptables → nftables equivalences
  8. Outbound rules and why almost nobody writes them
  9. Network sysctl settings with an impact on security
  10. fail2ban: from the log to the automatic block
  11. Verifying from outside with nmap
  12. The Tramontana case: the policy and the report to Marta

  1. What a firewall is and what it is not

A firewall decides which packets enter, leave or cross a machine, according to rules you write. That is all it does, and it is worth being very clear about it:

A firewall does A firewall does not
Reduce the surface exposed to the network Fix a badly configured service
Limit who can attempt to authenticate Prevent an attack through the port you do leave open
Slow down scans and automated noise Detect that they are already inside (that is 06-04)
Contain lateral damage after a compromise Replace security updates

Defence in depth consists precisely of not depending on a single layer: if an attacker gets past the firewall, they run into SSH with no passwords; if they get past SSH, they run into svc-tramontana with no shell and with ProtectSystem=strict; if they reach the disk, they run into the encrypted secrets from 06-05. No layer is sufficient on its own, and that is the whole idea.

  1. The Linux model: netfilter and its front ends

There is a single filtering engine in the kernel, netfilter. Everything else is a way of writing rules for it:

Layer What it is Its status in Ubuntu 24.04
netfilter The engine, inside the kernel What actually filters
nftables The current language and the nft tool The native option: one command for IPv4, IPv6, ARP and bridge
iptables The historical interface Present as iptables-nft: it translates to nftables underneath
ufw Uncomplicated Firewall, Ubuntu's front end Installed by default, it writes nftables rules
firewalld A front end with zones, typical of RHEL Available, not common here

The practical consequence: on Ubuntu 24.04, when you write ufw allow 22/tcp, ufw generates rules that end up in the same nftables engine you would see with nft list ruleset. They are not rival systems: they are two levels of abstraction over the same thing.

And the warning that saves whole afternoons: use one or the other, not both at once. If you enable ufw and also enable the nftables service with your own /etc/nftables.conf, you will have two competing sets of rules, a flush ruleset that wipes ufw's at every boot and behaviour that is impossible to reason about.

  1. Tables, chains, hooks and connection state

Four concepts and you can already read any ruleset:

  • Table: the container. In nftables it is declared with a family: ip (IPv4), ip6, inet (both at once, which is what you will want almost always), arp, bridge, netdev.
  • Chain: an ordered set of rules hooked onto a kernel hook. Three matter: input (packets destined for this machine, where you write nearly everything), output (packets it generates itself, the egress of section 8) and forward (packets passing through it, only if it is a router or a VPN → 08-04).
  • Default policy: what happens to a packet that matches no rule. There are only two philosophies, and one of them is the right one: policy drop (an allowlist: everything is forbidden and what is needed is permitted) versus policy accept (a blocklist: everything is permitted and known bad things are forbidden). The blocklist is impossible to maintain, because it requires knowing in advance everything that can go wrong.
  • Connection state: netfilter remembers connections in progress (connection tracking). That lets you write ct state established,related accept and forget about return traffic.

That last point is the one that changes everything. Without stateful filtering you would have to open the high ports for the replies by hand, which amounts to filtering nothing at all. With state the logic is beautifully clean: new is a connection's first packet and that is where the decision is made; established belongs to a connection already accepted and is accepted without a second look; related is a secondary connection of another one (ICMP errors, FTP data) and is accepted too; and invalid matches no known connection, so it is dropped.

  1. ufw in practice

ufw is what you will use 95% of the time. Always start by measuring:

$ sudo ufw status verbose
Status: inactive

The correct order of operations, and there is no other way of getting it right on a remote machine:

# 1. Default policies: deny incoming, allow outgoing
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw default deny routed
# 2. FIRST allow SSH. Without this, step 4 locks you out.
$ sudo ufw limit 22/tcp comment 'SSH with attempt limiting'
# 3. The remaining services
$ sudo ufw allow 443/tcp comment 'Public HTTPS'
$ sudo ufw allow from 10.0.2.0/24 to any port 5432 proto tcp comment 'PostgreSQL internal network'
# 4. NOW you can enable it
$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

That warning in step 4 is not decorative: if you run ufw enable with the deny incoming policy and without having allowed SSH, the session is cut off there and then and the only way back to the machine is the VM console. Have that console open before you start.

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT IN    Anywhere                   # SSH with attempt limiting
443/tcp                    ALLOW IN    Anywhere                   # Public HTTPS
5432/tcp                   ALLOW IN    10.0.2.0/24                # PostgreSQL internal network
22/tcp (v6)                LIMIT IN    Anywhere (v6)

Four details that matter in this output:

  • ufw automatically creates the equivalent IPv6 rule. That is exactly why you do not disable IPv6 "just in case": here it is covered.
  • LIMIT is not ALLOW: ufw limit blocks an IP that opens 6 or more connections in 30 seconds. It is a cheap defence against brute force, although fail2ban (section 10) is far more precise.
  • The comment is living documentation, and deny (routed) makes it clear that this machine forwards nothing, consistent with the ip_forward = 0 of 06-01.

deny versus reject, which is a real difference and not a nuance:

Action What the kernel does What the caller sees When to use it
deny (DROP) Discards silently A timeout: they do not know whether the machine exists Facing the Internet: it costs the scanner time
reject Replies with ICMP port unreachable or a TCP RST An immediate "closed" The internal network: it saves your applications 30-second waits

Managing existing rules means seeing them numbered, because order matters: the first one that matches wins.

$ sudo ufw status numbered
     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     LIMIT IN    Anywhere
[ 2] 443/tcp                    ALLOW IN    Anywhere
[ 3] 5432/tcp                   ALLOW IN    10.0.2.0/24
$ sudo ufw delete 3
$ sudo ufw insert 1 deny from 203.0.113.44 comment 'manual bot block'

Careful: when you delete rule 3, the following ones are renumbered. Always delete from highest to lowest or, better still, delete by specification (sudo ufw delete allow 443/tcp), which is idempotent and does not depend on numbers.

  1. Application profiles

ufw ships named profiles in /etc/ufw/applications.d/, which translate "OpenSSH" into "port 22/tcp":

$ sudo ufw app list
Available applications:
  Nginx Full
  OpenSSH
$ sudo ufw app info OpenSSH
Profile: OpenSSH — Ports: 22/tcp
$ sudo ufw allow OpenSSH

And you can write your own, which is the clean way to document your application's ports:

[Tramontana]
title=Tramontana Bookings
description=Bookings web application (reverse proxy in 08-01)
ports=443/tcp

Save it in /etc/ufw/applications.d/tramontana and reload it with sudo ufw app update Tramontana: if the port changes tomorrow, it changes in a single place.

  1. When ufw is not enough: nftables directly

ufw falls short when you need fine-grained rate limiting, address sets (set), packet marking, elaborate NAT or simply to read exactly what is there. Then you write nftables by hand, in /etc/nftables.conf:

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
    set internal_networks {
        type ipv4_addr ; flags interval ; elements = { 10.0.2.0/24 }
    }

    chain input {
        type filter hook input priority 0; policy drop;
        # Return traffic and rubbish: first of all, for efficiency
        ct state established,related accept
        ct state invalid drop
        # Loopback: without this half the system and the SSH tunnels break
        iif lo accept
        # Essential ICMP (do not block ICMP wholesale: it breaks MTU discovery)
        ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } accept
        ip6 nexthdr ipv6-icmp accept
        # SSH with rate limiting on new attempts, and public HTTPS
        tcp dport 22 ct state new limit rate 6/minute burst 6 packets accept
        tcp dport 443 accept
        # PostgreSQL only from the internal network
        ip saddr @internal_networks tcp dport 5432 accept
        # Everything else falls to the policy, leaving a bounded trail
        limit rate 5/minute log prefix "nft-drop-in: " level info
        counter
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }

    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Notes on the design, which is what you are here to learn:

  • table inet covers IPv4 and IPv6 with the same rules. With iptables you would have to duplicate everything in ip6tables, and that is where people leave holes.
  • policy drop on input and forward, accept on output: an allowlist where it matters.
  • The order is deliberate: the most frequent at the top (established), the exceptional at the bottom. Every packet walks the chain until it finds its rule.
  • Do not block all ICMP. destination-unreachable carries MTU discovery: without it you will have connections that hang when transferring large files and you will go mad looking for the cause.
  • The final log carries its own limit rate, because a scan can generate thousands of lines a second and fill your disk. Logging what is dropped is what makes a firewall diagnosable.
$ sudo nft -c -f /etc/nftables.conf && echo "syntax OK"
syntax OK
$ sudo systemctl enable --now nftables
$ sudo nft list ruleset | head -2
table inet filter {
        set internal_networks {

nft -c checks without applying: it is the netplan generate of firewalls, and it is always used. Persistence comes from the nftables service, which runs /etc/nftables.conf at every boot; without it, nft is as volatile as ip addr add.

  1. iptables → nftables equivalences

There are still mountains of documentation written in iptables. This table lets you read it:

iptables nftables
-A INPUT -p tcp --dport 22 -j ACCEPT tcp dport 22 accept
-P INPUT DROP policy drop in the chain definition
-m state --state ESTABLISHED,RELATED -j ACCEPT ct state established,related accept
-s 10.0.2.0/24 / -i lo -j ACCEPT ip saddr 10.0.2.0/24 / iif lo accept
-j REJECT --reject-with icmp-port-unreachable reject with icmpx type port-unreachable
iptables -L -n -v / iptables-save nft list ruleset
Separate rules in iptables/ip6tables A single one in table inet

  1. Outbound rules and why almost nobody writes them

Almost everybody writes output policy accept and forgets about it. It is convenient and it is a missed opportunity: egress rules do not stop people getting in, but they hugely limit what an attacker can do afterwards. Without free outbound access, they cannot download their second stage from an external server, they cannot exfiltrate bookings.csv to some arbitrary host and they cannot join a botnet.

    chain output {
        type filter hook output priority 0; policy drop;
        ct state established,related accept
        oif lo accept
        udp dport { 53, 123 } accept       # DNS and NTP
        tcp dport { 53, 80, 443 } accept   # DNS/TCP and apt repositories
        ip daddr 10.0.2.0/24 accept        # internal network
        limit rate 5/minute log prefix "nft-drop-out: "
    }

Why almost nobody writes them, honestly: they break things in subtle, hard-to-diagnose ways. An apt update against a new mirror, a webhook, a certificate renewal... everything fails with timeouts nobody connects to the firewall. If you adopt them, do it by measuring first which outbound traffic the machine really uses (with logging on output and the policy at accept for a week) and only then changing the policy to drop.

  1. Network sysctl settings with an impact on security

These parameters are not about performance — that is 07-03 — but about how the network stack behaves in the face of abuse:

$ sudo tee /etc/sysctl.d/60-network-security.conf > /dev/null <<'CONF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
CONF
$ sudo sysctl --system | grep -A8 60-network-security
Parameter What it prevents
rp_filter = 1 Packets with a forged source that would not come back through the same interface (antispoofing)
accept_redirects = 0 / send_redirects = 0 Somebody else's ICMP redirect rewriting your routing table, and you reporting topology to third parties
accept_source_route = 0 Packets that dictate their own path, a classic evasion technique
tcp_syncookies = 1 A SYN flood exhausting the half-open connection queue
icmp_echo_ignore_broadcasts = 1 Being an amplifier for a smurf attack
log_martians = 1 Logs impossible packets: an early sign that something is odd

Careful with rp_filter = 1 (strict): on a machine with several interfaces and asymmetric routes it drops legitimate traffic. On srv-tramontana, with a single interface, it is safe; on a router or a machine with a VPN, use 2 (loose mode).

  1. fail2ban: from the log to the automatic block

fail2ban closes the loop: it reads the logs, detects patterns of abuse and asks the firewall to block the IP for a while. It does not replace the firewall, it drives it.

$ sudo apt install -y fail2ban
$ sudo tee /etc/fail2ban/jail.local > /dev/null <<'CONF'
[DEFAULT]
# NEVER block yourself: internal network and the team's laptops
ignoreip  = 127.0.0.1/8 ::1 10.0.2.0/24
bantime   = 1h
findtime  = 10m
maxretry  = 5
backend   = systemd
banaction = ufw

[sshd]
enabled   = true
port      = ssh
maxretry  = 3
bantime   = 24h
CONF
$ sudo systemctl enable --now fail2ban
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Total failed:     47
|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
   |- Currently banned: 1
   `- Banned IP list:   203.0.113.44

You edit jail.local, never jail.conf: the latter belongs to the package and the next update sweeps your changes away. The three figures that govern everything:

Parameter Meaning Chosen value
maxretry / findtime Failures tolerated and the window in which they are counted 3 in 10 minutes
bantime How long the block lasts 24 h (-1 would be permanent)
banaction How it blocks ufw, so as not to create a third set of rules
ignoreip Who is immune Your network and your laptops

ignoreip is the protection against the most humiliating mistake of all: mistyping your sudo password three times and having your own server block you. If it happens anyway:

$ sudo fail2ban-client set sshd unbanip 10.0.2.77
$ sudo fail2ban-client set sshd banip 203.0.113.44

And the honesty this deserves: with PasswordAuthentication no from 06-02, fail2ban no longer protects you from anybody guessing anything, because there is nothing to guess. What it does is reduce noise, resource use and surface: fewer connections, fewer logs, fewer opportunities in the face of a future sshd bug. It is a hygiene measure, not the one that saves you.

  1. Verifying from outside with nmap

A firewall is not verified until you look at it from outside. From laptop-student, against your own VM:

$ nmap -Pn -p 22,443,5432,8080 10.0.2.15
Nmap scan report for srv-tramontana (10.0.2.15)
PORT     STATE    SERVICE
22/tcp   open     ssh
443/tcp  open     https
5432/tcp open     postgresql
8080/tcp filtered http-proxy

How to read it: open answers, closed answers that nobody is there, and filtered means something is silently dropping the packet — precisely the effect of your deny policy. Port 8080 shows as filtered: objective achieved. 5432 comes out open because the scan comes from 10.0.2.77, which is inside the permitted internal network; repeated from outside that range it would come out filtered.

Legal warning, with no qualifications: scanning the ports of systems that are not yours without written authorisation is illegal in Spain and in practically every jurisdiction, and it can constitute a criminal offence even if you cause no damage. In this course every scan is run from laptop-student against srv-tramontana, both of which are yours and inside your lab. A tool being legal does not make every use of it legal: the difference between an audit and an attack is permission, and permission is documented in writing before you type anything.

  1. The Tramontana case: the policy and the report to Marta

The policy ends up like this, and it is decided before any command is touched:

Service Port From where Reason
SSH 22/tcp Anywhere, with limit Administration; hardened in 06-02
HTTPS 443/tcp Anywhere The public face; the proxy arrives in 08-01
PostgreSQL 5432/tcp Only 10.0.2.0/24 Never from the Internet
Application 8080/tcp Nobody It will sit behind the reverse proxy (08-01)
Everything else — Denied It is an allowlist

Diagnosis when "the service does not answer" and the cause is you: first look at whether the service is listening (ss -tulpn | grep 8080), then at whether the firewall is dropping it.

$ sudo journalctl -k --since "-15m" | grep 'UFW BLOCK' | tail -2
Aug 18 19:22:41 srv-tramontana kernel: [UFW BLOCK] IN=enp0s3 SRC=203.0.113.44 DST=10.0.2.15 PROTO=TCP SPT=44210 DPT=8080 SYN
Aug 18 19:23:02 srv-tramontana kernel: [UFW BLOCK] IN=enp0s3 SRC=198.51.100.9 DST=10.0.2.15 PROTO=TCP SPT=51882 DPT=23 SYN

Read it from left to right: it came in through enp0s3, from SRC, towards DPT (the destination port). The first line is the bot looking for the application on 8080; the second, somebody trying Telnet in 2026. If the thing showing up blocked is your own legitimate service, you already know which rule is missing.

The report to Marta, following the course's structure — what it protects and what it does not:

Firewall applied on srv-tramontana on 18/08. What it protects: only SSH (with attempt limiting) and HTTPS are reachable from outside; the database only accepts connections from the internal network; the application's port 8080 is no longer reachable from the Internet; the IP 203.0.113.44, with 47 access attempts, is automatically blocked for 24 hours by fail2ban, and so will any other that tries. What it does NOT protect: it does not defend against a flaw in the web application itself, which is still reachable through the port that has to be open; it prevents nothing for anybody holding valid credentials; it does not detect that somebody is already inside (that is covered in the detection review); and it does not encrypt the traffic, which still travels in the clear until we install the certificate. Outstanding: TLS encryption and getting the database password out of the configuration file.

Security and compliance warning: in a real environment, any change to the firewall rules of a system that processes personal data must be planned with a maintenance window, be documented and be reviewed by the security officer; the exposure of services to the Internet is part of the risk analysis required by the GDPR. Scanning and testing techniques are applied exclusively to systems that are your own or with written authorisation.

Common Mistakes and Tips

  • ufw enable before allowing SSH. The classic mistake and the most expensive one. Always allow first, enable afterwards, and with the VM console open.
  • Mixing ufw and /etc/nftables.conf. Two competing sets of rules and a flush ruleset that wipes the other one at every boot. Pick one.
  • Blocking all ICMP. It breaks MTU discovery and causes connections that hang on large transfers. Allow at least destination-unreachable and time-exceeded.
  • Forgetting iif lo accept. Half the machine stops working: SSH tunnels, databases over a local TCP socket, processes that talk to each other over 127.0.0.1.
  • Rules with no comment. In six months' time nobody will dare delete a rule when they do not know what it is for, and the ruleset will only grow.
  • log without limit rate. A scan fills /var/log in minutes, and running out of disk is a self-inflicted outage. And do not rely on the firewall alone for internal traffic: the application should be listening only on 127.0.0.1 anyway.
  • Tip: keep /etc/nftables.conf or the output of ufw status numbered in git, and add a check that the firewall is active to health_check.sh.

Exercises

  1. The full policy from scratch. Write the exact sequence of ufw commands to apply the policy in the table in section 12 on a remote machine, in the right order, including the checks before and after.
  2. A rule ufw does not express well. Marta asks that only laptop-luis (10.0.2.30) should be able to reach 8080 for testing, with a maximum of 10 new connections per minute. Write the nftables rule and explain why ufw falls short.
  3. Self-inflicted block. Luis calls: he cannot get in over SSH from 10.0.2.77 and says "the server has thrown him out". Diagnose the problem, resolve it and propose the permanent fix.

Solutions

1. With the VM console open and a second SSH session active:

# Measure first
$ sudo ufw status verbose > /root/ufw-before.txt ; ss -tulpn | grep LISTEN
# Default policies
$ sudo ufw default deny incoming && sudo ufw default allow outgoing
$ sudo ufw default deny routed
# SSH FIRST, always; then the rest
$ sudo ufw limit 22/tcp comment 'SSH administration'
$ sudo ufw allow 443/tcp comment 'Public HTTPS'
$ sudo ufw allow from 10.0.2.0/24 to any port 5432 proto tcp comment 'PostgreSQL internal'
# Enable and verify
$ sudo ufw enable && sudo ufw status numbered
$ sudo diff -u /root/ufw-before.txt <(sudo ufw status verbose)

And the verification that really closes the job, from laptop-student: open a new SSH session without closing the previous one, and run nmap -Pn -p 22,443,5432,8080 10.0.2.15 to check that 8080 shows as filtered. Notice that 8080 needs no rule at all: the deny incoming policy already covers it. In an allowlist, whatever is not named is forbidden.

2. The rule in nftables, inside the input chain and before the final log:

        ip saddr 10.0.2.30 tcp dport 8080 ct state new \
            limit rate 10/minute burst 5 packets accept

ufw falls short for two reasons: it only offers limit, with a fixed threshold (6 connections in 30 seconds) that cannot be adjusted without editing its internal templates, and it does not allow you to combine a specific source, a port, a connection state and a custom rate in a single rule. Here we need exactly that. The alternative within ufw would be sudo ufw allow from 10.0.2.30 to any port 8080 proto tcp, which gives you the source control but not the rate limiting.

An important detail: ct state new makes the rate apply only to new connections. Without it, you would also be limiting the packets of a transfer in progress and causing random cut-offs that would look like a network problem.

3. Diagnosis in three commands:

$ sudo fail2ban-client status sshd | grep 'Banned IP list'
   `- Banned IP list:   203.0.113.44 10.0.2.77
$ sudo journalctl -u fail2ban --since "-1h" | grep 10.0.2.77
fail2ban.actions [1204]: NOTICE  [sshd] Ban 10.0.2.77

Luis has failed three times in ten minutes — probably with the wrong key, or without having loaded it into the agent — and the sshd jail has blocked him for 24 hours. Immediate unblocking and permanent fix:

$ sudo fail2ban-client set sshd unbanip 10.0.2.77
$ grep ignoreip /etc/fail2ban/jail.local
ignoreip  = 127.0.0.1/8 ::1 10.0.2.0/24

And here is the real lesson: ignoreip already included 10.0.2.0/24, so if the block happened it is because the file was edited without reloading the service, or because the line was written in jail.conf instead of in jail.local and a package update carried it away. The permanent fix is sudo fail2ban-client reload after every change, verifying with sudo fail2ban-client get sshd ignoreip, and adding that check to health_check.sh. The underlying root cause is that Luis is using a password where he should be using his ed25519 key: review his authorized_keys.

Conclusion

srv-tramontana no longer talks to just anybody. It has an allowlist policy that is written, applied in the right order and verified from outside with nmap: rate-limited SSH, open HTTPS, PostgreSQL restricted to the internal network and 8080 invisible from the Internet while it waits for the reverse proxy in Module 8. You know that underneath ufw there is nftables and that underneath nftables there is netfilter; you know how to write a complete inet ruleset with connection state, sets and rate limiting; you have tuned the sysctl settings that prevent spoofing, redirects and SYN floods; and fail2ban automatically blocks 203.0.113.44 and whoever comes next, without blocking you. The first of the three open incidents is now closed.

And now the uncomfortable part. Everything you have built in these three lessons is prevention, and prevention fails: it fails through a CVE that has no patch yet, through a leaked credential — like the one in that backup with 644 permissions — through a lapse in opening a rule "just for a moment", or because somebody with legitimate access does something they should not. The day it fails, your firewall will tell you nothing: it will go on obediently applying the rules while the attacker's connection shows up as established. The question stops being "how do I stop them getting in?" and becomes "how do I find out that they are already in?". That is lesson 06-04: Intrusion Detection Systems, where you will set up AIDE to watch file integrity, put auditd on the trail of every write to app.conf, hunt for the signs of compromise in the logs, run Lynis against your server and learn the incident response procedure — including the rule nobody likes hearing: a compromised server is reinstalled, not cleaned.

Linux Course: From Beginner to System Administrator

Module 1: Introduction to Linux

Module 2: Basic Linux Commands

Module 3: Advanced Command-Line Skills

Module 4: Shell Scripting

Module 5: System Administration

Module 6: Networking and Security

Module 7: Advanced Topics

Module 8: Practical Projects

© Copyright 2026. All rights reserved