ACCESS CONTROL
SSH · User Accounts · PAM · Sudo Privileges
3 CONTROLS
⌄
⚠ ISSUE
Default SSH configuration allows root login, password authentication, all cipher suites (including weak ones), and exposes the default port 22 to automated scanning and brute-force attacks.
🔥 RISK
Unauthorized root access via brute-force, credential stuffing, MITM via weak SSH ciphers, and lateral movement using compromised SSH keys.
📁 FILES & PATHS
/etc/ssh/sshd_config
/etc/ssh/sshd_config.d/99-hardening.conf
~/.ssh/authorized_keys
🔧 FIX — /etc/ssh/sshd_config.d/99-hardening.conf
/etc/ssh/sshd_config.d/99-hardening.conf (drop-in — takes priority)
# ─── Protocol & Port ────────────────────────────────────────── Protocol 2 Port 2222 # Non-standard port (change to your port) ListenAddress 0.0.0.0 # ─── Authentication Controls ────────────────────────────────── PermitRootLogin no # CRITICAL: never allow root SSH PasswordAuthentication no # Keys only — disable passwords ChallengeResponseAuthentication no KbdInteractiveAuthentication no PermitEmptyPasswords no PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys # ─── Session Limits ─────────────────────────────────────────── MaxAuthTries 3 MaxSessions 4 LoginGraceTime 30 # seconds to complete login ClientAliveInterval 300 # keepalive every 5 min ClientAliveCountMax 2 # disconnect after 10 min idle # ─── Access Restriction ─────────────────────────────────────── AllowUsers deployer sysadm # Explicit user allowlist # AllowGroups sshusers # Or restrict by group # ─── Strong Cryptography Only ───────────────────────────────── KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 # ─── Feature Restrictions ───────────────────────────────────── X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no # Disable SSH tunneling PermitTunnel no GatewayPorts no PrintLastLog yes # Show last login on connect # ─── Login Banner ───────────────────────────────────────────── Banner /etc/ssh/banner # /etc/ssh/banner content: "AUTHORIZED ACCESS ONLY. All activity monitored." # ─── Logging ────────────────────────────────────────────────── SyslogFacility AUTHPRIV LogLevel VERBOSE
Shell — Apply and validate
# Test config before restarting (critical!) $ sshd -t # → No output = config is valid # Reload SSH (NOT restart — keeps current sessions alive) $ systemctl reload sshd # RHEL/CentOS $ systemctl reload ssh # Ubuntu/Debian # Regenerate host keys (remove weak DSA/ECDSA keys) $ rm /etc/ssh/ssh_host_dsa_key* /etc/ssh/ssh_host_ecdsa_key* $ ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N "" $ ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N "" # Moduli hardening — remove weak DH parameters (< 3072 bits) $ awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe $ mv /etc/ssh/moduli.safe /etc/ssh/moduli
👥 RESPONSIBLE
Linux/System Admin
🔄 SERVICE RESTART
⟳ systemctl reload sshd
⟳ systemctl reload ssh
✅ VERIFICATION
ssh-audit -p 2222 your-server-ip
→ [info] - algorithm recommendations
→ [pass] - kex: curve25519-sha256
→ [pass] - cipher: chacha20-poly1305@openssh.com
→ [warn] - no warnings (if hardened correctly)
ssh -o PasswordAuthentication=yes user@server
→ Permission denied (publickey). ← passwords blocked ✓
ssh root@server
→ Permission denied (publickey). ← root blocked ✓
nmap -sV -p 22 server-ip
→ 22/tcp filtered ← port 22 should be closed/filtered ✓
📋 VAPT CLOSURE
🔒
Audit Closure
SSH daemon hardened: root login disabled, password authentication disabled (public key only), MaxAuthTries limited to 3, weak cipher suites (CBC, MD5, SHA1) removed, X11/TCP forwarding disabled, non-standard port configured. LoginGraceTime and idle timeout enforced. A legal access banner is displayed. Configuration validated via ssh-audit reporting zero critical findings.⌄
⚠ ISSUE
Default Linux PAM configuration allows weak passwords, no account lockout policy, no password history, and no complexity enforcement. Service accounts often have interactive shells and login capability.
🔥 RISK
Password spray attacks against local accounts, privilege escalation via service account compromise, and credential reuse attacks due to no password history enforcement.
🔧 FIX — Password Policy (pwquality + login.defs)
/etc/security/pwquality.conf
# Minimum password length minlen = 14 # Require at least 1 uppercase, 1 lowercase, 1 digit, 1 special ucredit = -1 lcredit = -1 dcredit = -1 ocredit = -1 # Reject passwords containing username usercheck = 1 # Reject dictionary words dictcheck = 1 # Number of characters changed from previous password difok = 8 # Reject easy sequences (123, abc) maxsequence = 3 maxrepeat = 3
/etc/login.defs — Password aging
PASS_MAX_DAYS 90 # Password expires in 90 days PASS_MIN_DAYS 1 # Minimum 1 day between changes PASS_WARN_AGE 14 # Warn 14 days before expiry PASS_MIN_LEN 14 # Redundant with pwquality but belt+suspenders # Encrypt passwords with yescrypt (strongest) ENCRYPT_METHOD YESCRYPT
/etc/pam.d/system-auth — Account lockout (pam_faillock)
# Add BEFORE auth sufficient pam_unix.so auth required pam_faillock.so preauth silent audit deny=5 unlock_time=900 fail_interval=900 auth sufficient pam_unix.so try_first_pass auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900 account required pam_faillock.so # /etc/security/faillock.conf deny = 5 # lockout after 5 failures fail_interval = 900 # within 15 minutes unlock_time = 900 # locked for 15 minutes audit # log to audit log silent # don't reveal lockout to attacker even_deny_root # also lock root root_unlock_time = 60 # root unlocks after 60 seconds
/etc/pam.d/password-auth — Password history (pam_pwhistory)
# Add to password section — remember last 24 passwords
password requisite pam_pwquality.so try_first_pass local_users_only
password required pam_pwhistory.so use_authtok remember=24 enforce_for_root
Shell — Disable unnecessary system account shells
# List all accounts with login shells (review these) $ awk -F: '($7 != "/sbin/nologin" && $7 != "/bin/false" && $1 != "root") {print $1": "$7}' /etc/passwd # Lock service accounts — they should never login interactively $ usermod -s /sbin/nologin nginx $ usermod -s /sbin/nologin mysql $ usermod -L nginx mysql postgres www-data # lock password (no login) # Remove all users not needed $ userdel -r lp news uucp games gopher ftp # Set password aging for existing accounts $ chage --maxdays 90 --warndays 14 username
👥 RESPONSIBLE
Linux/System AdminSecurity Team
✅ VERIFICATION
faillock --user testuser
→ Shows failed attempts counter
chage -l username
→ Maximum number of days between password change : 90
→ Password expiration warning : 14
passwd -S username
→ username P (password set), locked accounts show L
grep "remember" /etc/pam.d/password-auth
→ pam_pwhistory.so ... remember=24 ✓
📋 VAPT CLOSURE
🔒
Audit Closure
PAM configured with pam_pwquality (14-char minimum, complexity requirements), pam_faillock (5-attempt lockout, 15-min window), and pam_pwhistory (24 passwords remembered). Password aging enforced via /etc/login.defs (90-day max, 14-day warning). Service accounts use /sbin/nologin shell. YESCRYPT password hashing algorithm configured. Unnecessary system accounts removed.⌄
⚠ ISSUE
Overly permissive sudo rules (
ALL=(ALL) NOPASSWD: ALL), users in the wheel/sudo group with unrestricted access, and no sudo logging allow privilege escalation and untraceable admin actions.🔥 RISK
Privilege escalation from unprivileged user to root, lateral movement via sudo misuse, and inability to audit who ran what privileged command when.
🔧 FIX — /etc/sudoers.d/hardening
Run: visudo -f /etc/sudoers.d/hardening
# ─── Defaults ────────────────────────────────────────────────── Defaults logfile=/var/log/sudo.log # dedicated sudo log Defaults log_input, log_output # log commands + output Defaults iolog_dir=/var/log/sudo-io/%{user}/%{hostname} Defaults timestamp_timeout=5 # re-auth after 5 min idle Defaults passwd_tries=3 # 3 wrong password attempts Defaults requiretty # sudo only from real TTY Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" Defaults env_reset # reset environment Defaults use_pty # force pseudo-TTY (audit sessions) # ─── Restrict wheel group ────────────────────────────────────── # ✗ REMOVE: %wheel ALL=(ALL) NOPASSWD: ALL # ✓ USE: explicit commands only # ─── Named command aliases ───────────────────────────────────── Cmnd_Alias SERVICES = /bin/systemctl start *, /bin/systemctl stop *, /bin/systemctl restart * Cmnd_Alias PKGMGMT = /usr/bin/dnf, /usr/bin/yum, /usr/bin/apt, /usr/bin/apt-get Cmnd_Alias NETWORK = /sbin/ip, /usr/sbin/iptables, /usr/sbin/firewall-cmd # ─── Least-privilege sudo grants ────────────────────────────── %sysadmins ALL=(ALL) PASSWD: ALL # admins with password deployer ALL=(root) NOPASSWD: SERVICES # deployment user: restart services only developer ALL=(root) NOPASSWD: PKGMGMT # devs: package manager only # ─── Explicitly deny dangerous commands ─────────────────────── %users ALL=(ALL) !NOPASSWD: /bin/bash, /bin/sh, /usr/bin/python*
Shell — Audit current sudo rights
# List all sudoers and their rights $ sudo -l -U username $ grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ # Find users in wheel/sudo group $ getent group wheel sudo # Check sudoers syntax after editing $ visudo -c # → /etc/sudoers: parsed OK
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
sudo -l -U deployer
→ (root) NOPASSWD: /bin/systemctl start *, stop *, restart *
sudo -l -U developer
→ (root) NOPASSWD: /usr/bin/dnf, /usr/bin/apt
cat /var/log/sudo.log | tail -10
→ Apr 21 10:22:01 : deployer : TTY=pts/0 ; PWD=/home/deployer ;
USER=root ; COMMAND=/bin/systemctl restart nginx
📋 VAPT CLOSURE
🔒
Audit Closure
Sudo access follows least-privilege: each user/group has explicit command restrictions, no global NOPASSWD:ALL grants, and a mandatory password for administrative users. Sudo logging (including I/O logging) is enabled to /var/log/sudo.log. requiretty prevents sudo from scripts. env_reset prevents environment injection. All NOPASSWD grants are documented and limited to specific system commands.
MANDATORY ACCESS CONTROL
SELinux (RHEL/CentOS) · AppArmor (Ubuntu/Debian)
2 CONTROLS
⌄
⚠ ISSUE
SELinux is commonly disabled or set to Permissive mode by admins to "fix permission errors" — leaving the kernel-level MAC layer completely inactive. This is one of the most critical misconfigurations on RHEL-based systems.
🔥 RISK
Without SELinux, a compromised process (nginx, Apache, MySQL) has full filesystem and network access as its user. SELinux enforcing contains compromised processes to their policy-defined boundaries even if the application is fully exploited.
🔧 FIX — Enable SELinux Enforcing
/etc/selinux/config
# ✗ NEVER in production: # SELINUX=disabled # SELINUX=permissive # ✓ REQUIRED: SELINUX=enforcing SELINUXTYPE=targeted
Shell — Enable enforcing without reboot (runtime)
# ─── Check current status ───────────────────────────────────── $ sestatus $ getenforce # ─── Enable enforcing immediately (no reboot) ───────────────── $ setenforce 1 # ─── If switching from disabled → need relabel + reboot ─────── $ touch /.autorelabel $ reboot # ─── Investigate AVC denials ────────────────────────────────── $ ausearch -m avc -ts recent | audit2why $ sealert -a /var/log/audit/audit.log # ─── Fix common denials without disabling SELinux ───────────── # Fix web server file context $ semanage fcontext -a -t httpd_sys_content_t "/var/www/myapp(/.*)?" $ restorecon -Rv /var/www/myapp/ # Allow nginx to connect to backend (common need) $ setsebool -P httpd_can_network_connect 1 # Allow custom SSH port (change 2222 to your port) $ semanage port -a -t ssh_port_t -p tcp 2222 # Generate custom policy from AVC denials (AUDIT before applying) $ ausearch -m avc -ts recent | audit2allow -M mypolicy $ semodule -i mypolicy.pp
Shell — Key SELinux boolean security settings
# ─── Hardening: disable dangerous booleans ──────────────────── # Prevent daemons from making network connections (restrict if not needed) $ setsebool -P httpd_can_network_connect 0 # disable if nginx is front-end only # Prevent scripts from executing dangerous commands $ setsebool -P httpd_execmem 0 $ setsebool -P httpd_can_sendmail 0 # Enable protective booleans $ setsebool -P deny_ptrace 1 # prevent ptrace (anti-debugging) $ setsebool -P secure_mode 1 # restrict insecure operations # List all booleans and their state $ getsebool -a | grep httpd
👥 RESPONSIBLE
Linux/System AdminSecurity Team
🔄 REQUIRED
⟳ Reboot if switching from disabled
✅ VERIFICATION
getenforce
→ Enforcing
sestatus
→ SELinux status: enabled
→ SELinuxfs mount: /sys/fs/selinux
→ SELinux mount point: /sys/fs/selinux
→ Loaded policy name: targeted
→ Current mode: enforcing
→ Mode from config file: enforcing
cat /etc/selinux/config | grep ^SELINUX=
→ SELINUX=enforcing
ausearch -m avc -ts today | wc -l
→ Low number = good (review any denials)
📋 VAPT CLOSURE
🔒
Audit Closure
SELinux is confirmed in Enforcing mode (targeted policy) via both getenforce output and persistent /etc/selinux/config configuration. File contexts are correctly labeled for all application directories. Dangerous SELinux booleans (httpd_execmem, deny_ptrace=on) are configured. AVC denials are reviewed and resolved via proper context labeling rather than policy disabling. No setenforce 0 or disabled entries in configuration files.⌄
⚠ ISSUE
AppArmor profiles may be in complain mode (logs but doesn't enforce), or key service profiles may not exist at all. Like SELinux, AppArmor is often left inactive after troubleshooting.
🔥 RISK
Without AppArmor, compromised applications (nginx, MySQL, Docker) have unrestricted filesystem and capability access. AppArmor confines exploited services to their defined permission profile.
🔧 FIX — Enable AppArmor & enforce profiles
Shell — Install, enable, and enforce all profiles
# ─── Install AppArmor utilities ─────────────────────────────── $ apt install apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra -y # ─── Enable AppArmor in GRUB (if disabled) ──────────────────── $ sed -i 's/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 apparmor=1 security=apparmor"/' /etc/default/grub $ update-grub # ─── Check status ───────────────────────────────────────────── $ aa-status # Shows: profiles loaded, profiles in enforce mode, etc. # ─── Enforce all complain-mode profiles ─────────────────────── $ aa-enforce /etc/apparmor.d/* # ─── Enforce specific high-value services ───────────────────── $ aa-enforce /etc/apparmor.d/usr.sbin.nginx $ aa-enforce /etc/apparmor.d/usr.sbin.mysqld $ aa-enforce /etc/apparmor.d/usr.sbin.sshd # ─── Generate profile for a new application ─────────────────── $ aa-genprof /usr/bin/myapp # Run the app, let AppArmor learn, then enforce $ aa-complain /usr/bin/myapp # learning mode # ... run app normally for a period ... $ aa-logprof # review and update profile from logs $ aa-enforce /usr/bin/myapp # switch to enforce # ─── Reload a profile after editing ─────────────────────────── $ apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
/etc/apparmor.d/local/nginx — Custom nginx restrictions
# Additional restrictions for nginx beyond the base profile # Place in /etc/apparmor.d/local/ so upstream updates don't overwrite # Deny write access to system directories deny /etc/passwd rw, deny /etc/shadow rw, deny /etc/sudoers rw, deny /root/** rw, # Allow only specific network capabilities network inet stream, deny network raw,
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
aa-status | head -20
→ apparmor module is loaded.
→ 47 profiles are loaded.
→ 47 profiles are in enforce mode.
→ 0 profiles are in complain mode.
systemctl is-active apparmor
→ active
cat /sys/module/apparmor/parameters/enabled
→ Y
📋 VAPT CLOSURE
🔒
Audit Closure
AppArmor is active and loaded (aa-status confirms module loaded). All service profiles are in enforce mode — 0 profiles in complain mode. Key services (nginx, MySQL, sshd) have individual enforced profiles. AppArmor is persistent across reboots via GRUB configuration. Profile denials are logged to /var/log/syslog and monitored via auditd integration.
ANTIVIRUS / EDR / MALWARE DETECTION
MDATP (Microsoft Defender) · ClamAV · Rootkit Detection
3 CONTROLS
⌄
⚠ ISSUE
Linux servers often lack enterprise-grade EDR (Endpoint Detection & Response). Traditional antivirus is insufficient against fileless attacks, process injection, and lateral movement. MDATP on Linux provides real-time protection, behavioral analytics, and integration with Microsoft Sentinel.
🔥 RISK
Undetected malware, cryptominers, webshells, and backdoors running on Linux servers without EDR visibility. Fileless attacks and process injection invisible to traditional scanners.
🔧 FIX — Install MDATP on RHEL/Ubuntu
Shell — RHEL 8/9 Installation
# ─── RHEL / CentOS / Rocky Linux ───────────────────────────── # 1. Add Microsoft package repository $ curl -o /tmp/microsoft.rpm https://packages.microsoft.com/config/rhel/9.0/prod.repo $ mv /tmp/microsoft.rpm /etc/yum.repos.d/microsoft-prod.repo $ rpm --import https://packages.microsoft.com/keys/microsoft.asc # 2. Install MDATP $ dnf install mdatp -y # 3. Onboard to Microsoft 365 Defender portal # Download onboarding package from: security.microsoft.com # → Endpoints → Device management → Onboarding → Linux Server $ python3 MicrosoftDefenderATPOnboardingLinuxServer.py # 4. Enable and start $ systemctl enable --now mdatp
Shell — Ubuntu 22.04 Installation
# ─── Ubuntu / Debian ───────────────────────────────────────── $ curl -o /tmp/microsoft.list https://packages.microsoft.com/config/ubuntu/22.04/prod.list $ mv /tmp/microsoft.list /etc/apt/sources.list.d/microsoft-prod.list $ curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null $ apt update && apt install mdatp -y # Onboard (same Python script as RHEL) $ python3 MicrosoftDefenderATPOnboardingLinuxServer.py $ systemctl enable --now mdatp
Shell — Configure MDATP for security hardening
# ─── Verify health ──────────────────────────────────────────── $ mdatp health # Key fields to verify: # healthy : true # licensed : true # real_time_protection_enabled : true # cloud_enabled : true # definition_status : up_to_date # ─── Enable real-time protection (must be on) ───────────────── $ mdatp config real-time-protection --value enabled # ─── Set protection level to maximum ───────────────────────── $ mdatp config cloud-diagnostic --value enabled $ mdatp config cloud-automatic-sample-submission --value enabled # ─── Enable behavior monitoring ────────────────────────────── $ mdatp config behavior-monitoring --value enabled # ─── Run on-demand scan ────────────────────────────────────── $ mdatp scan quick $ mdatp scan full # full filesystem scan (intensive) # ─── Update definitions ─────────────────────────────────────── $ mdatp definitions update # ─── Check for threats detected ─────────────────────────────── $ mdatp threat list # ─── Create exclusion for high-I/O directories (performance) ─ $ mdatp exclusion folder add --path /var/lib/mysql $ mdatp exclusion folder add --path /var/cache $ mdatp exclusion list
/etc/opt/microsoft/mdatp/managed/mdatp_managed.json — Hardened policy
{
"antivirusEngine": {
"enableRealTimeProtection": true,
"enableBehaviorMonitoring": true,
"threatTypeSettings": [
{"key": "potentially_unwanted_application", "value": "block"},
{"key": "archive_bomb", "value": "block"}
],
"scanAfterDefinitionUpdate": true,
"scanArchives": true
},
"cloudService": {
"enabled": true,
"diagnosticLevel": "optional",
"automaticSampleSubmissionConsent": "safe"
},
"edr": {
"tags": [{"key": "GROUP", "value": "LinuxServers"}]
}
}
👥 RESPONSIBLE
Linux/System AdminSecurity Team
🔄 SERVICE
⟳ systemctl enable --now mdatp
✅ VERIFICATION
mdatp health --field healthy
→ true
mdatp health --field real_time_protection_enabled
→ true
mdatp health --field definition_status
→ up_to_date
# Test EICAR test file (harmless antivirus test)
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar.txt
→ mdatp threat list should show EICAR detected and quarantined within seconds
mdatp threat list
→ EICAR: quarantined ✓
📋 VAPT CLOSURE
🔒
Audit Closure
Microsoft Defender for Endpoint (MDATP) is installed and onboarded to the Microsoft 365 Defender portal. Real-time protection, behavior monitoring, cloud protection, and automatic sample submission are enabled. Definition status is up-to-date. EICAR test file detection confirms AV engine is functional. The server appears in the Defender portal under the LinuxServers device group. Managed policy JSON enforces PUA blocking and archive scanning.⌄
⚠ ISSUE
Without antivirus, malicious files (webshells, malware, trojans) can be uploaded to the server and remain undetected. ClamAV provides free, open-source malware scanning with automated signature updates.
🔥 RISK
PHP webshells uploaded via vulnerable file upload endpoints remain active indefinitely without AV scanning, enabling persistent attacker access after initial exploitation.
🔧 FIX — Install & Configure ClamAV
Shell — Install and configure ClamAV
# ─── Install ───────────────────────────────────────────────── $ dnf install clamav clamav-update clamd -y # RHEL $ apt install clamav clamav-daemon clamav-freshclam -y # Ubuntu # ─── Update virus definitions ──────────────────────────────── $ freshclam # ─── Enable and start services ─────────────────────────────── $ systemctl enable --now clamav-freshclam # auto-update definitions daily $ systemctl enable --now clamd@scan # ClamAV daemon (RHEL) $ systemctl enable --now clamav-daemon # (Ubuntu)
/usr/local/bin/clam-daily-scan.sh — Automated scan with alerting
#!/bin/bash # Daily ClamAV scan with email alert on findings # Cron: 0 3 * * * /usr/local/bin/clam-daily-scan.sh SCAN_DIRS="/var/www /home /tmp /var/tmp" LOG="/var/log/clamav/daily-scan-$(date +%F).log" ALERT_EMAIL="security@your-org.com" mkdir -p /var/log/clamav clamscan \ --recursive \ --infected \ # only print infected files --remove=no \ # don't auto-delete (quarantine instead) --move=/var/quarantine \ # move threats to quarantine --log="$LOG" \ --stdout \ $SCAN_DIRS INFECTED=$(grep "Infected files:" "$LOG" | awk '{print $NF}') if [ "$INFECTED" -gt 0 ]; then echo "ALERT: ClamAV found $INFECTED infected files on $(hostname)" | \ mail -s "[SECURITY] ClamAV Alert $(date +%F)" $ALERT_EMAIL fi
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
freshclam --version
→ ClamAV x.y.z ... Daily: version 27xxx
echo 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > /tmp/eicar_test.txt && clamscan /tmp/eicar_test.txt
→ /tmp/eicar_test.txt: Win.Test.EICAR_HDB-1 FOUND
→ Infected files: 1 ✓
systemctl status clamav-freshclam
→ Active: active (running)
📋 VAPT CLOSURE
🔒
Audit Closure
ClamAV antivirus is installed with clamav-freshclam auto-update service running (definitions updated daily). A daily automated scan covers /var/www, /home, /tmp, and /var/tmp — infected files are moved to quarantine and security team is notified via email. EICAR test confirms detection is functional. Definitions are current as verified by freshclam.⌄
⚠ ISSUE
Rootkits install as kernel modules or replace system binaries to hide processes, files, and network connections from the operating system. Traditional file scans miss them because they operate below the OS level.
🔥 RISK
Persistent attacker presence invisible to standard tools — ps, netstat, ls show clean results while the rootkit hides malicious processes, network connections, and files from the administrator.
🔧 FIX — Install and schedule rkhunter + chkrootkit
Shell — rkhunter setup
# ─── Install ───────────────────────────────────────────────── $ dnf install rkhunter -y # RHEL $ apt install rkhunter chkrootkit -y # Ubuntu # ─── Build initial baseline of system files ─────────────────── # IMPORTANT: Run this on a KNOWN-CLEAN system $ rkhunter --propupd # → File properties database created # ─── Update rkhunter signatures ────────────────────────────── $ rkhunter --update # ─── Run a check ──────────────────────────────────────────── $ rkhunter --check --sk --rwo # --sk=skip key press, --rwo=report warnings only # ─── Configure rkhunter ─────────────────────────────────────── # /etc/rkhunter.conf key settings: # MAIL-ON-WARNING=security@your-org.com # ALLOW_SSH_ROOT_USER=no # ALLOW_SSH_PROT_V1=0 # ─── Cron job — daily scan ─────────────────────────────────── $ echo '0 4 * * * root /usr/bin/rkhunter --check --sk --rwo --logfile /var/log/rkhunter/rkhunter.log 2>&1 | mail -s "[rkhunter] $(hostname)" security@your-org.com' >> /etc/crontab # ─── chkrootkit scan ───────────────────────────────────────── $ chkrootkit # INFECTED lines need investigation # "not infected" lines are clean
👥 RESPONSIBLE
Linux/System AdminSecurity Team
📋 VAPT CLOSURE
🔒
Audit Closure
rkhunter and chkrootkit are installed with a file properties database baseline built on a known-clean system. Daily automated scans are scheduled via cron at 4 AM with warning reports emailed to the security team. Signatures are updated before each scan. Both tools report no rootkits, backdoors, or suspicious kernel modules on the current system.
NETWORK SECURITY
Firewall · Fail2ban · Kernel Network Hardening
3 CONTROLS
⌄
⚠ ISSUE
Default firewall state on many Linux systems is either disabled or set to accept all traffic. Exposed management ports (MySQL 3306, Redis 6379, internal APIs) are publicly reachable, creating direct attack surfaces.
🔥 RISK
Direct exploitation of database/cache services with no authentication bypass (Redis AUTH bypass, MySQL anonymous access, internal API enumeration) and port scanning exposing server's complete service landscape.
🔧 FIX — firewalld (RHEL/CentOS) Hardening
Shell — firewalld configuration
# ─── Install and enable ─────────────────────────────────────── $ dnf install firewalld -y $ systemctl enable --now firewalld # ─── Default deny: drop all inbound ────────────────────────── $ firewall-cmd --set-default-zone=drop # ─── Allow only what is needed ──────────────────────────────── # SSH (use your custom port) $ firewall-cmd --permanent --zone=drop --add-port=2222/tcp # Web services $ firewall-cmd --permanent --zone=drop --add-service=http $ firewall-cmd --permanent --zone=drop --add-service=https # Rate-limit SSH connections (max 3 new connections/min) $ firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="3/m" accept' # ─── Block internal services from external access ───────────── # MySQL: only allow from application server IP $ firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.1.10" port port="3306" protocol="tcp" accept' # Redis: only localhost $ firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="6379" protocol="tcp" accept' # Block ICMP (ping) — optional but reduces attack surface $ firewall-cmd --permanent --add-icmp-block=echo-request # ─── Reload and verify ──────────────────────────────────────── $ firewall-cmd --reload $ firewall-cmd --list-all
Shell — ufw (Ubuntu) Hardening
# ─── ufw for Ubuntu / Debian ───────────────────────────────── $ ufw --force reset # start clean $ ufw default deny incoming # block all inbound by default $ ufw default allow outgoing # allow all outbound (restrict as needed) $ ufw default deny forward # no forwarding # Allow specific services $ ufw allow 2222/tcp # SSH custom port $ ufw allow http $ ufw allow https # Rate-limit SSH $ ufw limit 2222/tcp # Allow MySQL only from app server $ ufw allow from 10.0.1.10 to any port 3306 $ ufw enable $ ufw status verbose
/etc/sysconfig/nftables.conf — Advanced nftables ruleset
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Accept established connections
ct state established,related accept
ct state invalid drop
# Loopback
iifname "lo" accept
# ICMP (limited)
ip protocol icmp icmp type { echo-request, destination-unreachable, time-exceeded } limit rate 10/second accept
ip6 nexthdr icmpv6 icmpv6 type { echo-request, nd-neighbor-solicit } accept
# SSH with rate limiting
tcp dport 2222 ct state new limit rate 3/minute accept
# Web
tcp dport { 80, 443 } accept
# Log and drop everything else
log prefix "nftables-drop: " limit rate 10/minute drop
}
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
}
👥 RESPONSIBLE
Linux/System AdminNetwork Admin
✅ VERIFICATION
firewall-cmd --list-all
→ target: DROP
→ ports: 2222/tcp 80/tcp 443/tcp
→ (no 3306, 6379, or other internal ports)
nmap -sS -p 1-65535 your-server-ip
→ PORT STATE SERVICE
→ 80/tcp open http
→ 443/tcp open https
→ 2222/tcp open EtherNetIP-1
→ All other ports: filtered/closed ✓
nc -zv your-server-ip 3306
→ Connection refused ✓ (MySQL not exposed)
📋 VAPT CLOSURE
🔒
Audit Closure
Firewall is active (firewalld/ufw/nftables) with default-deny inbound policy. Only ports 80, 443, and custom SSH port are accessible externally. Database (3306), cache (6379), and internal API ports are restricted to specific source IPs. SSH connections are rate-limited. Port scan confirms only approved services are reachable. Firewall rules are persistent across reboots.⌄
⚠ ISSUE
Without fail2ban, brute-force attacks against SSH, web applications, FTP, and SMTP can run indefinitely — making thousands of attempts per hour without any automated response or IP blocking.
🔥 RISK
Credential brute-force success via unthrottled login attempts. A typical 8-character password can be brute-forced at 10k attempts/minute without fail2ban protection.
🔧 FIX — /etc/fail2ban/jail.local
/etc/fail2ban/jail.local — Comprehensive jail config
[DEFAULT] bantime = 3600 # 1 hour ban findtime = 600 # 10 min window maxretry = 5 # 5 failures triggers ban banaction = iptables-multiport backend = systemd # use journald for modern systems # Whitelist your admin IPs — NEVER get locked out ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 # Send alert emails destemail = security@your-org.com sender = fail2ban@your-server.com mta = sendmail action = %(action_mwl)s # ban + email with log lines # ─── SSH ────────────────────────────────────────────────────── [sshd] enabled = true port = 2222 # your SSH port filter = sshd logpath = %(sshd_log)s maxretry = 3 bantime = 86400 # 24h ban for SSH (more strict) # ─── Nginx HTTP Auth ────────────────────────────────────────── [nginx-http-auth] enabled = true filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log # ─── Nginx Limit Request ───────────────────────────────────── [nginx-limit-req] enabled = true filter = nginx-limit-req port = http,https logpath = /var/log/nginx/error.log maxretry = 10 # ─── Nginx Bad Bots ────────────────────────────────────────── [nginx-botsearch] enabled = true filter = nginx-botsearch port = http,https logpath = /var/log/nginx/access.log maxretry = 2 bantime = 86400 # ─── PAM generic authentication ────────────────────────────── [pam-generic] enabled = true filter = pam-generic logpath = /var/log/auth.log maxretry = 5
Shell — Deploy and test
# Start fail2ban $ systemctl enable --now fail2ban # Verify jails are active $ fail2ban-client status # Check sshd jail $ fail2ban-client status sshd # Manually ban/unban an IP $ fail2ban-client set sshd banip 1.2.3.4 $ fail2ban-client set sshd unbanip 1.2.3.4 # Test a jail filter against log $ fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
fail2ban-client status
→ Number of jail: 5
→ Jail list: sshd, nginx-http-auth, nginx-limit-req, nginx-botsearch, pam-generic
fail2ban-client status sshd
→ Filter: File list, Currently failed: 0, Total failed: X
→ Actions: Currently banned: X, Total banned: X
iptables -L f2b-sshd -n --line-numbers
→ Lists currently banned IPs ✓
📋 VAPT CLOSURE
🔒
Audit Closure
Fail2ban is active with 5 configured jails covering SSH, Nginx HTTP auth, Nginx rate limiting, bot detection, and PAM authentication. SSH jail enforces a 24-hour ban after 3 failed attempts within 10 minutes. Admin IPs are whitelisted to prevent accidental lockout. Email alerts are configured for ban events. fail2ban-client status confirms all jails are active and running.⌄
⚠ ISSUE
Default Linux kernel network parameters leave the system vulnerable to IP spoofing, ICMP redirect attacks, SYN flood DDoS, and source routing. Kernel core dump settings may also expose sensitive memory.
🔥 RISK
IP spoofing enabling traffic hijacking, ICMP redirect attacks rerouting packets, SYN flood exhausting connection tables (DoS), and kernel core dumps exposing cryptographic keys and passwords in memory.
🔧 FIX — /etc/sysctl.d/99-security.conf
/etc/sysctl.d/99-security.conf
# ═══════════════════════════════════════════════════════════════ # Linux Kernel Security Parameters — CIS Benchmark L2 compliant # Apply with: sysctl --system or sysctl -p /etc/sysctl.d/99-security.conf # ═══════════════════════════════════════════════════════════════ # ─── Network: Anti-Spoofing ─────────────────────────────────── net.ipv4.conf.all.rp_filter = 1 # reverse path filtering net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.accept_source_route = 0 # no source routing net.ipv4.conf.default.accept_source_route= 0 net.ipv6.conf.all.accept_source_route = 0 net.ipv6.conf.default.accept_source_route= 0 # ─── Network: ICMP Redirect Defense ────────────────────────── net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 # don't send redirects net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.default.secure_redirects = 0 # ─── Network: SYN Flood Protection ─────────────────────────── net.ipv4.tcp_syncookies = 1 # CRITICAL: enable SYN cookies net.ipv4.tcp_syn_retries = 2 net.ipv4.tcp_synack_retries = 2 net.ipv4.tcp_max_syn_backlog = 4096 # ─── Network: Routing ───────────────────────────────────────── net.ipv4.ip_forward = 0 # no packet forwarding net.ipv6.conf.all.forwarding = 0 net.ipv4.conf.all.log_martians = 1 # log impossible source IPs net.ipv4.conf.default.log_martians = 1 # ─── Network: IPv6 Disable (if not used) ───────────────────── net.ipv6.conf.all.disable_ipv6 = 1 # comment out if IPv6 needed net.ipv6.conf.default.disable_ipv6 = 1 # ─── Network: ICMP Control ──────────────────────────────────── net.ipv4.icmp_echo_ignore_broadcasts = 1 # ignore broadcast pings net.ipv4.icmp_ignore_bogus_error_responses= 1 # ─── Kernel: Core Dump Prevention ──────────────────────────── fs.suid_dumpable = 0 # disable SUID core dumps kernel.core_uses_pid = 1 # ─── Kernel: ASLR — Address Space Layout Randomization ─────── kernel.randomize_va_space = 2 # full ASLR (most secure) # ─── Kernel: Restrict dmesg to root ────────────────────────── kernel.dmesg_restrict = 1 # prevents kernel info leak kernel.kptr_restrict = 2 # hide kernel pointers # ─── Kernel: Prevent ptrace abuse ──────────────────────────── kernel.yama.ptrace_scope = 1 # restrict ptrace to parent # ─── Kernel: Restrict perf events ──────────────────────────── kernel.perf_event_paranoid = 3 # most restrictive # ─── Filesystem: Restrict symlink/hardlink abuse ───────────── fs.protected_symlinks = 1 fs.protected_hardlinks = 1 fs.protected_fifos = 2 fs.protected_regular = 2
Shell — Apply sysctl settings
# Apply immediately (does not persist across reboot without the file) $ sysctl --system # or $ sysctl -p /etc/sysctl.d/99-security.conf # Verify specific settings $ sysctl net.ipv4.tcp_syncookies # → net.ipv4.tcp_syncookies = 1 $ sysctl kernel.randomize_va_space # → kernel.randomize_va_space = 2
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.rp_filter kernel.randomize_va_space kernel.dmesg_restrict
→ net.ipv4.tcp_syncookies = 1
→ net.ipv4.conf.all.rp_filter = 1
→ kernel.randomize_va_space = 2
→ kernel.dmesg_restrict = 1
sysctl net.ipv4.conf.all.accept_redirects net.ipv4.conf.all.send_redirects
→ net.ipv4.conf.all.accept_redirects = 0
→ net.ipv4.conf.all.send_redirects = 0
📋 VAPT CLOSURE
🔒
Audit Closure
Kernel network parameters are hardened via /etc/sysctl.d/99-security.conf: SYN cookies enabled (DoS protection), ICMP redirect acceptance disabled, IP source routing disabled, reverse path filtering enabled, IP forwarding disabled, martian packet logging enabled. Kernel security: ASLR at level 2, dmesg restricted to root, kernel pointers hidden, ptrace restricted to parent process. Settings persist across reboots via drop-in sysctl configuration.
AUDIT & FILE INTEGRITY
auditd · AIDE File Integrity · Lynis Security Audit
3 CONTROLS
⌄
⚠ ISSUE
Without auditd, there is no kernel-level tracking of privileged command execution, file access to sensitive locations, user authentication events, or system call abuse. Post-incident forensics is impossible.
🔥 RISK
Undetected unauthorized access, inability to reconstruct attack timelines, compliance failures (PCI-DSS, ISO 27001), and incomplete forensic evidence for incident response.
🔧 FIX — Comprehensive audit rules
/etc/audit/rules.d/99-hardening.rules
## ── Audit Rules — CIS Benchmark + Security Hardening ────────── ## Apply: augenrules --load or service auditd restart # ─── Buffer size ───────────────────────────────────────────── -b 8192 # ─── Authentication events ─────────────────────────────────── -w /var/log/faillog -p wa -k auth_failures -w /var/log/lastlog -p wa -k logins -w /var/run/faillock/ -p wa -k auth_lockout -w /etc/pam.d/ -p wa -k pam_changes -w /etc/nsswitch.conf -p wa -k nsswitch # ─── Privilege escalation ──────────────────────────────────── -w /usr/bin/sudo -p x -k sudo_usage -w /usr/bin/su -p x -k su_usage -w /etc/sudoers -p wa -k sudoers_change -w /etc/sudoers.d/ -p wa -k sudoers_change # ─── Privileged program execution (SUID/SGID) ──────────────── -a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k setuid_exec -a always,exit -F arch=b32 -S execve -C uid!=euid -F euid=0 -k setuid_exec # ─── System administration files ───────────────────────────── -w /etc/passwd -p wa -k identity_change -w /etc/shadow -p wa -k identity_change -w /etc/group -p wa -k identity_change -w /etc/gshadow -p wa -k identity_change -w /etc/hosts -p wa -k network_change -w /etc/ssh/sshd_config -p wa -k sshd_config # ─── Cron and scheduled tasks ──────────────────────────────── -w /etc/crontab -p wa -k cron_change -w /etc/cron.d/ -p wa -k cron_change -w /var/spool/cron/ -p wa -k cron_change # ─── Kernel module loading ─────────────────────────────────── -w /sbin/insmod -p x -k kernel_module -w /sbin/rmmod -p x -k kernel_module -w /sbin/modprobe -p x -k kernel_module -a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k kernel_module_syscall # ─── Time change (log tampering via time shift) ─────────────── -a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time_change -w /etc/localtime -p wa -k time_change # ─── File system mounts ────────────────────────────────────── -a always,exit -F arch=b64 -S mount,umount2 -k fs_mount # ─── File deletion ─────────────────────────────────────────── -a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat -k file_delete # ─── Auditd configuration changes ──────────────────────────── -w /etc/audit/ -p wa -k audit_change -w /etc/libaudit.conf -p wa -k audit_change # ─── Make audit rules immutable (require reboot to change) ─── # WARNING: This line MUST be last. Uncomment when baseline is stable. # -e 2
Shell — Apply and query auditd
# Apply audit rules $ augenrules --load $ systemctl enable --now auditd # Search audit log for specific events $ ausearch -k sudo_usage -ts today | aureport --summary $ ausearch -k identity_change -ts yesterday $ ausearch -m LOGIN -ts today # Generate a readable summary $ aureport --summary -ts today # Watch for privilege escalation in real time $ ausearch -k setuid_exec -ts recent --format text | tail -20
👥 RESPONSIBLE
Linux/System AdminSecurity Team
📋 VAPT CLOSURE
🔒
Audit Closure
Linux Audit Framework (auditd) is active with comprehensive rules covering authentication failures, sudo/su usage, SUID program execution, system file modifications (/etc/passwd, /etc/shadow, /etc/sudoers), kernel module loading, cron changes, and time modification. Rules use key labels for easy post-incident searching. Log buffer is 8192 to prevent event drops. Auditd service is enabled and persistent. aureport --summary produces clean summary.⌄
⚠ ISSUE
Attackers who gain access often modify system binaries (ls, ps, netstat) to hide their presence, or inject backdoors into existing executables. Without a file integrity baseline, these modifications go undetected.
🔥 RISK
Silent binary modification for persistence — a replaced /usr/bin/sudo or /bin/login can capture all passwords while appearing legitimate. AIDE detects any unauthorized modification to monitored files.
🔧 FIX — Install AIDE and create baseline
Shell — AIDE setup
# ─── Install ───────────────────────────────────────────────── $ dnf install aide -y # RHEL $ apt install aide -y # Ubuntu # ─── Initialize database baseline (KNOWN-CLEAN SYSTEM) ─────── # IMPORTANT: Do this immediately after a fresh, verified-clean install $ aide --init $ mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz # ─── Run integrity check ───────────────────────────────────── $ aide --check # Changed files, added files, removed files are reported # ─── Update database after legitimate changes ──────────────── # (e.g., after a security update) $ aide --update $ mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
/etc/aide.conf — Key monitoring rules
# Check all: permissions, owner, group, size, mtime, sha512 ALLXTENDED = p+i+n+u+g+s+m+S+sha512+xattrs+acl # Monitor all system binaries /bin ALLXTENDED /sbin ALLXTENDED /usr/bin ALLXTENDED /usr/sbin ALLXTENDED /usr/local/bin ALLXTENDED # Monitor critical config files /etc/ssh/sshd_config ALLXTENDED /etc/passwd ALLXTENDED /etc/shadow ALLXTENDED /etc/sudoers ALLXTENDED /etc/hosts ALLXTENDED /boot ALLXTENDED # Monitor web root (detect webshell uploads) /var/www/html ALLXTENDED # Exclude frequently changing files (reduces false positives) !/var/log !/var/tmp !/tmp !/proc !/sys
Shell — Daily AIDE check via cron with email
# Cron: daily AIDE check at 5 AM $ cat > /etc/cron.d/aide-daily << 'EOF' 0 5 * * * root /usr/bin/aide --check 2>&1 | mail -s "[FIM] AIDE Report $(hostname) $(date +%F)" security@your-org.com EOF # Store AIDE database on read-only medium or separate server # for maximum protection against database tampering
👥 RESPONSIBLE
Linux/System AdminSecurity Team
📋 VAPT CLOSURE
🔒
Audit Closure
AIDE (Advanced Intrusion Detection Environment) is installed with a baseline database initialized on the verified-clean system state. Monitored paths include all system binaries, /etc configuration files, /boot, and the web root. SHA-512 checksums and extended attribute checks are enabled. Daily automated checks run at 5 AM with full reports emailed to the security team. AIDE database is stored separately from the monitored filesystem.⌄
⚠ ISSUE
Without regular security auditing, hardening regressions (e.g., an update re-enabling a disabled service) go undetected. Lynis provides an automated, comprehensive security audit with a hardening score and prioritized findings.
🔥 RISK
Security baseline drift — package updates, configuration changes, or new software installations can silently revert hardening settings. Regular Lynis scans provide a measurable hardening index (target: >80/100).
🔧 FIX — Install and automate Lynis
Shell — Lynis installation and scanning
# ─── Install latest Lynis from source (more current than repos) ─ $ git clone https://github.com/CISOfy/lynis /usr/local/lynis $ ln -s /usr/local/lynis/lynis /usr/local/bin/lynis # Or install from repo: $ dnf install lynis -y # RHEL $ apt install lynis -y # Ubuntu # ─── Full system audit ──────────────────────────────────────── $ lynis audit system # ─── Target specific sections ───────────────────────────────── $ lynis audit system --tests-from-group authentication $ lynis audit system --tests-from-group networking $ lynis audit system --tests-from-group malware # ─── Output to log file ─────────────────────────────────────── $ lynis audit system --logfile /var/log/lynis/lynis.log --report-file /var/log/lynis/lynis-report.dat # ─── Check hardening index (target > 80) ───────────────────── $ lynis show hardening # → Hardening index : 83 (example) # ─── Weekly Lynis scan via cron ────────────────────────────── $ echo '0 6 * * 0 root /usr/local/bin/lynis audit system --quiet --logfile /var/log/lynis/lynis-weekly.log 2>&1 | grep -E "Warning|Suggestion" | mail -s "[Lynis] Weekly Scan $(hostname) $(date +%F)" security@your-org.com' >> /etc/crontab # ─── Custom hardening profile ───────────────────────────────── # /etc/lynis/custom.prf to suppress false positives and set thresholds
👥 RESPONSIBLE
Linux/System AdminSecurity Team
✅ VERIFICATION
lynis show hardening
→ Hardening index : 87
→ Tests performed : 261
→ Plugins enabled : 2
grep "Hardening index" /var/log/lynis/lynis.log | tail -1
→ Hardening index : 87 [████████████████████░░░]
📋 VAPT CLOSURE
🔒
Audit Closure
Lynis security audit tool is installed and a baseline scan has been completed. Hardening index is ≥80 (target threshold). All Critical and High findings from the Lynis report have been remediated. Weekly automated scans are scheduled to detect configuration drift. A custom profile excludes documented false positives. Scan results are retained for 90 days for trend analysis.
INFRASTRUCTURE SECURITY
Auto Updates · LUKS Disk Encryption · Log Management · NTP Security
4 CONTROLS
⌄
⚠ ISSUE
Servers running outdated packages with known CVEs are the primary target of exploit kits and automated attack campaigns. The average time-to-exploit for a critical CVE is now measured in hours after public disclosure.
🔥 RISK
Exploitation of known, patched vulnerabilities (e.g., Log4Shell, Shellshock, Heartbleed) months or years after patches are available due to manual update neglect.
🔧 FIX — Auto security updates (RHEL + Ubuntu)
Shell — RHEL/CentOS: dnf-automatic
# Install
$ dnf install dnf-automatic -y
/etc/dnf/automatic.conf — Security-only updates
[commands] upgrade_type = security # ONLY apply security updates apply_updates = yes # auto-apply (not just download) random_sleep = 360 # random 0-360s delay (spread load) [emitters] emit_via = email email_to = security@your-org.com email_from = dnf-auto@your-server.com [email] email_host = localhost
Shell — Enable and start
# Enable the timer (runs daily) $ systemctl enable --now dnf-automatic.timer # Check next run time $ systemctl list-timers dnf-* # ─── Ubuntu: unattended-upgrades ────────────────────────────── $ apt install unattended-upgrades apt-listchanges -y $ dpkg-reconfigure -plow unattended-upgrades # → Select "Yes" to enable # /etc/apt/apt.conf.d/50unattended-upgrades # Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; }; # Unattended-Upgrade::Mail "security@your-org.com"; # Unattended-Upgrade::Automatic-Reboot "false"; ← never auto-reboot in prod
👥 RESPONSIBLE
Linux/System AdminDevOps
📋 VAPT CLOSURE
🔒
Audit Closure
Automatic security-only updates are configured via dnf-automatic (RHEL) or unattended-upgrades (Ubuntu). Updates apply daily with security team email notification. Automatic reboots are disabled — kernel updates requiring reboot are flagged for manual scheduling during a maintenance window. All currently installed packages are at their latest security-patched versions (verified via dnf check-update --security / apt-get --just-print upgrade).⌄
⚠ ISSUE
Unencrypted disks can be removed from a server (physical or cloud snapshot) and mounted on an attacker's system to read all data — including /etc/shadow password hashes, database files, SSL private keys, and application secrets.
🔥 RISK
Complete data theft via physical disk theft or cloud snapshot, offline password cracking of /etc/shadow via direct disk access, and private key extraction bypassing all OS-level access controls.
🔧 FIX — LUKS2 encryption for data partitions
Shell — Encrypt a data partition with LUKS2
⚠ WARNING: THIS DESTROYS ALL DATA ON THE PARTITION Run on a fresh partition or after backing up data # ─── Encrypt a data partition ───────────────────────────────── $ cryptsetup luksFormat \ --type luks2 \ --cipher aes-xts-plain64 \ --key-size 512 \ # 256-bit key (512 = AES-256 + XTS) --hash sha512 \ --iter-time 5000 \ # 5 second key derivation (PBKDF2) --pbkdf argon2id \ # Argon2id KDF (strongest) /dev/sdX1 # ─── Open the encrypted volume ──────────────────────────────── $ cryptsetup luksOpen /dev/sdX1 data_encrypted # ─── Create filesystem ──────────────────────────────────────── $ mkfs.ext4 /dev/mapper/data_encrypted $ mount /dev/mapper/data_encrypted /var/data # ─── Configure auto-mount via /etc/crypttab ─────────────────── $ echo "data_encrypted /dev/sdX1 none luks,discard" >> /etc/crypttab $ echo "/dev/mapper/data_encrypted /var/data ext4 defaults 0 2" >> /etc/fstab # ─── View LUKS header information ───────────────────────────── $ cryptsetup luksDump /dev/sdX1 # ─── Verify encryption is LUKS2 with AES-256 ───────────────── $ cryptsetup luksDump /dev/sdX1 | grep -E "Version|Cipher|Key" # ─── Add a backup key slot (for recovery) ──────────────────── $ cryptsetup luksAddKey /dev/sdX1 # prompts for new passphrase # ─── Backup LUKS header (CRITICAL for recovery) ─────────────── $ cryptsetup luksHeaderBackup /dev/sdX1 --header-backup-file /secure/backup/luks-header-sdX1.bin # Store the header backup OFFSITE and securely
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
cryptsetup status data_encrypted
→ /dev/mapper/data_encrypted is active and is in use.
→ type: LUKS2
→ cipher: aes-xts-plain64
→ keysize: 512 bits
→ device: /dev/sdX1
cryptsetup luksDump /dev/sdX1 | grep -E "Version|Cipher|Key size"
→ Version: 2
→ Cipher name: aes
→ Key size: 512 bits
📋 VAPT CLOSURE
🔒
Audit Closure
Data partitions are encrypted with LUKS2 using AES-256-XTS cipher with Argon2id key derivation (5000ms iteration time). cryptsetup luksDump confirms LUKS2 version, correct cipher, and 512-bit key size. LUKS header backup is stored offsite in a secure location. A recovery key slot is documented and secured in the organization's password vault. Disk encryption protects data-at-rest against physical theft and snapshot attacks.⌄
⚠ ISSUE
Logs stored only on the server being attacked are easily tampered with by an attacker who has gained access. Default log rotation may delete logs before they are reviewed. No central log aggregation means correlation across servers is impossible.
🔥 RISK
Post-incident forensics blocked by log deletion or tampering, compliance failures (PCI-DSS Req 10, ISO 27001 A.12.4), and inability to detect attacks spanning multiple systems without centralized logs.
🔧 FIX — rsyslog forwarding + journald persistence
/etc/systemd/journald.conf — Persistent journal storage
[Journal] Storage=persistent # survive reboots (not just in-memory) Compress=yes # compress old logs SystemMaxUse=2G # max disk usage SystemKeepFree=1G # always keep 1G free MaxRetentionSec=7776000 # 90 days retention RateLimitInterval=30s RateLimitBurst=10000
/etc/rsyslog.d/99-forward.conf — Forward to central SIEM
# ─── Forward all logs to central syslog/SIEM ───────────────── # Load required modules module(load="imjournald") # read from journald module(load="omfwd") # forward module # Forward ALL logs via TLS to central syslog (port 6514) *.* action( type="omfwd" target="siem.your-org.com" port="6514" protocol="tcp" # TLS encryption for log transport StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="anon" ) # Local copy — also retain logs locally auth,authpriv.* /var/log/auth.log *.*;auth,authpriv.none -/var/log/syslog daemon.* -/var/log/daemon.log kern.* -/var/log/kern.log
/etc/logrotate.d/custom — Log retention policy
/var/log/nginx/*.log
/var/log/auth.log
/var/log/syslog {
daily
rotate 90 # keep 90 days
compress
delaycompress
missingok
notifempty
sharedscripts
postrotate
systemctl reload rsyslog
endscript
}
👥 RESPONSIBLE
Linux/System AdminSecurity Team
📋 VAPT CLOSURE
🔒
Audit Closure
systemd-journald is configured with persistent storage (survives reboots) and 90-day retention. All logs are forwarded via TLS-encrypted rsyslog to the central SIEM. Local log rotation retains 90 days of compressed logs. Log integrity is protected by the central forwarding — an attacker who compromises the server cannot delete or modify already-forwarded logs. logrotate confirms 90-day rotation policy is active.⌄
⚠ ISSUE
Incorrect system time causes TLS certificate validation failures, Kerberos authentication errors, log timestamp corruption (making forensics impossible), and enables replay attacks. NTP traffic can also be abused for DDoS amplification.
🔥 RISK
Time drift invalidates TLS certificates causing service outages, log timestamps become unreliable for forensics, and NTP reflection attacks when monlist is enabled (amplification factor 200x-700x).
🔧 FIX — Chrony with security hardening
/etc/chrony.conf — Secure NTP configuration
# ─── Use trusted NTP pool servers ──────────────────────────── # Use iburst for fast initial sync, prefer pool.ntp.org or vendor NTP server 0.pool.ntp.org iburst server 1.pool.ntp.org iburst server 2.pool.ntp.org iburst server 3.pool.ntp.org iburst # Or use internal NTP server (more control): # server ntp.your-org.com iburst # ─── Security: restrict NTP client access ──────────────────── # Only allow local system to query chrony (not act as server) allow 127.0.0.1 deny all # block external NTP queries to this server # ─── Disable monlist (prevents DDoS amplification) ─────────── # Not applicable in chrony (only ntpd has monlist) - chrony is safe # ─── Drift file (persist clock corrections) ────────────────── driftfile /var/lib/chrony/drift # ─── Log to syslog ─────────────────────────────────────────── log tracking measurements statistics logdir /var/log/chrony # ─── Makestep: correct large time differences (initial sync) ─ makestep 1.0 3 # step if off by > 1s, first 3 updates only # ─── RTC synchronization ───────────────────────────────────── rtcsync # ─── Stratum limit (reject inaccurate sources) ─────────────── maxdistance 1.0
Shell — Enable and verify
# Disable ntpd if running (use only chrony) $ systemctl disable --now ntpd ntp 2>/dev/null || true $ systemctl disable --now systemd-timesyncd 2>/dev/null || true # Enable chrony $ systemctl enable --now chronyd # Check sync status $ chronyc tracking # List NTP sources and their status $ chronyc sources -v # Verify firewall blocks external NTP queries $ firewall-cmd --list-all | grep ntp # Should show NTP NOT in allowed services (server-to-server only)
👥 RESPONSIBLE
Linux/System Admin
✅ VERIFICATION
chronyc tracking
→ Reference ID : X.X.X.X (pool.ntp.org)
→ Stratum : 3
→ System time : 0.000012345 seconds fast of NTP time
→ Last offset : -0.000000100 seconds
→ RMS offset : 0.000027000 seconds
timedatectl status
→ NTP service: active
→ NTP synchronized: yes
→ System clock synchronized: yes
📋 VAPT CLOSURE
🔒
Audit Closure
Chrony replaces ntpd/systemd-timesyncd as the sole NTP daemon. External NTP queries are blocked at the firewall (server acts as client only, not server). chronyc tracking confirms successful synchronization with sub-millisecond accuracy. System time is verified as correct and NTP-synchronized via timedatectl. Accurate timestamps ensure log integrity and forensic reliability across all audit trails.