# NIST 800-53 Hardened Ubuntu 24.04 - Proxmox VM Template A hardened Ubuntu 24.04 LTS VM template for Proxmox VE, aligned with NIST 800-53 security controls and validated against CIS Ubuntu 24.04 benchmarks via Wazuh SCA. The template is built through three scripts run in sequence on a fresh Ubuntu 24.04 Server (minimal) installation. Once complete, the VM is converted to a Proxmox template for rapid, secure clone deployment. --- ## Table of Contents - [Architecture Overview](#architecture-overview) - [Prerequisites](#prerequisites) - [VM Creation in Proxmox](#vm-creation-in-proxmox) - [SSH Key Setup (Windows)](#ssh-key-setup-windows) - [Script 1 - partition-layout.sh](#script-1--partition-layoutsh) - [Script 2 - nist-800-53-harden-v2.sh](#script-2--nist-800-53-harden-v2sh) - [Script 3 - remediate.sh](#script-3--remediatesh) - [Deployment Workflow](#deployment-workflow) - [Post-Template: Cloning](#post-template-cloning) - [Firewall Management](#firewall-management) - [NIST 800-53 Control Mapping](#nist-800-53-control-mapping) - [Wazuh SCA Check Coverage](#wazuh-sca-check-coverage) - [Known Exceptions](#known-exceptions) - [Maintenance](#maintenance) - [Troubleshooting](#troubleshooting) --- ## Architecture Overview ``` ┌──────────────────────────────────────────────────────────┐ │ Proxmox VM │ ├──────────────┬───────────────────────────────────────────┤ │ Disk 1 │ Disk 2 (LVM - vg_nist) │ │ /dev/sda │ /dev/sdb │ │ 64 GB │ 50 GB │ │ │ │ │ sda1: EFI │ lv_var ............ /var (10 GB) │ │ sda2: /boot │ lv_var_log ........ /var/log (8 GB) │ │ sda3: LVM │ lv_var_log_audit .. /var/log/audit (4 GB)│ │ └─ / │ lv_home ........... /home (8 GB) │ │ │ lv_tmp ............ /tmp (2 GB) │ │ │ lv_var_tmp ........ /var/tmp (2 GB) │ │ │ [~16 GB free for future growth] │ ├──────────────┴───────────────────────────────────────────┤ │ Cloud-Init Drive (auto-injected ISO on /dev/sr0) │ └──────────────────────────────────────────────────────────┘ ``` Each mount point carries restrictive options: | Mount Point | Options | |-------------------|----------------------------------| | `/home` | `nosuid,nodev` | | `/tmp` | `nosuid,nodev,noexec` | | `/var` | `nosuid,nodev` | | `/var/tmp` | `nosuid,nodev,noexec` | | `/var/log` | `nosuid,nodev,noexec` | | `/var/log/audit` | `nosuid,nodev,noexec` | | `/dev/shm` | `nosuid,nodev,noexec` | --- ## Prerequisites - Proxmox VE 8.x or later - Ubuntu 24.04 LTS Server ISO (minimal install) - An SSH key pair on your admin workstation (see [SSH Key Setup](#ssh-key-setup-windows)) - Wazuh agent (optional, for compliance scanning - the scripts pre-configure outbound firewall rules for Wazuh on ports 1514/1515) --- ## VM Creation in Proxmox Create the VM with these settings before installing Ubuntu: **System tab:** - Machine: `q35` - BIOS: `OVMF (UEFI)` with EFI Disk, pre-enroll keys checked (Secure Boot) - TPM: `v2.0` - SCSI Controller: `VirtIO SCSI single` - Qemu Agent: checked **Disks:** - Disk 1 (OS): SCSI, 20–64 GB, Discard checked, IO Thread checked - Disk 2 (LVM): Added after OS install - SCSI, 50 GB, Discard checked, IO Thread checked **CPU:** Type `host` (exposes AES-NI), 2+ cores **Memory:** 2048 MB minimum, Ballooning checked **Network:** VirtIO (paravirtualized), Firewall checked **After OS installation, add:** - The second 50 GB disk (Hardware → Add → Hard Disk) - A Cloud-Init drive (Hardware → Add → CloudInit Drive) **Cloud-Init tab:** - User: your admin username (e.g., `chris`) - SSH public key: paste your `id_ed25519.pub` - IP Config: DHCP or static - DNS: your internal servers --- ## SSH Key Setup (Windows) Generate an Ed25519 key pair from PowerShell on your admin workstation: ```powershell ssh-keygen -t ed25519 -C "admin@proxmox" ``` Accept the default path (`C:\Users\YourName\.ssh\id_ed25519`) and set a strong passphrase. Copy the public key: ```powershell cat ~/.ssh/id_ed25519.pub ``` This one public key is used for all VMs cloned from the template. Paste it into both the Cloud-Init tab in Proxmox and (during initial setup) the template VM's `~/.ssh/authorized_keys`: ```bash mkdir -p ~/.ssh && chmod 700 ~/.ssh echo "ssh-ed25519 AAAA...your-key..." >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys ``` **Optional - enable the SSH agent** so you don't retype the passphrase every session: ```powershell # One-time (run as Admin) Get-Service ssh-agent | Set-Service -StartupType Automatic -PassThru | Start-Service # Then from regular PowerShell ssh-add ~/.ssh/id_ed25519 ``` --- ## Script 1 - partition-layout.sh **Purpose:** Migrates a default single-partition Ubuntu install into a NIST/CIS-compliant LVM layout with six separate mount points on the second disk. **When to run:** After Ubuntu install, after attaching the second disk, before the hardening script. ```bash chmod +x partition-layout.sh sudo ./partition-layout.sh /dev/sdb ``` **What it does:** 1. Installs LVM2 if missing 2. Creates a GPT partition and LVM volume group (`vg_nist`) on the second disk 3. Creates six logical volumes with ext4 filesystems 4. Migrates existing data from `/var`, `/home`, `/tmp`, etc. using rsync 5. Writes fstab entries with NIST-compliant mount options 6. Mounts everything and sets correct permissions (sticky bits, directory modes) 7. Leaves ~16 GB free in the volume group for future growth **Post-run:** Reboot and verify all six volumes mount automatically: ```bash sudo reboot # After reboot: df -h | grep vg_nist findmnt -t ext4 | grep vg_nist ``` **Growing a volume later:** ```bash sudo lvextend -L +5G /dev/vg_nist/lv_var_log sudo resize2fs /dev/vg_nist/lv_var_log ``` **Known issue:** The script may fail to mount `/var/log/audit` because the mount point doesn't exist on the freshly mounted `/var/log` volume. If this happens: ```bash sudo mkdir -p /var/log/audit sudo mount /var/log/audit sudo chmod 700 /var/log/audit ``` --- ## Script 2 - nist-800-53-harden-v2.sh **Purpose:** Comprehensive OS hardening covering NIST 800-53 controls and CIS Ubuntu 24.04 benchmarks. Uses iptables (not UFW) for Docker compatibility and clean compliance scans. **When to run:** After partition-layout.sh and a successful reboot confirming all mounts. ```bash # Optional: set admin username (defaults to chris) export ADMIN_USER="chris" chmod +x nist-800-53-harden-v2.sh sudo ./nist-800-53-harden-v2.sh ``` **What it does (by NIST control family):** **Access Control (AC):** - Locks root account, removes unnecessary users - Configures pam_faillock (5 attempts / 15-min lockout) - Sets login warning banners (`/etc/issue`, `/etc/issue.net`, `/etc/motd`) - 15-minute idle session timeout (TMOUT with readonly guard) - SSH: key-only auth, AllowUsers, PermitRootLogin no, DisableForwarding **Audit and Accountability (AU):** - Comprehensive auditd rules covering CIS checks 35731–35748 (sudoers, user emulation, time changes, network changes, file access, DAC modifications, mounts, deletions, MAC policy, chcon/setfacl/chacl/usermod, kernel modules) - Audit log retention: `keep_logs`, space warnings, admin halt on full - Chrony NTP (systemd-timesyncd disabled and masked) - Journald: persistent, compressed, rotated, size-limited - Rsyslog: file creation mode 0640, remote forwarding placeholder **Identification and Authentication (IA):** - Password quality: minlen=15, minclass=4, maxsequence=3, maxrepeat=3 - Password aging: 60-day max, 1-day min, 14-day warning, 30-day inactive lock - PAM: faillock, pwhistory (remember=24, enforce_for_root), nullok removed - opasswd file created with strict permissions **System and Communications Protection (SC):** - iptables firewall with default deny on all chains - ICMP type-filtered with rate-limited ping (1/sec, burst 4) - SSH rate-limited (4 new/min per source IP) - Outbound allowed: DNS (53), HTTP (80), HTTPS (443), NTP (123), Wazuh (1514/1515) - DOCKER-USER chain pre-created for Docker compatibility - Rules persisted via iptables-persistent - SSH cryptography: Ed25519/RSA-SHA2 host keys, Curve25519/AES-GCM ciphers, SHA2-ETM MACs - /dev/shm hardened with noexec,nosuid,nodev **System and Information Integrity (SI):** - Kernel hardening: ASLR, ptrace restriction, BPF hardening, core dumps disabled, dmesg restricted - AIDE file integrity monitoring with audit tool coverage - Unattended security upgrades enabled - AppArmor enforced and added to GRUB command line - `audit=1 audit_backlog_limit=8192` added to GRUB **Configuration Management (CM):** - Unnecessary services disabled and masked - Filesystem modules blacklisted (afs, ceph, cifs, exfat, fat, fscache, fuse, gfs2, nfs, nfsd, smbfs, cramfs, squashfs, etc.) - Network protocol modules blacklisted (dccp, sctp, rds, tipc) - USB storage disabled - Telnet, FTP, rsync, nftables, UFW, apport, snapd removed - File permissions hardened, cron/at restricted, su restricted to sudo group - Sudo I/O logging enabled **Template Preparation:** - QEMU guest agent enabled - Cloud-init configured for Proxmox (NoCloud/ConfigDrive) - Machine-id truncated (regenerated on clone) - SSH host keys removed (regenerated on first boot via firstboot-harden.service) - initramfs rebuilt, apt cache cleaned, logs truncated **Post-run:** AIDE database initialization takes 10–15 minutes. After the script completes, reboot: ```bash sudo reboot ``` --- ## Script 3 - remediate.sh **Purpose:** Post-scan remediation script that fixes remaining Wazuh SCA failures after the initial hardening. Addresses specific check IDs with targeted fixes. **When to run:** After the hardening script and a Wazuh SCA scan reveals remaining failures. Can also be run standalone on an already-hardened system. > **Note:** This script was written during the initial hardening iteration and uses UFW for firewall rules (sections 35619–35639). If you ran `nist-800-53-harden-v2.sh` (which uses iptables and removes UFW), the UFW sections will be skipped harmlessly since UFW won't be installed. The iptables rules from the v2 hardening script satisfy the same controls. ```bash chmod +x remediate.sh sudo ./remediate.sh ``` **What it fixes (by Wazuh check ID):** | Check ID | Description | |-------------|----------------------------------------------------| | 35509 | Filesystem kernel modules blacklisted | | 35522 | `nodev` added to `/var` mount | | 35537 | AppArmor enabled in GRUB command line | | 35545 | Apport disabled, masked, and purged | | 35573 | rsync removed | | 35585 | telnet removed | | 35587 | ftp removed | | 35589 | systemd-timesyncd masked (chrony is NTP source) | | 35600 | `/etc/cron.allow` created | | 35601 | `/etc/at.allow` created | | 35604–35607 | dccp, sctp, rds, tipc modules blacklisted | | 35619–35639 | Firewall fully configured (UFW in remediate.sh, iptables in v2) | | 35641–35642 | SSH host key permissions set | | 35643–35661 | Complete SSH hardening rewrite | | 35664 | Sudo logfile configured | | 35668 | `su` restricted to sudo group via pam_wheel | | 35672–35690 | Full PAM stack rewrite (common-auth, common-account, common-password, common-session) | | 35681/35683 | Password quality: minclass, maxsequence added | | 35694–35698 | Password aging and inactive lock on all users | | 35703 | Root umask set in `.bashrc` and `.bash_profile` | | 35708 | Journald rotation configured | | 35719 | Rsyslog file creation mode 0640 | | 35722 | Log file permissions tightened | | 35725–35726 | `audit=1 audit_backlog_limit=8192` in GRUB | | 35728–35730 | auditd log retention: keep_logs, space alerts | | 35731–35748 | Complete CIS-compliant audit ruleset | | 35752 | Audit config file permissions (640) | | 35755 | Audit tool permissions | | 35760 | AIDE integrity rules for audit tools | | 35770 | opasswd/opasswd.old permissions | --- ## Deployment Workflow The full sequence from start to template: ``` 1. Create VM in Proxmox (settings above) 2. Install Ubuntu 24.04 Server (minimal) 3. Attach second 50 GB disk in Proxmox Hardware tab 4. Add Cloud-Init drive in Proxmox Hardware tab 5. Boot VM, log in via Proxmox console (noVNC) 6. Add your SSH public key to ~/.ssh/authorized_keys 7. Copy scripts to the VM 8. Run: sudo ./partition-layout.sh /dev/sdb 9. Reboot, verify mounts with df -h 10. Run: sudo ./nist-800-53-harden-v2.sh 11. Reboot, verify SSH access with key auth 12. (Optional) Install Wazuh agent, run SCA scan 13. (Optional) Run: sudo ./remediate.sh 14. Reboot, re-scan, review results 15. Set GRUB password (manual - see below) 16. Remove Ubuntu install ISO from CD drive 17. Shut down VM 18. Right-click → Convert to Template (or: qm template ) ``` ### Setting the GRUB Password (Manual Step) This is the one control that cannot be automated because it requires interactive hash generation: ```bash # Generate the hash sudo grub-mkpasswd-pbkdf2 # Enter and confirm your password, copy the output hash # Create the GRUB config sudo tee /etc/grub.d/40_custom > /dev/null < EOF sudo chmod 755 /etc/grub.d/40_custom sudo update-grub ``` --- ## Post-Template: Cloning To deploy a new VM from the template: 1. Right-click the template → **Clone** → **Full Clone** 2. Set VM ID and name 3. Go to the clone's **Cloud-Init tab** and configure: - User (or keep the default) - SSH public key (or keep the template's key) - IP configuration (static or DHCP) - DNS servers 4. Start the VM On first boot, the `firstboot-harden.service` automatically: - Regenerates SSH host keys - Sets correct host key permissions - Rebuilds the AIDE integrity database - Creates `/var/lib/firstboot-done` to prevent re-running The Cloud-Init subsystem handles hostname, user creation, SSH key injection, and network configuration. --- ## Firewall Management The v2 hardening script uses iptables with persistent rules. Common management tasks: **View current rules:** ```bash sudo iptables -L -v -n --line-numbers sudo ip6tables -L -v -n --line-numbers ``` **Add a new inbound service (e.g., HTTPS):** ```bash # Insert before the LOG rule (check line numbers first) sudo iptables -I INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT sudo iptables-save | sudo tee /etc/iptables/rules.v4 ``` **Add a new outbound service:** ```bash sudo iptables -I OUTPUT -p tcp --dport 8080 -m conntrack --ctstate NEW -j ACCEPT sudo iptables-save | sudo tee /etc/iptables/rules.v4 ``` **Docker compatibility:** The script pre-creates a `DOCKER-USER` chain. When Docker is installed, it inserts its own rules but respects the `DOCKER-USER` chain for custom filtering. To restrict Docker container traffic: ```bash sudo iptables -I DOCKER-USER -s 10.0.0.0/8 -j DROP sudo iptables-save | sudo tee /etc/iptables/rules.v4 ``` **Persistent rules** are stored in `/etc/iptables/rules.v4` and `/etc/iptables/rules.v6`, loaded on boot by `netfilter-persistent`. --- ## NIST 800-53 Control Mapping | Control | Title | Implementation | |------------|----------------------------------------|------------------------------------------------------------| | AC-2 | Account Management | Root locked, unnecessary users removed, umask 027 | | AC-3 | Access Enforcement | AppArmor, su restricted to sudo group | | AC-7 | Unsuccessful Logon Attempts | pam_faillock: 5 attempts, 15-min lockout | | AC-8 | System Use Notification | /etc/issue, /etc/issue.net, /etc/motd banners | | AC-11 | Session Lock | TMOUT=900 (15-min idle timeout) | | AC-17 | Remote Access | SSH: key-only, AllowUsers, modern crypto | | AU-2/3/12 | Audit Events, Content, Generation | Comprehensive auditd rules (CIS 4.1.3.1–18) | | AU-4 | Audit Log Storage | keep_logs, space alerts, admin halt | | AU-8 | Time Stamps | Chrony NTP, systemd-timesyncd disabled | | CM-2 | Baseline Configuration | Full system update before hardening | | CM-6 | Configuration Settings | File permissions, sudo logging, cron/at restrictions | | CM-7 | Least Functionality | Services disabled, modules blacklisted, packages purged | | IA-5 | Authenticator Management | Password quality/aging, PAM stack, opasswd | | MP-2 | Media Access | Mount options (noexec, nosuid, nodev) | | MP-7 | Media Use | USB storage module disabled | | SC-5 | Denial-of-Service Protection | SYN cookies, iptables rate limiting | | SC-7 | Boundary Protection | iptables default-deny, connection tracking | | SC-8/13 | Transmission Confidentiality/Crypto | SSH: Curve25519, AES-GCM, SHA2-ETM | | SC-28 | Protection of Information at Rest | Filesystem mount hardening | | SI-2 | Flaw Remediation | Unattended security upgrades | | SI-7 | Software Integrity | AIDE file integrity monitoring | | SI-16 | Memory Protection | ASLR, ptrace, BPF, core dumps disabled | --- ## Wazuh SCA Check Coverage Total checks addressed across all three scripts: **70+** Checks are tracked by their Wazuh SCA IDs (35xxx series) corresponding to the CIS Ubuntu Linux 24.04 LTS Benchmark. See the summary tables at the end of each script for a complete list of check IDs mapped to their fixes. --- ## Known Exceptions These checks will continue to fail or require environment-specific action. Document them as compensating controls for auditors. | Check ID | Description | Reason / Compensating Control | |----------|------------------------------------|--------------------------------------------------------------------------------| | 35509 | `fat` module not disabled | Required for UEFI boot partition (`/boot/efi`). Built into kernel on 24.04 - blacklisting the loadable module is safe but the check may still flag it. | | 35540 | GRUB bootloader password | Requires interactive hash generation. Set manually (see Deployment Workflow). | | 35589 | systemd-timesyncd not enabled | Chrony is the NTP source (AU-8). timesyncd is intentionally disabled. Both satisfy the control; the scanner may not detect chrony. | | 35710 | systemd-journal-upload auth | Only required if using journal-upload to a remote journal gateway. Most setups use Wazuh or rsyslog instead. | | 35720 | rsyslog remote logging | Environment-specific. Placeholder config at `/etc/rsyslog.d/99-remote.conf`. Wazuh agent serves as a compensating control for centralized log collection. | --- ## Maintenance **Quarterly review checklist:** - Run Wazuh SCA scan and review any new failures - Review `/var/log/sudo.log` and audit logs - Verify AIDE integrity: `sudo aide --check` - Check that unattended-upgrades is running: `sudo systemctl status unattended-upgrades` - Review and rotate known-exception documentation - Verify NTP sync: `chronyc tracking` - Check LVM free space: `sudo vgdisplay vg_nist | grep Free` **Updating the template:** 1. Clone the template to a temporary VM 2. Boot, apply updates (`sudo apt update && sudo apt upgrade`) 3. Re-run the hardening script (it's idempotent) 4. Rebuild AIDE database: `sudo aideinit -y -f` 5. Clean up and shut down 6. Convert to new template, retire the old one --- ## Troubleshooting **Can't SSH in after hardening:** The hardening script sets `PasswordAuthentication no` and `AllowUsers chris` (or your configured admin user). Verify your SSH key is in `~/.ssh/authorized_keys` and your username matches `AllowUsers`. Use the Proxmox noVNC console as a fallback - it's a virtual console, not SSH. **TMOUT readonly variable error on login:** The v2 script includes a guard for this. If you see the error on an older install, replace the timeout script: ```bash sudo tee /etc/profile.d/nist-timeout.sh > /dev/null <<'EOF' if [ -z "${TMOUT:-}" ]; then readonly TMOUT=900 export TMOUT fi EOF ``` **AIDE takes a long time:** Initial database creation scans all of `/boot`, `/bin`, `/sbin`, `/lib`, `/usr`, and `/etc`. This takes 10–20 minutes on a VM. Subsequent checks are faster. Verify it's running with `top -c | grep aide`. **Docker bypasses firewall rules:** The v2 script pre-creates the `DOCKER-USER` chain. Docker respects rules inserted there. To block specific traffic to containers, add rules to `DOCKER-USER`, not `INPUT`. **iptables rules lost after reboot:** Verify `netfilter-persistent` is enabled: `sudo systemctl is-enabled netfilter-persistent`. Rules should be in `/etc/iptables/rules.v4` and `rules.v6`. To re-save current rules: `sudo iptables-save | sudo tee /etc/iptables/rules.v4`. **Wazuh still flags firewall checks (35626–35639):** If you ran `remediate.sh` (which uses UFW) and then `nist-800-53-harden-v2.sh` (which removes UFW), the iptables rules satisfy the same CIS controls. Some Wazuh SCA checks are written for a specific firewall tool. Add Wazuh rule-level exceptions for the ones that don't match your chosen tool (see the Wazuh exception guidance in the conversation that produced these scripts). --- ## File Reference ``` . ├── README.md # This file ├── partition-layout.sh # Step 1: LVM partition layout ├── nist-800-53-harden-v2.sh # Step 2: OS hardening (iptables, CIS/Wazuh aligned) └── remediate.sh # Step 3: Post-scan targeted fixes ``` ## License These scripts are provided as-is for educational and operational use. Review and test thoroughly in a non-production environment before deployment. Compliance is a shared responsibility - these scripts address technical controls but do not replace organizational policies, procedures, or risk assessments.