Module 3 closed by pointing a finger at a wall: the crontab line full of escapes, the report pipeline that no longer fitted on the screen, and that unbearable dependency on you remembering every step at three in the morning. This module tears that wall down with the simplest idea in the world: a script is nothing more than a text file containing the commands you already know how to type.
There is no new language to learn: grep, awk, find, curl and df are still the same. What changes is that they stop living in your memory and start living in a file with a name, permissions, an author and a date; a file that can be run identically a thousand times and run on its own while you sleep. This lesson lays the foundations and ends with health_check.sh, the first script of the running thread, which the next six lessons will keep improving.
Contents
- Does this deserve to be a script?
- What exactly a script is
- The shebang: who interprets your file
- The three ways to run a script
- Execute permissions
- Where scripts live, and the PATH
- Anatomy of a decent script
- Useful comments and useless comments
- Exit codes
- First real script:
health_check.sh - Style and tooling:
shellcheckandshfmt
- Does this deserve to be a script?
Writing a script has a cost: it has to be maintained, versioned, documented, and you have to remember it exists. Before you open the editor, place what you want to do somewhere on this scale.
| Form | When | Advantage | Limit |
|---|---|---|---|
| One-liner / alias | You do it once, or it is a fixed daily command | Zero cost, or one line in .bashrc |
Lost when the session closes; an alias cannot take arguments in the middle |
| Shell function | Short logic with arguments, just for you | Lives in your environment, no subprocess | Neither cron nor another user can run it |
| Script | A repeatable procedure, with steps and decisions | Versionable, shareable, schedulable | Bash gets uncomfortable beyond ~300 lines |
| Ansible | Configuring machines, and more than one | Idempotency and inventory out of the box | Requires infrastructure and learning |
| Programming language | Data structures, JSON, concurrency, APIs | Types, tests, libraries | Dependencies to install and maintain |
The practical rule: if you have done it three times by hand, or if somebody who is not you is going to run it, or if cron is going to run it, it is a script. And if on top of that it starts handling nested data or talking to JSON APIs, the alarm is going off and it is time to look towards Python; we will come back to that in 04-07. The health check Marta asks for every morning meets all three criteria at once: it is a script.
- What exactly a script is
A plain text file with commands that the interpreter reads and executes in order. It is not compiled, it is not linked, it produces no binaries.
operator@srv-tramontana:~$ printf 'echo "Hello"\ndate +%%F\n' > /tmp/first.sh
operator@srv-tramontana:~$ bash /tmp/first.sh
Hello
2026-08-18bash opened the file, read the first line, ran it as if you had typed it, and moved on to the second. That complete equivalence between "what you type" and "what goes in a script" is the reason this module will feel natural to you: you already know how to write the content, all that was missing was the container.
- The shebang: who interprets your file
If the file is run on its own, the kernel needs to know which program should interpret it: that is what the first two characters, #!, followed by the path to the interpreter, say. When you run ./script.sh, the kernel reads those magic bytes and actually starts /bin/bash ./script.sh. Without a shebang the result depends on where you are, so never leave it to chance.
| Shebang | What it does | When to use it |
|---|---|---|
#!/bin/bash |
Fixed path to Bash | Linux servers, where Bash is always there |
#!/usr/bin/env bash |
Looks for bash in the PATH |
Portability: macOS, BSD, Bash in /usr/local |
#!/bin/sh |
The system's POSIX shell | Scripts that must run on any Unix, with no extensions |
In this course we will use #!/usr/bin/env bash: it costs exactly the same and it saves you the day you run the script on a macOS where modern Bash lives in /opt/homebrew/bin/bash.
Why /bin/sh is not Bash on Ubuntu
On Ubuntu and Debian, ls -l /bin/sh reveals a link to dash, a tiny shell that implements POSIX and nothing else. It was chosen that way because there are thousands of boot scripts and dash runs them much faster. The consequence is that everything Bash-specific fails:
operator@srv-tramontana:~$ cat > /tmp/test_sh.sh <<'EOF'
#!/bin/sh
if [[ "bookings" == book* ]]; then
echo "matches"
fi
EOF
operator@srv-tramontana:~$ bash /tmp/test_sh.sh
matches
operator@srv-tramontana:~$ sh /tmp/test_sh.sh
/tmp/test_sh.sh: 2: [[: not foundThe same file, two interpreters, two results. [[ ]] belongs to Bash, and so do arrays, ${VAR^^}, local -n and <<<. And notice the cruel detail: the script did not fail when you wrote it, it failed when it was run with the wrong interpreter, possibly weeks later and inside cron. If you write #!/bin/sh, do not use anything from Bash.
- The three ways to run a script
There are three of them — bash script.sh starts a new Bash that reads the file, ./script.sh lets the kernel read the shebang, and source script.sh (or . script.sh, the POSIX form) has the current shell run it — and the difference is not cosmetic: it changes which process your commands run in.
flowchart LR
A["Your interactive shell<br/>PID 3120, cwd=/home/operator"] -->|"bash script.sh<br/>./script.sh"| B["Bash subprocess<br/>PID 3455, a copy of the environment"]
B -->|"when it ends, it dies"| C["The original shell<br/>has not changed"]
A -->|"source script.sh"| D["Same process, PID 3120<br/>cd, variables and functions persist"]
The classic demonstration, a script that changes directory:
operator@srv-tramontana:~$ cat > /tmp/go_releases.sh <<'EOF'
#!/usr/bin/env bash
cd /opt/tramontana/releases
echo "Inside the script I am in: $PWD"
EOF
operator@srv-tramontana:~$ chmod +x /tmp/go_releases.sh
operator@srv-tramontana:~$ /tmp/go_releases.sh
Inside the script I am in: /opt/tramontana/releases
operator@srv-tramontana:~$ pwd
/home/operator
operator@srv-tramontana:~$ source /tmp/go_releases.sh
Inside the script I am in: /opt/tramontana/releases
operator@srv-tramontana:~$ pwd
/opt/tramontana/releasesThe script did change directory... in its own process, which no longer exists. A child inherits a copy of its parent's environment, but nothing the child changes comes back to the parent: not the directory, not the variables, not the functions. It is the one-way inheritance from 03-01; with source there is no new process and that is why the cd persists.
| Form | New process? | Does it need chmod +x? |
Does it respect the shebang? | Effects that persist |
|---|---|---|---|---|
bash script.sh |
Yes | No | No (bash decides) |
None |
./script.sh |
Yes | Yes | Yes | None |
source script.sh |
No | No | No | cd, variables, functions |
Two practical rules. Scripts are run with ./ or by their name, the only form that respects the shebang and the one cron will use. And source is reserved for configuration and function libraries, which is exactly what we will use it for in 04-05. One warning: since source creates no process, an exit 1 inside a file you source in your terminal closes your session. It has happened to everybody once.
- Execute permissions
A text file is not executable until you tell the system so. Picking up 02-07 again:
operator@srv-tramontana:~/scripts$ ./health_check.sh
-bash: ./health_check.sh: Permission denied
operator@srv-tramontana:~/scripts$ chmod +x health_check.sh && ls -l health*
-rwxr-x--- 1 operator operator 512 Aug 18 09:14 health_check.shchmod +x respects the umask 027 from Module 2: it adds x for the user and the group, not for others; chmod 750 says the same thing without depending on the umask. Two nuances: the x bit does not mean "this is machine code" but "allow me to try to execute it" — what decides how to interpret it is the shebang — and bash script.sh works without the x bit, because there the file is merely read, which is the convenient way to try out something half-written.
- Where scripts live, and the PATH
Typing /home/operator/scripts/health_check.sh every morning guarantees nobody will use it. The solution is the PATH, which you already dissected in 03-01.
| Location | For whom | Notes |
|---|---|---|
~/scripts |
You, with an explicit path | Where the source code lives in this course |
~/bin or ~/.local/bin |
Your user only | Ubuntu adds it to the PATH if it exists at login |
/usr/local/bin |
All users | The right place for local system scripts (/usr/bin belongs to the package manager: never put anything of yours there) |
operator@srv-tramontana:~$ mkdir -p ~/bin ~/scripts
operator@srv-tramontana:~$ ln -s ~/scripts/health_check.sh ~/bin/health-check
operator@srv-tramontana:~$ source ~/.profile && which health-check
/home/operator/bin/health-checkWe use a symbolic link instead of moving the file: the code stays in ~/scripts (versioned in git, in 04-07) and ~/bin merely publishes a convenient name, with no extension and with hyphens, like Unix commands. The Module 3 rule still stands: never . in the PATH, because it would be enough for somebody to leave a file called ls in a shared directory for you to run their code without knowing it.
- Anatomy of a decent script
This is the template you are going to start all your scripts with from today. Every line avoids a specific problem you will meet during the module.
#!/usr/bin/env bash
#
# name.sh - One line saying what it does.
#
# Author : Full Name <email>
# Date : 2026-08-18
# Usage : name.sh [options] <argument>
#
# A somewhat longer description if needed: what it checks, what it modifies,
# what it writes and where.
set -euo pipefail
# --- Constants ------------------------------------------------------------
LOG_DIR="/var/log/tramontana"
DISK_THRESHOLD=80
# --- Body -----------------------------------------------------------------
# ... the real work goes here ...
exit 0- Header. Name, purpose, author, date and usage: when you find this file at four in the morning eight months from now, those five lines will be worth more than everything else.
set -euo pipefail. Strict mode:-eaborts if a command fails,-uaborts if you use an undefined variable, andpipefailmakes a pipeline fail if any of its stages fails, not just the last one (we announced it in 03-04). Put it in everything you write from today; the detailed why and — above all — where it does not work is the heart of 04-06.- Constants in UPPERCASE, lowercase for working variables, and all of them grouped at the top: changing a threshold must not force you to read the whole script.
- An explicit
exit 0. Without it, the script returns the code of the last command, which can be anything.exit 0says "I got this far and everything went well", and that is what cron and the monitoring read.
- Useful comments and useless comments
Everything after # is ignored to the end of the line, except the shebang. But commenting a lot is not the same as commenting well.
# USELESS: repeats what the code already says
usage=$(df --output=pcent / | tail -1) # assigns the disk usage to usage
# USEFUL: documents a decision that looks like a mistake
# We do not use a plain 'grep -c' because it returns 1 when there are no
# matches, and with 'set -e' that would abort the script on a quiet day.
errors=$(grep -c "^$today" "$ERROR_LOG" || true)The rule: the code says what it does; the comment says why. If you need a comment to understand what a line does, the fix is usually to rewrite it or to extract it into a well-named function (04-05).
- Exit codes
Every process finishes returning an integer from 0 to 255. You already know it as $?; now you are the one producing it, and that changes the responsibility: the exit code is the only part of your script another program is going to read. The convention is the opposite of the intuitive one — 0 is success — and it makes sense: there is only one way to get it right and many ways to fail, each with its own number.
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Generic error |
2 |
Incorrect usage: missing arguments, unknown option |
126 / 127 |
It exists but could not be executed (missing x bit) / command not found (typically, cron's PATH) |
128+N |
Terminated by signal N: 130 = Ctrl+C, 143 = SIGTERM, 137 = SIGKILL |
(And exit -1 ends up as 255, out of range.) exit with no argument returns the code of the last command: be explicit. And avoid inventing codes above 125, which clash with the reserved ones; the 3-125 range is more than enough for your own meanings, documented in the header. In 04-03 we will formalise a table for our scripts.
- First real script:
health_check.sh
health_check.shMarta asks for the same thing every morning: "is the site up, is there disk space left, and were there any errors overnight?". We write it exactly like that, as a run of commands, applying the template from the previous section.
#!/usr/bin/env bash
#
# health_check.sh - Quick health check for Tramontana Bookings.
#
# Author : Systems operator <operator@srv-tramontana>
# Date : 2026-08-18
# Usage : health_check.sh
#
# Checks three things: that the application answers on port 8080, how much
# disk is left on the root filesystem, and how many errors were logged today.
# It modifies nothing: it is read-only and safe to run in production.
set -euo pipefail
# --- Constants ------------------------------------------------------------
HEALTH_URL="http://10.0.2.15:8080/health"
ERROR_LOG="/var/log/tramontana/errors.log"
DISK_THRESHOLD=80
MAX_WAIT=5
# --- Body -----------------------------------------------------------------
echo "=== Tramontana Bookings health check ==="
echo "Server : $(hostname)"
echo "Date : $(date '+%F %T')"
echo
# -s silences the progress bar, -o /dev/null throws away the response body,
# -w prints only the code, --max-time stops it hanging.
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
--max-time "$MAX_WAIT" "$HEALTH_URL" || true)
echo "HTTP 8080 : $http_code"
# --output=pcent gives only the percentage column; tail -1 skips the
# "Use%" header; tr -dc '0-9' leaves a clean number to compare.
disk_usage=$(df --output=pcent / | tail -1 | tr -dc '0-9')
echo "Disk / : ${disk_usage}% used (threshold ${DISK_THRESHOLD}%)"
# grep -c returns 1 when it finds nothing and with set -e that would abort
# the script on precisely the day everything is fine. Hence the || true.
today=$(date '+%Y-%m-%d')
errors_today=$(grep -c "^$today" "$ERROR_LOG" || true)
echo "Errors today: $errors_today"
exit 0operator@srv-tramontana:~$ chmod 750 ~/scripts/health_check.sh
operator@srv-tramontana:~$ ~/scripts/health_check.sh; echo "code: $?"
=== Tramontana Bookings health check ===
Server : srv-tramontana
Date : 2026-08-18 09:22:41
HTTP 8080 : 200
Disk / : 30% used (threshold 80%)
Errors today: 6
code: 0It is already infinitely better than three commands in the history. But it is honestly bad, and it is worth seeing why from the start, because that list is the table of contents for the rest of the module:
- The constants are inside the script and it accepts no options: no
-h, no different threshold, no quiet mode (04-02 and 04-03). - It prints the threshold but compares it against nothing. There is no decision at all: if the disk were at 95% it would calmly say "95%" (04-04).
- Three almost identical blocks that are crying out to be functions (04-05), and if
errors.logdoes not exist the script dies halfway through with an ugly message (04-06). - It always returns 0, so no monitoring system can use it (04-07).
Save it as it is: at the end of the module you will compare it with its final version.
- Style and tooling:
shellcheck and shfmt
shellcheck and shfmtBash has a syntax full of traps and an interpreter that almost never complains: write $file without quotes and it will work for months, until the day a name with a space turns up. Static analysis is not optional in shell.
operator@srv-tramontana:~$ sudo apt install -y shellcheck shfmt
operator@srv-tramontana:~$ printf '#!/usr/bin/env bash\ndir=/opt/tramontana/releases\ncd $dir\nrm -f $1\n' > /tmp/bad.sh
operator@srv-tramontana:~$ shellcheck /tmp/bad.sh
In /tmp/bad.sh line 3:
cd $dir
^--^ SC2086: Double quote to prevent globbing and word splitting.
^-----^ SC2164: Use 'cd ... || exit' in case cd fails.
In /tmp/bad.sh line 4:
rm -f $1
^-- SC2086: Double quote to prevent globbing and word splitting.Three warnings in four lines and all three are time bombs: without quotes, a directory with spaces is split into two arguments; and if the cd fails, the rm -f runs in whatever directory you happened to be in. That is SC2164. Every SCxxxx code has its page in the ShellCheck wiki; in 04-06 we will pick apart the four most frequent ones. The module's rule from today: no script is finished until shellcheck has nothing to say.
operator@srv-tramontana:~$ shellcheck ~/scripts/health_check.sh && echo "Clean"
Clean
operator@srv-tramontana:~$ shfmt -i 4 -d ~/scripts/health_check.sh # -d: the diff only
operator@srv-tramontana:~$ shfmt -i 4 -w ~/scripts/health_check.sh # -w: rewritesshfmt formats instead of criticising: -i 4 sets four spaces, -d shows what it would change without touching anything (the course's --dry-run habit) and -w writes. On style, three decisions we will not discuss again: four spaces of indentation (never tabs, which break when copied), lines under 100 characters split with \, and one statement per line.
Common Mistakes and Tips
- Writing the script on Windows and uploading it. CRLF line endings make the shebang
#!/usr/bin/env bash\r, and the error,bad interpreter: No such file or directory, tells you nothing. Diagnose it withfile script.shand fix it withdos2unix. - Leaving lines before the shebang. It must be the first line, starting in column 1. And do not call your script
test.sh:testis a builtin and the name clash causes headaches. - Running a script with a Bash shebang using
sh script.sh. The shebang is ignored when you invoke the interpreter by hand: it is the number one cause of "it works in my terminal and fails on the server". - Expecting a script to change your directory or your variables. It cannot: it is another process. If you genuinely need that, it is a function or a file to
source. And do not forget the finalexit 0: if the last line is agrepwith no matches, your perfectly correct script returns 1 and cron reports a failure that never happened. - Tip: while developing,
bash -n script.shchecks the syntax without running anything; it costs a second and saves you from discovering a missingfihalfway through a deployment. Also integrateshellcheckinto your editor, and start every script by copying the template from section 7.
Exercises
Exercise 1. Demonstrate empirically, with a single file and without editing it between tests, the difference between bash script.sh, ./script.sh and source script.sh. The script must define a variable, change directory and print its own PID. Explain what you observe in each case and why.
Exercise 2. Write ~/scripts/release_info.sh: which version of Tramontana is active (resolving /opt/tramontana/app), how much that release takes up and when it was deployed. Apply the complete template, leave it clean under shellcheck and publish it in ~/bin as release-info.
Exercise 3. Luis has left this script on the server. Find four distinct problems without running it, justify each one and rewrite it.
#! /bin/sh
# cleans temp files
cd /srv/tramontana/backups/temp
rm -rf *
if [[ $? == 0 ]]; then echo ok; fiSolutions
Solution 1. $$ holds the PID of the shell running the code: it is the witness that gives away whether there was a new process.
operator@srv-tramontana:~$ cat > /tmp/demo_scope.sh <<'EOF'
#!/usr/bin/env bash
echo "Script PID : $$"
MY_VARIABLE="defined inside"
cd /opt/tramontana/releases
echo "Directory inside : $PWD"
EOF
operator@srv-tramontana:~$ chmod +x /tmp/demo_scope.sh; echo "My shell: $$"
My shell: 3120
operator@srv-tramontana:~$ /tmp/demo_scope.sh
Script PID : 3456
Directory inside : /opt/tramontana/releases
operator@srv-tramontana:~$ echo "$PWD - ${MY_VARIABLE:-(does not exist)}"
/home/operator - (does not exist)
operator@srv-tramontana:~$ source /tmp/demo_scope.sh
Script PID : 3120
Directory inside : /opt/tramontana/releases
operator@srv-tramontana:~$ echo "$PWD - ${MY_VARIABLE:-(does not exist)}"
/opt/tramontana/releases - defined insidebash /tmp/demo_scope.sh gives the same result as ./: a PID different from your shell's, therefore a new process, and the cd and the variable die with it. Only the mechanism changes: bash file starts Bash explicitly and ignores the shebang, whereas ./file makes the kernel read the shebang and decide — and that is why that case, and only that case, requires the x bit. The third prints the same PID, 3120: there is no new process, your own Bash ran it, and that is why everything persists. The ${MY_VARIABLE:-(does not exist)} stops echo printing an ambiguous blank; you will see it in detail in 04-02.
Solution 2.
#!/usr/bin/env bash
#
# release_info.sh - Reports the active release of Tramontana Bookings.
#
# Author : Systems operator <operator@srv-tramontana>
# Date : 2026-08-18
# Usage : release_info.sh (read-only)
set -euo pipefail
APP_LINK="/opt/tramontana/app"
# readlink -f resolves the link to its final target as an absolute path,
# necessary because 'app' is a *relative* link to the active release.
release_path=$(readlink -f "$APP_LINK")
version=$(basename "$release_path")
size=$(du -sh "$release_path" | cut -f1)
# stat on the LINK (not on its target) gives when it was changed, which is
# exactly when it was deployed. cut removes the fraction of a second.
deployed=$(stat -c '%y' "$APP_LINK" | cut -d'.' -f1)
printf 'Active release : %s\nPath : %s\n' "$version" "$release_path"
printf 'Size : %s\nDeployed : %s\n' "$size" "$deployed"
exit 0operator@srv-tramontana:~$ chmod 750 ~/scripts/release_info.sh
operator@srv-tramontana:~$ shellcheck ~/scripts/release_info.sh && echo "Clean"
Clean
operator@srv-tramontana:~$ ln -s ~/scripts/release_info.sh ~/bin/release-info
operator@srv-tramontana:~$ release-info
Active release : 3.2.1
Path : /opt/tramontana/releases/3.2.1
Size : 98M
Deployed : 2026-08-11 17:42:03Solution 3. The four problems, from most to least serious:
- An unchecked
cdfollowed byrm -rf *. The lethal flaw: if the directory does not exist or you have no permission, thecdfails, the script carries on wherever you were and deletes everything there. That is SC2164, and it is fixed withcd ... || exitor with strict mode. #! /bin/shwith[[ ]]. Two errors: the space after#!is accepted by Linux but not by every Unix, and above all/bin/shis dash, which knows nothing about[[ ]]. The script dies on the last line with[[: not found.if [[ $? == 0 ]]. Redundant and fragile: in Bash you branch directly on the command (if rm -rf ...; then), as we will see in 04-04. Besides,==compares strings where a numeric test should be used.- Everything else is missing: no header, no strict mode, no explicit
exit, and the comment "cleans temp files" does not say what it considers temporary or from when. Anrm -rf *deserves more explanation than any other line in the file.
The rewritten version also does what the name promises — delete what is old, not everything:
#!/usr/bin/env bash
#
# clean_temp.sh - Deletes backup temp files older than 7 days.
# Author : Systems operator <operator@srv-tramontana> Date: 2026-08-18
set -euo pipefail
TEMP_DIR="/srv/tramontana/backups/temp"
RETENTION_DAYS=7
# -mindepth 1 does not touch the directory itself; -xdev does not cross
# mount points; -delete avoids depending on a glob that could be left
# unexpanded and try to delete a file literally called *.
find "$TEMP_DIR" -mindepth 1 -xdev -type f \
-mtime +"$RETENTION_DAYS" -delete
echo "Temp files older than ${RETENTION_DAYS} days removed."
exit 0There is no cd any more, so the danger disappears at the root, and strict mode aborts if find fails.
Conclusion
You have the foundations, and there are more of them than it looks.
- You know when to write a script and when an alias, a function or Ansible is enough, with the three-repetitions rule as your criterion.
- You understand that a script is plain text with commands, you have mastered the shebang and why
#!/usr/bin/env bashis the default choice, and you know the trap that on Ubuntu/bin/shis dash and[[ ]]does not exist there. - You distinguish the three ways of running a script and why only
sourceaffects your shell; you put scripts in~/scripts, publish them with a link in~/binand still keep.out of the PATH. - You start every script with the same template — shebang, header with usage,
set -euo pipefail, constants in uppercase, body andexit 0— you comment the why, you return exit codes with meaning and you passshellcheckbefore considering anything finished.
health_check.sh already exists and already works, but it has its constants nailed into the code, it compares nothing and it cannot say whether the result is good or bad. In the next lesson, Variables and Data Types, we attack the first shortcoming: you will see that in Bash everything is a string and what that implies, you will master parameter expansion — that collection of ${VAR#...}, ${VAR:-...} and ${VAR//.../...} that turns ten lines into one — you will learn why Bash cannot add decimals and what to do with the average amount in bookings.csv, and you will count bookings per house with associative arrays without leaving the shell. By the end of it, health_check.sh will have its configuration outside the body of the script.
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
