The three previous lessons have built prevention: an access control model, well-managed identities, a confined service and a hardened system. All of that reduces the probability of something bad happening and limits the damage if it does. But none of those measures answers the question that really keeps anyone running a production server awake: how do I know whether something has happened?
Imagine the concrete scenario we will solve at the end of this lesson. It is 09:00 and monitoring raised an alert in the small hours: between 03:12 and 03:40 there was a spike in writes to /var/lib/meteora twenty times the norm for that hour, and right now there is a running process whose name nobody on the team recognizes. What do you do? Kill it? Reboot the server? Disconnect the network? Look at the logs first, or at the process? Each of those decisions, taken in the wrong order, destroys information that will not come back, and some of them make the incident worse instead of containing it.
This lesson gives you the method. First, what gets logged on a Linux system and where: syslog with its facilities and priorities, and journald with its filters — the ones you will actually use. Then, how an application's logging is designed for something like meteo-api: what every event must contain and, above all, what must never appear there, with the corresponding GDPR warning. Then rotation and retention, the kernel's auditd subsystem with its real performance cost, and the most uncomfortable problem of all: the integrity of a log written by a system the attacker controls. We go on with host change detection — AIDE, rootkits, concrete indicators — and finish with the incident response cycle applied step by step to the 03:12 case, with evidence preservation and its legal implications.
Contents
- Why without logging there is no detection and no response
syslog: facilities, priorities and/var/log/journaldandjournalctlin practice- Designing an application's logging
- Rotation and retention with
logrotate - The kernel's audit subsystem:
auditd - Log integrity against an attacker with privileges
- Detecting changes and intruders on the host
- The incident response cycle
- Case study: the 03:12 spike
- Evidence preservation
- Which preventive measures each incident leaves behind
Why without logging there is no detection and no response
Security controls fall into three families, and it is worth seeing where each thing we have learned fits:
| Type of control | What it does | Examples from the module |
|---|---|---|
| Preventive | Stops it from happening | Permissions, capabilities, AppArmor, firewall, seccomp |
| Detective | Tells you it has happened | Logs, auditd, AIDE, monitoring |
| Corrective | Repairs or contains | Backups, isolation, reinstallation |
A system with preventive controls and no detective controls has a structural problem: when prevention fails, nobody finds out. And prevention always fails at some point, because software has bugs, configurations decay and people make mistakes.
Without logs, four things become impossible, not merely difficult:
- Detecting. An attacker who breaks nothing visible can stay for months. The median undetected dwell time in real incidents is measured in weeks, not hours.
- Investigating. With no trail you cannot answer the questions that matter: what got in, when, through where, what it touched and what it took.
- Containing sensibly. Without knowing the scope, the only option is to shut everything down, which is usually disproportionate and sometimes destroys the evidence.
- Learning. An incident from which no concrete preventive measure is drawn will happen again.
Hence the working rule of this lesson: logging is not a by-product of the system, it is a security control and it is designed as such, with its content, its retention, its protection and its verification.
syslog: facilities, priorities and /var/log/
syslog is the classic UNIX mechanism: a daemon receives messages from every program and classifies them along two dimensions. It is still relevant because it is the industry's common language: routers, firewalls, storage arrays and central collectors all speak syslog.
| Dimension | Values |
|---|---|
| Facility (where it comes from) | kern, user, mail, daemon, auth, authpriv, cron, syslog, lpr, news, uucp, local0–local7 |
| Priority (how serious it is) | emerg(0), alert(1), crit(2), err(3), warning(4), notice(5), info(6), debug(7) |
The ones that will matter most to you: auth and authpriv collect everything to do with authentication and elevation — logins, sudo, PAM — and authpriv is the variant for anything that may contain sensitive data, which is why its file has more restrictive permissions. And local0–local7 are reserved for your own applications: meteo-api can use local3, for instance, keeping it separate from everything else without touching the rest of the system.
| File | What it contains | Why look at it |
|---|---|---|
/var/log/auth.log |
Authentication, sudo, SSH, PAM |
The first one in any investigation |
/var/log/syslog |
Everything general | A joined-up view |
/var/log/kern.log |
Kernel messages | OOM killer, disks, AppArmor denials |
/var/log/meteora/meteo-api.log |
The application's log | Service activity |
/var/log/audit/audit.log |
The auditd subsystem |
Fine-grained auditing (section 6) |
/var/log/wtmp, btmp, lastlog |
Successful sessions, failed ones and last login | Binary: read with last, lastb, lastlog |
# /etc/rsyslog.d/50-meteora.conf local3.* /var/log/meteora/meteo-api.log auth,authpriv.* /var/log/auth.log *.emerg :omusrmsg:* auth,authpriv.* @@collector.meteora.example:6514 # REMOTE copy over TLS
How to read that configuration: each line is a selector (facility.priority) followed by a destination. local3.* sends everything from the application to its own file. *.emerg notifies all logged-in users of emergencies. And the last line is the important one for section 7: @@ means sending over TCP with TLS to a remote collector — with a single @ it would be UDP, with no delivery guarantee — so that authentication events also exist off the machine. That detail is what separates a log that is useful in an investigation from one that is not.
journald and journalctl in practice
On a current Debian, the main log is the systemd journal: a binary, indexed store in which every entry carries structured fields as well as the text — unit, PID, UID, executable, priority, boot identifier — which is why it can be queried like a small database.
| syslog | journald | |
|---|---|---|
| Format | Plain text | Indexed binary |
| Querying | grep, awk |
journalctl with per-field filters |
| Metadata | Whatever the line carries | Automatic and unforgeable by the sender |
| Persistence | Always on disk | Configurable: volatile or persistent |
| Integrity | None | FSS sealing (--setup-keys) |
| Interoperability | Universal | Specific to systemd (with forwarding to syslog) |
The decisive property is the third one: journald adds the metadata itself, taking it from the system and not from the text of the message. A process cannot lie about its PID, its UID or its unit, because it is not the one writing them. In syslog, anyone who can write to the socket can claim to be whoever they like.
# --- Persistence: the FIRST thing to check ---
sudo mkdir -p /var/log/journal && sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage
# --- Essential filters ---
journalctl -u meteo-api # by unit
journalctl -u meteo-api -f # follow live
journalctl -p err # priority err or above
journalctl --since "2026-09-01 03:00" --until "2026-09-01 04:00"
journalctl --since "-2h" # the last two hours
journalctl -b # this boot only
journalctl -b -1 # the PREVIOUS boot
journalctl _UID=990 # by field: everything from user meteora
journalctl _COMM=sudo # every elevation
journalctl -k -g -i apparmor # kernel messages matching a pattern
journalctl -u meteo-api -o json-pretty # with ALL the fields, for automationThe ones that make the difference in an investigation. --since and --until narrow the time window, which is always the first step: you do not read "the log", you read the relevant half hour. -b -1 queries the previous boot, essential when the server has been rebooted — or someone rebooted it — and you need to see what happened just before. The per-field filters (_UID=, _COMM=, _PID=) are the real advantage of the indexed format: journalctl _UID=990 --since "-24h" gives you everything the service's identity did in a day, without depending on the message text containing the right word. And -o json-pretty exposes every field, which is what you need to feed a collector or to automate a check.
The persistence check is not optional. Without
/var/log/journal, the journal lives in/run, which is tmpfs (04-03): it is lost entirely on every reboot. An attacker who reboots the machine erases the log without touching a single file, and you are left investigating an incident of which nothing remains. It is one of the first things to verify on a freshly installed server.
And the three retention limits, which have to be set deliberately:
# /etc/systemd/journald.conf Storage=persistent SystemMaxUse=2G # total cap on the journal SystemMaxFileSize=128M MaxRetentionSec=90day # ← THE INVESTIGATION WINDOW ForwardToSyslog=yes # copy to rsyslog, and from there to the remote collector
MaxRetentionSec is the security decision in this block: it defines how far back you can investigate. If a compromise is detected six weeks later — as is usual — and your retention is seven days, the logs from the moment of entry no longer exist and you will never know how they got in.
Designing an application's logging
A useful log does not happen by itself: it is designed. These are the fields every meteo-api event must carry:
| Field | Why | Example |
|---|---|---|
| Timestamp with time zone | Without the zone, correlating with other systems is impossible | 2026-09-01T03:12:07.481+02:00 |
| Event | What happened, in a closed vocabulary | auth.failure, query.ok, export.denied |
| Identity | Who did it | client_id=CLI-4471 |
| Origin | Where from | origin=203.0.113.45 |
| Object | On what | resource=/readings/2026-08-31 |
| Result | Success or failure, always | result=denied |
| Correlation identifier | To follow a request across services | trace=7f3a9c21 |
| Severity | For filtering and alerting | level=warning |
import logging, json, uuid
from logging.handlers import SysLogHandler
log = logging.getLogger("meteo-api")
log.addHandler(SysLogHandler(address="/dev/log", facility=SysLogHandler.LOG_LOCAL3))
def log_event(event, result, **fields):
log.info(json.dumps({"event": event, "result": result, **fields}))
# At the relevant point in the application:
trace = uuid.uuid4().hex[:8]
log_event("auth.failure", "denied", client="CLI-4471",
origin=request.ip, reason="token_expired", trace=trace)Three decisions in this code. The structured format (JSON) allows querying and aggregating without depending on fragile regular expressions; a collector can filter by event without understanding the text. The correlation identifier is generated when the request arrives and accompanies every event it causes, including those of ingestor and aggregator if it is propagated: it is what lets you reconstruct one request across three services. And the reason for the failure is always logged, because "denied" with no reason is of no use whatsoever in an investigation.
What must NEVER appear in a log
| Never | Why |
|---|---|
| Passwords, not even failed ones | A failure is usually a typo on the correct password |
| Tokens, API keys, session cookies | Whoever reads the log can reuse them as they are |
| Card numbers, banking data | Forbidden by sector regulation |
| Unnecessary personal data | Minimization: if it is not needed to operate or investigate, it is not logged |
| Full request bodies | It drags in all of the above without meaning to |
| Memory dumps in the log | They contain freshly read secrets (05-03) |
The case of failed passwords deserves a pause. Logging the failed attempt "for investigation" seems harmless, and it is one of the worst possible practices: most failures are typing errors on the right password, so the log ends up containing, in the clear and with the user name next to it, your users' real credentials. And that log is read by the whole adm group, travels to the central collector and goes into the backups.
With personal data, the operational rule is log identifiers, not people: client=CLI-4471 instead of the name and the email address, and a separate table — with its own access control — that translates the identifier when it is genuinely needed.
GDPR and compliance warning. An IP address, a user identifier or an access history are personal data under the GDPR. Recording them requires a legal basis, information to the data subjects, minimization, a defined and justified retention period, access control and, where applicable, an impact assessment. Log retention also routinely conflicts with the right to erasure, and there are sector obligations that impose minimum periods. Define the logging and retention policy together with the compliance officer or legal counsel, in writing, before deploying it, and do not improvise it in the middle of an incident.
Rotation and retention with logrotate
Without rotation, a log grows until it fills the disk, and a full /var/log stops services — including logging itself, so you lose the trail exactly when you need it most.
# /etc/logrotate.d/meteora
/var/log/meteora/*.log {
daily # rotate every day
rotate 90 # keep 90 files = a 90-day window
compress # compress the old ones (.log files compress ~10:1)
delaycompress # do not compress the newest: it may still be open
missingok # do not fail if it does not exist yet
notifempty # do not rotate an empty file
create 0640 meteora adm # the new one is born with the permissions of 04-06
dateext # date-based names: meteo-api.log-20260901
sharedscripts
postrotate
systemctl reload meteo-api > /dev/null 2>&1 || true
endscript
}The three directives that cause trouble if misunderstood. create 0640 meteora adm is essential: without it, the new file inherits the umask of the rotation process — which runs as root — and may end up with the wrong permissions or with owner root, and then the service can no longer write to its own log. postrotate with reload exists for a reason you already know from 04-04: when logrotate renames the file, the process keeps writing to the same inode through its open descriptor, so its messages go to the rotated file and the new one stays empty; it has to be told to reopen. The alternative is copytruncate, which copies and truncates in place, but loses the lines written between the copy and the truncation, so it is only used when there is no way to signal the process. And delaycompress avoids compressing a file that may still be open.
The retention trade-off is worked out with concrete numbers. meteo-api generates about 40 MB of log a day; ninety days uncompressed would be 3.6 GB, and compressed comes to around 400 MB. The question that decides the value is not how much space it takes, but how far back you need to be able to investigate:
| Retention | Space (compressed) | What it lets you investigate |
|---|---|---|
| 7 days | ~30 MB | Only incidents detected immediately |
| 90 days | ~400 MB | A compromise detected weeks later: the usual case |
| 365 days | ~1.6 GB | Full investigation; may clash with data minimization |
Ninety days is a reasonable starting point for meteo-01: it covers the realistic case of late detection without accumulating personal data for a year. But it is a decision that must be validated with compliance, because there may be mandatory minimum periods or maximums imposed by minimization.
sudo logrotate -d /etc/logrotate.d/meteora # simulate WITHOUT doing anything
sudo logrotate -f /etc/logrotate.d/meteora # force a test rotationThe kernel's audit subsystem: auditd
journald records what programs decide to tell you. auditd records what happens in the kernel, whether the program likes it or not: which process opened which file, which system call was executed, who changed a permission. It is the difference between the suspect's testimony and the camera footage.
# /etc/audit/rules.d/meteora.rules ## 1. Watches on critical files (w = write, a = attribute change) -w /etc/meteora/meteora.conf -p wa -k meteora_conf -w /etc/meteora/secrets.conf -p rwa -k meteora_secrets # READS too -w /etc/passwd -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/sudoers -p wa -k escalation -w /etc/sudoers.d/ -p wa -k escalation -w /etc/ssh/sshd_config -p wa -k remote_access ## 2. Sensitive system calls -a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=-1 -k root_exec -a always,exit -F arch=b64 -S mount -S umount2 -k mounting -a always,exit -F arch=b64 -S init_module -S finit_module -k modules -a always,exit -F arch=b64 -S chmod -S chown -S setxattr -F dir=/var/lib/meteora -k permissions ## 3. Immutable rules: NOBODY can change them without a reboot -e 2
Each block answers a different need. The -w rules watch paths: notice that secrets.conf also carries r, because in a key file reading it is already the relevant event, not just modifying it. The system call rules are more precise: the first records every execve run with root privileges by someone who logged in as a normal user — auid is the audit identifier, which does not change with su or sudo and therefore lets you attribute the action to the real person; the following ones cover mounting, module loading and permission changes in the data tree. And -e 2 makes the rules immutable: from then on not even root can modify them without rebooting the machine, and a reboot is itself a highly visible event.
sudo augenrules --load && sudo auditctl -l # load and verify
# Queries
sudo ausearch -k meteora_secrets -i # -i translates UIDs and calls into names
sudo ausearch -k escalation --start today -i
sudo ausearch -ua 1001 --start recent -i # everything for auid 1001 (carlos)
sudo aureport --summary ; sudo aureport --auth --failed -iausearch searches by key, by audit user, by time range or by process, and -i is practically mandatory because it translates the numbers into readable names. aureport produces summaries: the failed authentication one is among the most useful for a daily review.
The performance cost is real and has to be sized. Every audited event means work in the kernel and a write to disk, and a badly thought-out rule can degrade the system noticeably. The dangerous rule par excellence is auditing all reads and writes of an active tree: the ingestor writes 720,000 readings a day to /var/lib/meteora/readings, so a -S read -S write rule on that directory would generate millions of events a day, fill the disk in hours and add latency to every operation. The correct criterion is to audit the rare, not the frequent: configuration changes, access to secrets, privilege elevations, module loading, mounts. And check the volume with aureport --summary during the first few days to tune it.
Log integrity against an attacker with privileges
We come to the most uncomfortable problem, and the one that has to be clear before anything else:
An attacker with root on
meteo-01can modify or delete any local log. They can edit/var/log/auth.log, purge the journal, stopauditd, remove achattr +awithCAP_LINUX_IMMUTABLEand rewrite history. A local log is never sufficient proof of anything.
That does not mean local defenses are useless; it means you have to understand what each one contributes.
| Defense | What it contributes | What it does not contribute |
|---|---|---|
chattr +a (04-06) |
Prevents truncation and modification; only appending is possible | Root can remove the attribute. It prevents accidents and forces a deliberate step |
Permissions 0640 meteora:adm |
Prevents reading and writing by other users | Nothing against root |
| journald FSS sealing | Detects tampering after the fact with an external key | It does not prevent it |
| Immediate remote shipping | The event is already off the machine before they can delete it | If shipping is batched, the window is lost |
| Collector with its own credentials | The server cannot delete what has already been sent | Requires separate infrastructure |
# 1. Append-only on the local logs
sudo chattr +a /var/log/meteora/meteo-api.log /var/log/auth.log
# 2. Cryptographic sealing of the journal (keep the key OFF the machine)
sudo journalctl --setup-keys --interval=1h
sudo journalctl --verify # detects whether the journal has been tampered with
# 3. Immediate shipping to the collector, over TCP with TLS
# /etc/rsyslog.d/60-remote.conf
# *.* action(type="omfwd" target="collector.meteora.example" port="6514"
# protocol="tcp" StreamDriverMode="1" StreamDriverAuthMode="x509/name"
# queue.type="LinkedList" queue.saveOnShutdown="on"
# action.resumeRetryCount="-1")The remote shipping configuration has four important decisions. TCP with TLS guarantees delivery and confidentiality, unlike UDP, which loses messages silently exactly when there is saturation — which is when incidents happen. StreamDriverAuthMode="x509/name" makes the sender verify the collector's identity, so nobody can impersonate it and absorb your logs. The on-disk queue with saveOnShutdown avoids losing events if the collector is unavailable for a while. And resumeRetryCount="-1" retries indefinitely.
And the three properties the collector must have for all of this to be worth anything:
Separate credentials. meteo-01 must be able to send events and not be able to read or delete them. If the server's certificate allowed administering the collector, an attacker with root on meteo-01 would delete the remote copy as well, and we would have gained nothing.
Immediate, append-only writing at the destination. The value of remote shipping is that the event leaves at the moment, before anyone can delete it; batched shipping every fifteen minutes leaves a perfectly exploitable fifteen-minute window.
Independent retention. The period is set by the collector, not by the originating server.
The underlying idea, worth stating explicitly: the evidence must get out of the attacker's reach as soon as possible. It is the same reasoning as the immutable backups of 05-03 and, incidentally, the reason why in section 11 the memory capture is taken before anything is touched.
Detecting changes and intruders on the host
File integrity checking with AIDE
AIDE computes a reference baseline with the checksums, permissions, owners and timestamps of every file you tell it about, and then compares against it periodically. It detects exactly what an attacker needs to do in order to persist: modify a binary, add a systemd unit, touch the configuration.
# /etc/aide/aide.conf (extract) Binary = p+i+n+u+g+s+m+c+md5+sha256 # everything, checksums included Config = p+i+n+u+g+s+m+c+sha256 LogFile = p+u+g+n+S # logs GROW: do not watch their size /usr/bin Binary /usr/sbin Binary /etc Config /etc/meteora Config /var/log/meteora LogFile !/var/lib/meteora/readings # the .dat files change nonstop: EXCLUDE !/var/log/journal
sudo aideinit # create the reference baseline
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
sudo aide --check # compareTwo decisions are what make AIDE work or turn it into useless noise. The rules by file type: for a binary you watch everything, but for a log that grows every second you cannot watch the size or the checksum, only permissions and owner — hence the LogFile rule with S, which tolerates growth but detects a truncation. And the exclusions: /var/lib/meteora/readings changes constantly by design, so including it would generate thousands of differences a day and nobody would ever read the report again.
And the critical point: the reference baseline must be stored off the machine, or at least on read-only media. If it lives in /var/lib/aide/aide.db and the attacker has root, they update it after making their changes and AIDE will report that everything is in order. The same goes for the setuid and capability reference lists of 05-02.
Rootkit detection and indicators of compromise
sudo apt install rkhunter chkrootkit
sudo rkhunter --update && sudo rkhunter --check --skip-keypress
sudo debsums -c # ALTERED package files
sudo dpkg --verify # the equivalent, built into dpkgThese tools compare binaries against known checksums and look for common patterns. They are useful and they have a limit you must know: if the compromise is in the kernel, the system's answers are not trustworthy, including the ones these tools receive. They are good for the common case, not for the sophisticated one.
The indicators you look for by hand, with the command and why it is suspicious:
# 1. Processes with NO binary on disk (the executable was deleted after starting)
sudo ls -l /proc/*/exe 2>/dev/null | grep -i deleted
# 2. Unexpected listening ports and established connections
sudo ss -tulnp ; sudo ss -tp state established
# 3. NEW setuid files or files with capabilities (05-02)
sudo find / -xdev -perm -4000 -type f 2>/dev/null | sort | diff /root/ref/suid.ref -
sudo getcap -r / 2>/dev/null | sort | diff /root/ref/caps.ref -
# 4. Unknown scheduled jobs and units
sudo ls -la /etc/cron.* /var/spool/cron/crontabs/ ; systemctl list-timers --all
systemctl list-unit-files --state=enabled | grep -v '@'
# 5. Recently modified files in system paths
sudo find /etc /usr/bin /usr/sbin -newermt '-3 days' -type f -ls 2>/dev/null
# 6. SSH keys added
sudo find /home /root -name authorized_keys -newermt '-30 days' -lsThe first one deserves an explanation because it is the most revealing of all: (deleted) in /proc/<pid>/exe means the program is running but its file no longer exists on disk. It is a classic evasion technique — the binary is executed and deleted immediately afterwards, so that no file system analysis finds it — and although it has legitimate explanations (a binary updated while the old process is still alive, exactly the case from 04-02), on a stable server it is an anomaly that has to be explained. And it has a lovely forensic consequence that we will use in section 11: the file still exists as long as the process lives, because its link count has not reached zero, and it can be recovered by copying /proc/<pid>/exe.
Centralized correlation
A single server produces events; an organization produces millions, spread across servers, firewalls, applications and cloud services. Centralized correlation — what is generically known as a SIEM, without getting into products — does three things no single host can do on its own: it normalizes different formats into a common schema, it correlates events from different sources — "a successful SSH login from an IP the firewall saw scanning ten minutes ago" — and it alerts on defined patterns. It requires the remote shipping of section 7 and event naming discipline like that of section 4; without those two things, it is an expensive text store nobody queries.
The incident response cycle
An incident is managed with a method, not by improvising. The six phases are the same in every reference framework:
graph LR
A["1. PREPARATION<br/>Before it happens"] --> B["2. DETECTION<br/>AND ANALYSIS<br/>What is going on?"]
B --> C["3. CONTAINMENT<br/>Stop it without destroying"]
C --> D["4. ERADICATION<br/>Remove the cause"]
D --> E["5. RECOVERY<br/>Return to service"]
E --> F["6. LESSONS<br/>LEARNED"]
F -.improve.-> A
| Phase | What is done | Typical mistake |
|---|---|---|
| 1. Preparation | Logging, backups, contacts, a written procedure, practice | Not having it: improvising under pressure |
| 2. Detection and analysis | Confirm, delimit the scope, preserve evidence | Acting before understanding; destroying the evidence |
| 3. Containment | Stop the damage without destroying the information | Shutting the server down immediately |
| 4. Eradication | Remove the root cause, not just the symptom | Killing the process and calling it closed |
| 5. Recovery | Restore service and watch closely | Going back to production without knowing if they are still inside |
| 6. Lessons learned | Concrete preventive measures, with an owner and a date | A document nobody reads |
The two mistakes that do the most damage belong to phase 3, and it is worth understanding them before the case study. Shutting the server down destroys all the volatile memory: processes, connections, encryption keys in RAM and the contents of deleted binaries that only exist through /proc. And just killing the suspicious process removes the evidence and solves nothing, because if there is a persistence mechanism — a scheduled job, a systemd unit, an SSH key — the process will be back in minutes and you will have lost the chance to observe it.
Case study: the 03:12 spike
Situation. It is 09:00 on 1 September 2026. Monitoring flagged a spike in writes to /var/lib/meteora between 03:12 and 03:40, twenty times the norm for that hour. There is a process called kworkerd that nobody on the team recognizes.
Phase 2: detection and analysis (without touching anything)
# 1. SNAPSHOT OF THE VOLATILE STATE — first, because it is what disappears soonest
ps auxf > /tmp/ev/ps.txt ; ss -tunap > /tmp/ev/net.txt
sudo lsof -n > /tmp/ev/lsof.txt ; date -Iseconds > /tmp/ev/time.txt
# 2. THE PROCESS: what it is and where it comes from
pgrep -a kworkerd # PID and command line
sudo ls -l /proc/<PID>/exe # does the binary exist?
# /proc/4471/exe -> /var/tmp/.cache/kworkerd (deleted) ← DELETED
sudo cat /proc/<PID>/environ | tr '\0' '\n' # environment
sudo ls -l /proc/<PID>/cwd ; sudo cat /proc/<PID>/status | grep -E 'Uid|PPid'
# Uid: 990 990 990 990 ← it runs as meteora
# PPid: 1 ← its parent died: it was "adopted" by init (02-01)
# 3. RECOVER THE BINARY before the process dies
sudo cp /proc/<PID>/exe /tmp/ev/recovered-binary
sha256sum /tmp/ev/recovered-binary > /tmp/ev/binary.sha256
# 4. THE TIME WINDOW in the logs
journalctl --since "2026-09-01 02:30" --until "2026-09-01 04:30" > /tmp/ev/journal.txt
sudo ausearch --start 09/01/2026 02:30:00 --end 09/01/2026 04:30:00 -i > /tmp/ev/audit.txt
sudo grep -E 'sshd|sudo|su\[' /var/log/auth.log | sed -n '/Sep 1 02:/,/Sep 1 05:/p'
last -F | head -20 ; sudo lastb -F | head -20 # successful and FAILED sessionsHow to read what you get, and why in this order. The snapshot of the volatile state goes first because of the order of volatility in the next section: processes and connections vanish as soon as anything changes, whereas the files will still be there in an hour's time. The (deleted) in /proc/<PID>/exe confirms the evasion technique of section 8, and the PPid: 1 indicates that the parent process has already died, which usually means it was launched and deliberately abandoned. The Uid: 990 is the most valuable piece of information in the whole block: the process runs as meteora, not as root, so the vector is almost certainly meteo-api or the ingestor, and the confinement of 05-03 tells us straight away how far it could have got. And the copy of the binary from /proc has to be made right now: if the process dies, the inode is freed and the binary is gone forever.
# 5. WHAT DID IT WRITE? Correlate with the data files
ls -la --time-style=full-iso /var/lib/meteora/readings/ | head -20
sudo find /var/lib/meteora -newermt '2026-09-01 03:00' ! -newermt '2026-09-01 04:00' -ls
sudo ausearch -k meteora_secrets --start 09/01/2026 -i # did it read the keys?
sudo ausearch -k escalation --start 09/01/2026 -i # did it try to go to root?
# 6. IS THERE PERSISTENCE?
sudo find / -xdev -perm -4000 -type f 2>/dev/null | sort | diff /root/ref/suid.ref -
systemctl list-unit-files --state=enabled | diff /root/ref/units.ref -
sudo ls -la /etc/cron.* /var/spool/cron/crontabs/
sudo find /home /root -name authorized_keys -newermt '-7 days' -ls
sudo aide --check | head -40Findings of the case. The process runs as meteora from a deleted binary in /var/tmp/.cache/. auditd shows that at 03:11:58 there was an execve from meteo-api's process tree, one minute before the spike. There are no events under meteora_secrets, so it never got to read the API keys. There are no events under escalation, and NoNewPrivileges=yes explains why. And aide --check flags no changes in /usr/bin or in /etc: the confinement of section 9 of 05-03 prevented writing outside the declared paths. The persistence is limited to a cron entry belonging to the user meteora that relaunches the binary every hour.
Phase 3: containment (stop it without destroying)
# a) ISOLATE THE NETWORK without shutting down: keeps memory and processes alive
sudo nft insert rule inet filter output ip daddr != 10.0.1.0/24 drop
# b) FREEZE the process instead of killing it: it stops acting and stays inspectable
sudo kill -STOP <PID>
# c) Cut off the persistence
sudo crontab -l -u meteora > /tmp/ev/cron-meteora.txt && sudo crontab -r -u meteora
# d) Preserve the data BEFORE any cleanup
sudo cp -a --preserve=all /var/lib/meteora /mnt/evidence/The four decisions, with their reasons. Isolating the network instead of shutting down stops exfiltration and remote command while preserving the whole volatile state, which is the most valuable evidence and the first to be lost. kill -STOP instead of kill -9: the process stops running but continues to exist, with its memory, its open descriptors and its /proc/<PID>/exe intact and available for analysis. Cutting off the persistence before anything else, because otherwise it will be back in less than an hour. And copying the data with -a --preserve=all to keep permissions, ACLs and timestamps (04-06), which are part of the evidence.
Phases 4 to 6
Eradication. Deleting the binary is not enough: you have to find how it got in. The execve at 03:11:58 from meteo-api's tree points to the application; with the correlation identifier of section 4 you locate the specific request that caused it and identify the bug. It is fixed, the dependencies are updated, and every credential the process could read is rotated — even though auditd says it did not read them, because the absence of an event is weaker proof than its presence.
Recovery. Restore the affected data from the backup (05-03) verifying the checksums, put the service back into production, and maintain heightened monitoring for weeks: specific alerts on execve by meteora, on out-of-hours writes and on new outbound connections. If there had been any indication of kernel compromise or of root access, the only defensible option would be to reinstall from scratch, because on a system with a compromised kernel you cannot trust anything the system says about itself.
Lessons learned. A meeting with no hunt for culprits, with a table of concrete measures, each with an owner and a date. For this case: noexec on /var/tmp (which would have prevented running the binary), SystemCallFilter without execve in meteo-api's unit (which would have prevented launching it), a review of the request parsing code, an automatic alert on execve with _UID=990, and cron disabled for service accounts.
Evidence preservation
If the incident may have legal consequences — a criminal complaint, insurance, an employment claim, notification to the data protection authority — the way the evidence is collected determines whether it will be worth anything.
The order of volatility dictates the collection sequence, from the most ephemeral to the most durable:
| Order | What | Lost when |
|---|---|---|
| 1 | CPU registers and caches | Instantly |
| 2 | RAM: processes, connections, keys, deleted binaries | On power-off |
| 3 | Network state: connections, ARP table | Within minutes |
| 4 | Running processes | On exit or reboot |
| 5 | Temporary file systems (/tmp, /run, /dev/shm) |
On reboot (04-03) |
| 6 | Disk | Persists |
| 7 | Backups and remote logs | Persist beyond the attacker's reach |
Why not shut down without thinking. A reboot destroys levels 1 to 5 of that table: all the memory, the active connections, the encryption keys that existed only in RAM, the contents of /tmp and /dev/shm — including /dev/shm/meteora-cache — and the deleted binaries that were only reachable through /proc. It is, by a wide margin, the fastest way to lose half the investigation. Shutting down is only justified if the ongoing damage outweighs the value of the evidence; in most cases, isolating the network contains things just as well and preserves everything.
# Memory dump (needs a specific tool, e.g. LiME) — BEFORE anything else
# Disk image: bit by bit, with verification, and always working on the COPY
sudo dd if=/dev/sda of=/mnt/evidence/meteo01-sda.img bs=4M status=progress conv=noerror
sha256sum /mnt/evidence/meteo01-sda.img | tee /mnt/evidence/meteo01-sda.sha256The checksum is what gives the image its value: it is computed when copying and computed again afterwards, and if it matches it proves the image has not been altered since it was taken. The analysis is always done on a working copy, mounted read-only, never on the original or on the affected system.
Chain of custody. This is the documentary record of who has had the evidence at every moment, and without it a technically impeccable piece of proof can be inadmissible. It must record, for each item: what it is and where it came from, who obtained it and when (with time zone), its checksum, where it is stored, and every transfer with date, people and reason. With restricted access and no gaps.
Legal warning, and it is an important one. A security incident can carry formal obligations with deadlines: under the GDPR, a breach affecting personal data requires notifying the supervisory authority within a very short period, and in certain cases the affected individuals as well. There may also be sector, contractual and insurance obligations. As soon as a breach involving personal data is suspected, involve legal counsel and the data protection officer immediately, and assess with them whether to report the matter to the competent authorities. Do not delete anything, do not negotiate with an attacker on your own, and do not make conclusions public before they are confirmed. Decisions about notification, evidence preservation and communication are not technical decisions.
Which preventive measures each incident leaves behind
An incident that produces no concrete measures is an incident that will happen again. This is the translation of the 03:12 case, and it serves as a model for any other:
| Finding from the incident | Preventive measure | Where it is implemented |
|---|---|---|
Binary executed from /var/tmp |
noexec on /var/tmp, /tmp and /dev/shm |
/etc/fstab (05-03) |
meteo-api was able to launch a process |
SystemCallFilter without execve; AppArmor with no execution rules |
systemd unit (05-01, 05-03) |
Persistence via meteora's cron |
cron disabled for service accounts |
/etc/cron.deny |
| Nobody detected it until 09:00 | Automatic alert on execve with _UID=990 |
Collector and alerting rules |
| The binary had been deleted | Alert on processes with exe (deleted) |
Periodic check |
| The bug was in the code | Review, sanitizers and dependency updates | Continuous integration (05-03) |
| The investigation was slow | A written and rehearsed procedure | Preparation phase |
Notice the pattern: most of the measures are configuration directives you already knew about. The difference between knowing them and having them applied is exactly what separates a contained incident from a complete compromise.
Common Mistakes and Tips
Not checking that the journal is persistent. Without /var/log/journal, journald stores in tmpfs and everything is lost on reboot. An attacker who reboots erases the log without touching a file. Verify it on every new server.
Logging passwords or tokens "for debugging". Password failures are usually typos on the right one, so the log ends up containing real credentials in the clear, readable by the adm group, replicated to the collector and present in the backups.
Retaining seven days. A compromise is detected weeks later. With seven days of retention, the logs from the moment of entry no longer exist and you will never know how they got in.
Trusting the local log after a compromise. An attacker with root modifies it. Send the events to a collector with different credentials and do it at the moment, not in batches.
Auditing too much with auditd. A rule on reads and writes of /var/lib/meteora would generate millions of events a day, fill the disk and add latency. Audit the rare, not the frequent, and measure with aureport --summary.
Shutting the server down when an incident is detected. It destroys the memory, the connections, /tmp and the deleted binaries reachable only through /proc. Isolate the network instead.
Killing the suspicious process. It removes the evidence and solves nothing: if there is persistence, it comes back in minutes. Use kill -STOP and look for the persistence mechanism.
Keeping AIDE's reference baseline on the machine itself. An attacker with root regenerates it after their changes and the report will come out clean. The same goes for the setuid and capability lists.
Forgetting create in logrotate. The new file may be born with the wrong owner or permissions, and the service can no longer write its own log. And without postrotate with reload, the process keeps writing to the old inode.
Tip: rehearse the response before you need it. An annual two-hour drill — "an unknown process shows up, what do we do and in what order?" — reveals that the phone numbers are missing, that nobody knows where the backups are and that the retention is seven days. Discovering that in a drill costs a morning; discovering it in a real incident costs vastly more.
Exercises
Exercise 1: auditing your own logging system
On a machine of your own or a virtual one: (a) check whether the journal is persistent and what its real retention is, and fix it if appropriate; (b) locate the last five failed authentication attempts and the last five uses of sudo, stating the command used in each case; (c) write three journalctl queries that use per-field filters instead of grep, and explain the advantage of each one; (d) calculate how much space it would take to retain 90 days of your current logs and decide on a justified retention policy, stating which aspects you would consult with compliance.
Exercise 2: designing Meteora's logging and auditing
Design the service's complete logging system: (a) the events meteo-api must record, with their fields, in structured format, and at least three that it must never record, with the reason; (b) the logrotate file for /var/log/meteora/, justifying each directive; (c) five auditd rules for meteo-01, explaining what each one detects and why you have not included a rule on writes to /var/lib/meteora/readings; and (d) the scheme for protecting log integrity, explaining what each layer contributes and what it does not contribute against an attacker who has obtained root.
Exercise 3: responding to an incident
At 08:15 you detect: meteo-api restarting in a loop every few minutes; /var/lib/meteora at 98% full when yesterday it was at 60%; a file /var/lib/meteora/readings/README_RECOVER.txt; and the .dat files of the last three days with the extension .dat.locked. Write the complete action plan by phases, with the exact commands in the right order and the justification for every decision. State explicitly: what you must not do and why; which evidence you preserve and in what order; when and why you involve legal counsel; and the five preventive measures this incident would leave behind.
Solutions
Solution 1
# (a) Persistence and retention
ls -ld /var/log/journal 2>/dev/null || echo "VOLATILE: lost on reboot"
journalctl --disk-usage ; journalctl --header | grep -i -A2 'sequential\|boot'
journalctl | head -1 # date of the OLDEST entry = real retention
sudo mkdir -p /var/log/journal && sudo systemctl restart systemd-journald # fix it
# (b) Failed authentication and elevations
sudo journalctl _COMM=sshd -p warning -n 20
sudo lastb -F | head -5
sudo journalctl _COMM=sudo -n 5 -o short-full
# (c) Three per-FIELD queries
journalctl _UID=990 --since "-24h" # everything done by the service's identity
journalctl _SYSTEMD_UNIT=ssh.service -p err # errors from one specific unit
journalctl _COMM=sudo _UID=1001 # elevations by one specific user(a) The real retention is not the configured one but the one that results from the space caps: if SystemMaxUse is reached before MaxRetentionSec, journald deletes the old entries and the effective window is shorter than you think. That is why the valid check is to look at the date of the oldest entry, not at the configuration file.
(c) The advantage of per-field filters over grep is twofold. They are precise: _UID=990 selects the events the kernel attributes to that UID, whereas grep 990 would also match a 990 appearing anywhere in a message. And they are unforgeable: the underscore fields are added by journald taking them from the system, not from the sender's text, so a process cannot lie about its UID or its unit, which it certainly can do with the message body.
(d) The calculation is journalctl --disk-usage divided by the current retention in days, multiplied by 90. The decision has to balance the investigation window — 90 days covers the realistic case of late detection — against data minimization, because the logs contain IP addresses and user identifiers, which are personal data. With compliance you must discuss: the legal basis for the processing, the maximum justifiable period, whether there are mandatory minimum periods in the sector, how access and erasure rights over the logs are handled, and who may read them.
Solution 2
(a) meteo-api events:
{"ts":"2026-09-01T03:12:07.481+02:00","event":"auth.failure","result":"denied",
"client":"CLI-4471","origin":"203.0.113.45","reason":"token_expired","trace":"7f3a9c21"}
{"ts":"2026-09-01T03:12:09.112+02:00","event":"query.ok","result":"success",
"client":"CLI-2210","resource":"/readings/2026-08-31","rows":720000,"ms":184,"trace":"9a1b4e77"}
{"ts":"2026-09-01T03:12:11.004+02:00","event":"config.reload","result":"success",
"actor":"uid=990","file":"/etc/meteora/meteora.conf","trace":"c2d8f013"}Always log: authentications (successful and failed, with the reason), data accesses with the resource and the volume, configuration changes, starts and stops, and errors with enough context to reproduce them.
Never, with the reason: passwords, failed ones included, because most failures are typos on the correct password and the log would end up containing real credentials in the clear; tokens and API keys, because whoever reads the log can reuse them directly — and the log is read by the adm group, travels to the collector and goes into the backups; and unnecessary personal data such as the client's name and email address, which are replaced by the identifier CLI-4471 plus a translation table with its own access control, applying the minimization principle.
(b) The logrotate file is the one from section 5. Justification per directive: daily + rotate 90 sets the investigation window at 90 days; compress reduces the space taken by .log files by roughly 10:1; delaycompress avoids compressing a file that may still be open; create 0640 meteora adm is essential because without it the new file may be born with owner root and the service would no longer be able to write; and postrotate with reload exists because after renaming, the process keeps writing to the same inode through its open descriptor (04-04), so without signalling it the new file stays empty.
(c) Five rules and what each one detects:
-w /etc/meteora/secrets.conf -p rwa -k meteora_secrets # 1. READING of the keys -w /etc/sudoers.d/ -p wa -k escalation # 2. new sudo rules -a always,exit -F arch=b64 -S execve -F euid=990 -k meteora_exec # 3. executions by the service -a always,exit -F arch=b64 -S init_module -S finit_module -k modules # 4. kernel modules -w /root/.ssh/ -p wa -k persistence # 5. root's SSH keys -e 2
(1) detects access to the secrets, and carries r because in a key file reading it is already the event. (2) detects the granting of new privileges. (3) is the one that would have detected the 03:12 incident as it happened, because meteo-api must not execute anything. (4) detects the step before a kernel rootkit. (5) detects one of the most common forms of persistence.
I do not include a rule on writes to /var/lib/meteora/readings because the ingestor writes 720,000 readings a day: it would generate millions of events a day, fill the disk in hours and add latency to the service's critical path, with practically no detection value, since those writes are normal operation. The criterion is to audit the rare, not the frequent.
(d) Log integrity:
| Layer | What it contributes | What it does NOT contribute |
|---|---|---|
chattr +a |
Prevents truncation and modification; forces a deliberate, auditable step | Root can remove it with CAP_LINUX_IMMUTABLE |
Permissions 0640 meteora:adm |
Isolates from other users and services | Nothing against root |
| FSS sealing | Detects tampering with a key stored elsewhere | It does not prevent it |
| Immediate remote shipping over TLS | The event is already out of the attacker's reach | Requires a collector; if batched, it leaves a window |
| Send-only credentials | The server cannot delete what has already been sent | Requires separate infrastructure |
The conclusion that organizes the table: only the last row really protects against an attacker with root, and the earlier ones have real value against accidents, against unprivileged users and for detecting tampering after the fact.
Solution 3
What you must NOT do, and why. Do not shut down or reboot: it would destroy the memory, the connections, /tmp and /dev/shm, and with them possible encryption keys in RAM that in some ransomware cases allow the data to be recovered without paying. Do not delete the encrypted files: they are evidence and are sometimes recoverable. Do not restore the backup immediately onto the same system: if the attacker is still inside, they will encrypt the restored copy too. Do not pay or negotiate on your own initiative: that is a management decision with legal implications. And do not kill the process before documenting it.
# --- PHASE 2: analysis and preservation, in order of volatility ---
mkdir -p /tmp/ev && date -Iseconds > /tmp/ev/time.txt
ps auxf > /tmp/ev/ps.txt ; ss -tunap > /tmp/ev/net.txt ; sudo lsof -n > /tmp/ev/lsof.txt
cat /var/lib/meteora/readings/README_RECOVER.txt | tee /tmp/ev/note.txt # only READ it
sudo find /var/lib/meteora -name '*.locked' -newermt '-24 hours' -ls > /tmp/ev/encrypted.txt
sudo journalctl --since "-24h" > /tmp/ev/journal.txt
sudo ausearch --start recent -i > /tmp/ev/audit.txt
sudo grep -E 'sshd|sudo' /var/log/auth.log > /tmp/ev/auth.txt
df -h ; sudo du -sh /var/lib/meteora/* # the 98% explains the restart loop
# --- PHASE 3: containment without destroying ---
sudo nft insert rule inet filter output ip daddr != 10.0.1.0/24 drop # isolate the network
sudo systemctl stop meteo-api ingestor aggregator # stop the service, NOT the machine
PID=$(pgrep -f '<suspicious process>') && sudo kill -STOP $PID
sudo mount -o remount,ro /var/lib/meteora # freeze the volume's stateJustification. The disk at 98% explains the restart loop: meteo-api cannot write and Restart=on-failure relaunches it; it is a symptom, not the cause. The ransom note and the .locked extensions confirm ransomware. The network is isolated to cut off communication with the attacker while preserving the volatile state; the service is stopped, since it no longer works and only adds noise; the process is frozen rather than killed so that it can be analyzed; and the volume is remounted read-only so that nothing else gets encrypted. The evidence is collected in order of volatility: memory and processes, then network, then temporary file systems, and finally the disk.
Legal counsel: immediately, and in parallel with the analysis. Ransomware almost certainly involves a personal data breach, with a possible obligation to notify the supervisory authority within a very short period, and perhaps the affected customers as well. On top of that, you have to assess reporting the matter to the competent authorities, contractual obligations towards customers and communication with the insurer. None of that is a technical decision and all of it has deadlines.
Recovery: rebuild the server from scratch — not clean up the existing one, because you cannot prove no persistence remains — restore from a backup predating the compromise verifying the checksums, apply the hardening of 05-03 before exposing it again, rotate every credential, and maintain heightened monitoring.
Five preventive measures: (1) immutable backups with separate credentials and retention enforced at the destination, which is the only real defense against ransomware; (2) a quarterly restore test with the time measured; (3) an alert on write volume outside the hourly pattern, which would have warned at 03:20 instead of 08:15; (4) noexec and ProtectSystem=strict to prevent execution from data paths; and (5) a written and rehearsed response procedure, with the phone numbers, the location of the backups and the steps in order, because at 08:15 in a real incident there is no improvising.
Conclusion
Without logging there is no detection, no investigation, no sensible containment and no learning: the preventive controls of the three previous lessons need detective controls alongside them, because prevention fails at some point and, when it fails silently, the attacker stays for weeks. In Linux, that logging has two faces: syslog, with its facilities and priorities — auth and authpriv are the first ones you look at — and its value as the industry's common language; and journald, binary and indexed, whose decisive advantage is that it adds the metadata itself and the sender cannot lie about it, which makes journalctl _UID=990 --since "-24h" worth more than any grep. The first thing to verify on a server is that the journal is persistent, because in /run it is lost entirely on reboot, and the second is that the real retention covers the investigation window you need — 90 days, not seven.
An application's logging is designed: timestamp with time zone, an event from a closed vocabulary, identity, origin, object, result always, correlation identifier and severity, in structured format. And with an exclusion list as important as the inclusion list: never passwords — failures are usually typos on the right one — never tokens or keys, and personal data minimized to identifiers with a separate translation table; all of it with the retention policy agreed in writing with compliance, because an IP address is personal data. Rotation with logrotate avoids filling the disk, and its two critical directives are create, which preserves permissions and owner, and postrotate with reload, because the process keeps writing to the old inode through its open descriptor. auditd adds what the kernel sees whether the program likes it or not — watching secrets.conf including reads, sudoers, privileged execve, modules and mounts, with -e 2 to make the rules immutable — with the golden rule of auditing the rare and not the frequent, because a rule on the ingestor's writes would generate millions of events a day.
On integrity, the statement to internalize is a hard one: an attacker with root modifies any local log, so a local log is never sufficient proof. chattr +a, the permissions and FSS sealing provide protection against accidents and detection after the fact; the only thing that really protects is immediate shipping to a collector with send-only credentials, because it takes the evidence out of the attacker's reach at the moment it is generated. On the host, AIDE detects changes in binaries and configuration — with rules by file type, sensible exclusions and the reference baseline off the machine — and the manual indicators cover the rest: processes with exe (deleted), unexpected ports, new setuid files and capabilities, unknown jobs and units, and recent SSH keys.
And the response cycle in six phases — preparation, detection and analysis, containment, eradication, recovery, lessons learned — with its two capital mistakes clearly identified: shutting the server down, which destroys the memory, the connections, /tmp and the deleted binaries reachable only through /proc, when isolating the network contains things just as well and preserves everything; and killing the process, when kill -STOP freezes it and leaves it inspectable. The 03:12 case showed the full method: a snapshot of the volatile state first because of the order of volatility, reading /proc/<PID>/ to discover the deleted binary and UID 990, recovering the binary from /proc before the process dies, the time window in journalctl and ausearch, the hunt for persistence against the reference lists, containment without destruction and, at the end, a table of concrete preventive measures with an owner and a date. Evidence preservation — order of volatility, an image with a checksum, always working on the copy, a chain of custody with no gaps — and the legal warning: faced with a breach involving personal data there are short notification deadlines and decisions that are not technical, so legal counsel and the data protection officer come in from the very first minute.
Wrapping up Module 5
It is worth looking at the whole route, because the module has had a very clear thread: from the mechanisms to the practice, and from prevention to detection.
We started with the framework (05-01): the distinction between protection — an internal, demonstrable mechanism — and security — a global property in the face of an adversary — which imposes the rule that no mechanism is a solution and all of them are layers. We saw the four concepts that describe any access control — subjects, objects, rights and protection domains, where the interesting part is the domain changes — the access matrix with its only two possible implementations — ACLs by columns and capabilities by rows, with the discovery that a file descriptor is a capability — the eight Saltzer and Schroeder principles applied one by one to Meteora, and the four models DAC, MAC, RBAC and ABAC. And the three mechanisms Linux layers on top of the nine bits: capabilities, which slice root's "all or nothing"; SELinux and AppArmor, which add a mandatory check not even root gets around; and seccomp, which reduces the syscall surface from 350 to 40. We closed with the TCB, the attack surface and the confused deputy.
Then we came down to identity (05-02): the UID as the real identity, the three UIDs of every process and the pattern of dropping privilege, the three /etc files field by field, the lifecycle of an account with its two risk points — the accumulation of privileges and the incomplete offboarding, where authorized_keys is what everyone forgets — the KDFs with salt and cost that protect passwords, PAM with its four stacks, SSH with a public key, and sudo with its subtlest lesson: a rule does not constrain what the user can do, but what they can execute.
Then the adversary and hardening (05-03): the catalog of threats with its observable traces and the four common axes — process, port, file, connection — the threat model as the exercise that gives you a criterion for prioritizing, the classes of vulnerability explained by their root cause, the system's defenses (ASLR, NX, canaries, PIE, RELRO) as layers that raise the cost but do not fix anything, and the ordered hardening of meteo-01: updates, minimum surface, a firewall with policy drop, isolation with systemd — the block with the most value per line written — mounting, limits, encryption, secrets and 3-2-1 backups with their restore test.
And this last lesson has answered how you know something has happened, and what to do then.
If Module 4 answered how something is stored so that it is still there tomorrow, Module 5 has answered how you guarantee that only those who should can touch it, and how you know whether anyone tried. The answer has always had the same shape: independent layers, each bounding a different surface, none sufficient on its own, and all of them verifiable with a specific command.
But notice what we have taken for granted for five whole lessons. We have protected meteo-01 as if it were one machine: one kernel, one file system, a set of processes that share everything except what we have separated by hand with permissions, capabilities, profiles and systemd directives. Every layer in this module has been, at bottom, an attempt to simulate isolation inside a system that is not isolated. ProtectSystem=strict pretends the file system is read-only; PrivateTmp pretends /tmp is private; IPAddressDeny pretends the network does not exist.
What if the isolation did not have to be pretended? What if meteo-api could run with its own file system, its own process table, its own network, without even seeing that the rest exists? What if the kernel itself could be duplicated, so that a total compromise of one operating system would not reach the one next door? That is no longer access control: it is isolation, and it is the next level of defense — as well as the foundation on which all modern cloud computing runs.
That is Module 6: Virtualization and Containers, and it begins with Virtualization: Hypervisors and Virtual Machines.
Operating Systems Fundamentals
Module 1: Introduction to Operating Systems
- Basic Concepts of Operating Systems
- History and Evolution of Operating Systems
- Types of Operating Systems
- Main Functions of an Operating System
- Kernel Architecture: Monolithic, Microkernel and Hybrid
- User Mode, Kernel Mode and System Calls
Module 2: Resource Management
- Process Management
- CPU Scheduling
- Memory Management
- Virtual Memory and Paging
- Storage Management
- Device Management
- Drivers, Interrupts and I/O Operations
Module 3: Concurrency
- Concurrency Concepts
- Threads and Processes
- Inter-Process Communication (IPC)
- Synchronization and Mutual Exclusion
- Classic Concurrency Problems
- Deadlocks: Prevention, Detection and Recovery
Module 4: File Structures
- File Systems
- Directory Structures
- Partitions, Mounting and the Virtual File System
- File Management
- Space Allocation, Journaling and Integrity
- File Security and Permissions
Module 5: System Protection and Security
- Protection Principles and Access Control
- Users, Authentication and Privilege Escalation
- Common Threats and System Hardening
- Auditing, Logging and Incident Response
Module 6: Virtualization and Containers
- Virtualization: Hypervisors and Virtual Machines
- Containers: Namespaces and cgroups
- The Operating System in the Cloud
- Mobile and Real-Time Operating Systems
