The NIST Cybersecurity Framework (CSF) is a voluntary framework for managing cybersecurity risk. Version 2.0, published in February 2024, organizes outcomes into six functions: Govern, Identify, Protect, Detect, Respond and Recover. In this tutorial you will translate those functions into concrete controls on an Ubuntu 24.04 server and collect the output as evidence, with each step labeled with the CSF 2.0 subcategory it supports.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges and SSH key authentication already working. - A second host reachable over SSH (SFTP) to store backups.
- A copy of the CSF 2.0 core (free at nist.gov) if you want to look up each subcategory in full.
How the CSF functions map to a Linux server
The CSF describes outcomes, not tools. The table shows how each function turns into work on a single server:
| Function | Goal | Controls in this guide |
|---|---|---|
| Govern (GV) | Set strategy, roles and policy | Written policy and ownership (outside the server) |
| Identify (ID) | Know your assets and risks | Package, service and port inventory |
| Protect (PR) | Limit the impact of an event | SSH hardening, firewall, automatic patching, permissions |
| Detect (DE) | Find events quickly | auditd, AIDE file integrity, fail2ban |
| Respond (RS) | Contain and analyze incidents | Evidence collection procedure |
| Recover (RC) | Restore normal operations | Encrypted backups with verified restores |
Govern is organizational: someone must own the risk, approve policies and decide the target profile. Record those decisions before you start, because they define which of the following controls are mandatory for you.
Step 1 - Building an asset inventory (Identify)
Subcategories ID.AM-01 and ID.AM-02 require inventories of hardware and of software and services. Create a directory for evidence that only root can read:
sudo install -d -m 700 /var/lib/nist-evidence
Record the installed packages with their versions:
dpkg-query -W -f='${Package}\t${Version}\n' | sudo tee /var/lib/nist-evidence/packages.tsv > /dev/null
wc -l /var/lib/nist-evidence/packages.tsv
612 /var/lib/nist-evidence/packages.tsv
Record the enabled services and the ports that accept connections. Every listening port is part of your attack surface:
systemctl list-unit-files --type=service --state=enabled --no-pager | sudo tee /var/lib/nist-evidence/services.txt > /dev/null
sudo ss -tulpn | sudo tee /var/lib/nist-evidence/listening.txt
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=512,fd=16))
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=880,fd=3))
Review each line and ask whether that service is needed. Anything you cannot justify is a risk to remove in the Protect step. Also look for binaries with the SUID bit, which run with the owner's privileges and are a common escalation path (ID.RA-01, vulnerabilities identified):
sudo find / -xdev -perm -4000 -type f 2>/dev/null | sudo tee /var/lib/nist-evidence/suid.txt
Compare this list with a fresh install; an unexpected entry deserves investigation.
Step 2 - Hardening access (Protect)
PR.AA-01 and PR.AA-05 cover managed identities and least-privilege access. Start by listing accounts that can log in:
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $7}' /etc/passwd
root /bin/bash
your_user /bin/bash
Lock any account on that list that has no current owner with sudo usermod -L -e 1 username. Then restrict SSH to key-based logins. Create a drop-in file; Ubuntu 24.04 reads sshd_config.d files alphabetically and the first value wins, so the 10- prefix overrides 50-cloud-init.conf:
sudo nano /etc/ssh/sshd_config.d/10-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
X11Forwarding no
LogLevel VERBOSE
Validate and reload:
sudo sshd -t
sudo systemctl reload ssh
Keep your current session open and confirm you can log in from a second terminal before closing it.
Step 3 - Protecting the network and keeping software patched (Protect)
PR.IR-01 asks that networks and environments be protected from unauthorized access. Allow only what the inventory justified. For a web server:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw enable
sudo ufw status verbose
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
80,443/tcp ALLOW IN Anywhere
PR.PS-02 requires software to be maintained. Enable automatic security updates:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Confirm the setting and see what is pending:
cat /etc/apt/apt.conf.d/20auto-upgrades
apt list --upgradable 2>/dev/null
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
For data at rest (PR.DS-01), check whether volumes are encrypted. A crypto_LUKS entry means the device is encrypted:
lsblk -f
If sensitive data lives on an unencrypted disk, plan a migration to a LUKS volume.
Step 4 - Monitoring activity with auditd (Detect)
DE.CM-03 and DE.CM-09 cover monitoring of personnel activity and of computing hardware and software. Install the audit daemon:
sudo apt install auditd
Create a rules file:
sudo nano /etc/audit/rules.d/nist-csf.rules
## Identity and privilege changes
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k privilege
-w /etc/sudoers.d/ -p wa -k privilege
## Remote access configuration
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
## Network configuration changes
-w /etc/netplan/ -p wa -k network
-a always,exit -F arch=b64 -S sethostname,setdomainname -k network
## Commands executed as root by logged-in users
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=unset -k root_cmd
Load the rules and check them:
sudo augenrules --load
sudo auditctl -l | wc -l
The count should match the number of rules in the file. Test one by adding a temporary user, then search the log:
sudo useradd audit-test && sudo userdel audit-test
sudo ausearch -k identity -i --start recent | tail -n 4
You should see PATH records for /etc/passwd with your username in the auid field.
Step 5 - Detecting unauthorized file changes with AIDE (Detect)
PR.DS-01 and DE.CM-09 also cover integrity. AIDE (Advanced Intrusion Detection Environment) stores hashes of system files and reports anything that changes. Install it:
sudo apt install aide
Build the initial database. This takes several minutes and should be done on a system you trust:
sudo aideinit
aideinit writes the database to /var/lib/aide/aide.db. Run a check to confirm it works:
sudo aide --check --config /etc/aide/aide.conf
AIDE found NO differences between database and filesystem. Looks okay!!
Change something harmless, such as sudo touch /etc/hosts, and run the check again: AIDE now reports the modified file. After planned changes (for example package upgrades), review the report and rebuild the baseline with sudo aideinit -y -f.
Step 6 - Blocking brute-force attempts with fail2ban (Detect)
DE.CM-01 asks you to monitor networks for adverse events. fail2ban watches the SSH log and bans addresses after repeated failures:
sudo apt install fail2ban
Create a local configuration so package updates do not overwrite your changes:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
backend = systemd
Enable the service and check the jail:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 3
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:
Step 7 - Preparing incident evidence collection (Respond)
RS.MA-01 (incident response plan executed) and RS.AN-03 (analysis performed) depend on collecting volatile data before it disappears. Write the procedure down now, not during an incident. Open a root shell so every command can write to the evidence folder:
sudo -i
Create a dated folder for the incident:
mkdir -p /root/incident-$(date +%F) && cd /root/incident-$(date +%F)
Capture sessions, processes and network state first, because they are lost on reboot:
w > sessions.txt
ps auxwwf > processes.txt
ss -tupan > connections.txt
Then capture logs and recent changes:
journalctl --since "-24h" -o short-iso > journal-24h.txt
ausearch --start today -i > audit-today.txt
find /etc /usr/local/bin -xdev -mtime -2 -type f > recent-changes.txt
aide --check --config /etc/aide/aide.conf > aide.txt
Finally, hash the evidence so you can prove it was not altered later, and leave the root shell:
sha256sum *.txt > SHA256SUMS
exit
Copy the directory to the backup host and do not reboot or reinstall the server until someone has decided on containment.
Step 8 - Automating verified backups (Recover)
PR.DS-11 requires backups to be created, protected, maintained and tested, and RC.RP-03 requires the integrity of backups to be verified before restoring. restic encrypts and deduplicates backups. Install it and create a password file:
sudo apt install restic
sudo mkdir -p /etc/restic
sudo sh -c 'openssl rand -base64 32 > /etc/restic/password'
sudo chmod 600 /etc/restic/password
Save the password in your password manager as well. Initialize the repository on the backup host, replacing backupuser and backup_host:
sudo restic -r sftp:backupuser@backup_host:/srv/restic/server1 --password-file /etc/restic/password init
Create the backup service:
sudo nano /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup
[Service]
Type=oneshot
Environment=RESTIC_REPOSITORY=sftp:backupuser@backup_host:/srv/restic/server1
Environment=RESTIC_PASSWORD_FILE=/etc/restic/password
ExecStart=/usr/bin/restic backup /etc /home /var/www /var/lib/nist-evidence
ExecStart=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
ExecStart=/usr/bin/restic check
And a daily timer:
sudo nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer, run a first backup and check the result:
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
sudo systemctl start restic-backup.service
sudo journalctl -u restic-backup.service -n 5 --no-pager
The last lines should include no errors were found. Test a restore of /etc into a temporary directory, which is what RC.RP-03 really asks for:
sudo restic -r sftp:backupuser@backup_host:/srv/restic/server1 --password-file /etc/restic/password restore latest --target /tmp/restore-test --include /etc
sudo ls /tmp/restore-test/etc | head -n 3
sudo rm -rf /tmp/restore-test
Control mapping reference
Use this table in your CSF profile to link each subcategory to its evidence:
| CSF 2.0 subcategory | Control on the server | Evidence |
|---|---|---|
| ID.AM-01, ID.AM-02 | Package, service and port inventory | /var/lib/nist-evidence/*.txt |
| ID.RA-01 | SUID review, pending updates | suid.txt, apt list --upgradable |
| PR.AA-01, PR.AA-05 | Named accounts, no root SSH, key-only login | sshd -T, /etc/passwd review |
| PR.IR-01 | UFW default deny | ufw status verbose |
| PR.PS-02 | Unattended security upgrades | 20auto-upgrades |
| PR.DS-01 | LUKS volumes, AIDE | lsblk -f, AIDE reports |
| PR.DS-11, RC.RP-03 | restic backups with check and restore test | journal of restic-backup.service |
| DE.CM-01 | fail2ban on SSH | fail2ban-client status sshd |
| DE.CM-03, DE.CM-09 | auditd rules | ausearch and aureport output |
| RS.MA-01, RS.AN-03 | Evidence collection procedure | Incident folders with SHA256SUMS |
Troubleshooting
AIDE reports hundreds of changes after every upgrade. That is expected: packages replace files. Review the report, then rebuild the baseline with sudo aideinit -y -f. To ignore a volatile path, add a line such as !/var/log/myapp to a file in /etc/aide/aide.conf.d/.
Audit logs fill the disk. Edit /etc/audit/auditd.conf, set max_log_file = 50, num_logs = 10 and max_log_file_action = ROTATE, then run sudo service auditd restart.
fail2ban shows no failures. Make sure the jail uses backend = systemd; Ubuntu 24.04 logs SSH to the journal and the jail will not see events from a log file path that does not match.
Conclusion
You mapped each CSF 2.0 function to working controls on Ubuntu 24.04: an asset inventory, SSH and firewall hardening, automatic patching, audit and integrity monitoring, a written evidence procedure and backups that are verified by restore. Next, document your current and target profiles under the Govern function, forward auditd and fail2ban events to a central SIEM, and review the evidence directory on a fixed schedule, for example quarterly.
