CIPHER Ultimate Hardening Reference Guide
CIPHER Ultimate Hardening Reference Guide
Classification: TRAINING REFERENCE — Definitive hardening configurations for production systems Version: 1.0 — 2026-03-14 Sources: CIS Benchmarks, DISA STIGs, Mozilla TLS Guidelines, OWASP Cheat Sheets, madaidans-insecurities, ANSSI, NIST SP 800-series, NSA Cybersecurity Advisories
Table of Contents
- Linux Server Hardening (Ubuntu/RHEL)
- Windows Server Hardening
- SSH Hardening
- TLS/SSL Hardening
- Web Server Hardening (NGINX/Apache)
- Database Hardening (MySQL/PostgreSQL)
- Application Framework Hardening
- Docker Container Hardening
- Kubernetes Cluster Hardening
1. Linux Server Hardening
Applicable to: Ubuntu 22.04/24.04 LTS, RHEL 8/9, Debian 12 Standards: CIS Benchmark L1/L2, DISA STIG, ANSSI Configuration Recommendations
1.1 Kernel Hardening — sysctl Parameters
Place in /etc/sysctl.d/99-hardening.conf:
# === Kernel Self-Protection ===
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.printk = 3 3 3 3
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
dev.tty.ldisc_autoload = 0
vm.unprivileged_userfaultfd = 0
kernel.kexec_load_disabled = 1
kernel.sysrq = 4
kernel.unprivileged_userns_clone = 0
kernel.perf_event_paranoid = 3
kernel.yama.ptrace_scope = 2
kernel.core_pattern = |/bin/false
fs.suid_dumpable = 0
# === Memory Protections ===
vm.mmap_rnd_bits = 32
vm.mmap_rnd_compat_bits = 16
vm.swappiness = 1
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
# === Network Hardening — IPv4 ===
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.icmp_echo_ignore_all = 1
net.ipv4.tcp_sack = 0
net.ipv4.tcp_dsack = 0
net.ipv4.tcp_fack = 0
net.ipv4.tcp_timestamps = 0
# === Network Hardening — IPv6 ===
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
net.ipv6.conf.all.use_tempaddr = 2
net.ipv6.conf.default.use_tempaddr = 2
Apply: sysctl --system
1.2 Boot Parameters (GRUB)
Edit /etc/default/grub, append to GRUB_CMDLINE_LINUX:
slab_nomerge init_on_alloc=1 init_on_free=1 page_alloc.shuffle=1 pti=on randomize_kstack_offset=on vsyscall=none debugfs=off oops=panic module.sig_enforce=1 lockdown=confidentiality mce=0 quiet loglevel=0
CPU mitigations (append for maximum security, accept ~5-30% performance cost):
spectre_v2=on spec_store_bypass_disable=on tsx=off tsx_async_abort=full,nosmt mds=full,nosmt l1tf=full,force nosmt=force kvm.nx_huge_pages=force
DMA protection (if applicable):
intel_iommu=on amd_iommu=on efi=disable_early_pci_dma
Regenerate:
# Ubuntu/Debian
update-grub
# RHEL
grub2-mkconfig -o /boot/grub2/grub.cfg
1.3 GRUB Bootloader Password
grub-mkpasswd-pbkdf2
# Record hash output
Create /etc/grub.d/40_password:
#!/bin/sh
cat <<EOF
set superusers="grubadmin"
password_pbkdf2 grubadmin grub.pbkdf2.sha512.10000.<hash>
EOF
chmod 700 /etc/grub.d/40_password
update-grub
1.4 Kernel Module Blacklisting
Create /etc/modprobe.d/hardening-blacklist.conf:
# Unnecessary network protocols
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
install n-hdlc /bin/false
install ax25 /bin/false
install netrom /bin/false
install x25 /bin/false
install rose /bin/false
install decnet /bin/false
install econet /bin/false
install af_802154 /bin/false
install ipx /bin/false
install appletalk /bin/false
install psnap /bin/false
install p8023 /bin/false
install p8022 /bin/false
install can /bin/false
install atm /bin/false
# Unnecessary filesystems
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install squashfs /bin/false
install udf /bin/false
# Network filesystems (if not needed)
install cifs /bin/true
install nfs /bin/true
install nfsv3 /bin/true
install nfsv4 /bin/true
install ksmbd /bin/true
install gfs2 /bin/true
# Wireless/Bluetooth (if server)
install bluetooth /bin/false
install btusb /bin/false
# FireWire/Thunderbolt (attack surface)
install firewire-core /bin/false
install thunderbolt /bin/false
# Camera (servers)
install uvcvideo /bin/false
1.5 Filesystem Hardening
/etc/fstab mount options:
proc /proc proc nosuid,nodev,noexec,hidepid=2,gid=proc 0 0
/dev/sda2 / ext4 defaults 1 1
/dev/sda3 /home ext4 defaults,nosuid,noexec,nodev 1 2
/dev/sda4 /tmp ext4 defaults,nosuid,noexec,nodev 1 2
/dev/sda5 /var ext4 defaults,nosuid 1 2
/dev/sda1 /boot ext4 defaults,nosuid,noexec,nodev 1 2
tmpfs /dev/shm tmpfs defaults,noexec,nodev,nosuid 0 0
tmpfs /run tmpfs defaults,noexec,nodev,nosuid,size=512M 0 0
Key mount options:
nosuid— ignore SUID/SGID bitsnoexec— prevent binary executionnodev— ignore device fileshidepid=2— users can only see their own processes
Directory permissions:
chmod 700 /home/*
chmod 700 /boot /usr/src /lib/modules /usr/lib/modules
chmod 600 /etc/shadow /etc/gshadow
chmod 644 /etc/passwd /etc/group
chmod 600 /etc/crontab
chmod 700 /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
Default umask — set in /etc/profile and /etc/login.defs:
umask 0077
1.6 Core Dumps Disabled
/etc/security/limits.conf:
* hard core 0
/etc/systemd/coredump.conf.d/disable.conf:
[Coredump]
Storage=none
/etc/sysctl.d/99-hardening.conf (already included above):
fs.suid_dumpable = 0
kernel.core_pattern = |/bin/false
1.7 User Account Security
Lock root login:
passwd -l root
Empty /etc/securetty to prevent root TTY login.
Restrict su — /etc/pam.d/su and /etc/pam.d/su-l:
auth required pam_wheel.so use_uid
Login delay — /etc/pam.d/system-login:
auth optional pam_faildelay.so delay=4000000
Password complexity — /etc/pam.d/passwd or /etc/security/pwquality.conf:
password required pam_pwquality.so retry=2 minlen=16 difok=6 dcredit=-3 ucredit=-2 lcredit=-2 ocredit=-3 enforce_for_root
password required pam_unix.so use_authtok sha512 shadow rounds=65536
/etc/login.defs:
PASS_MAX_DAYS 90
PASS_MIN_DAYS 7
PASS_WARN_AGE 14
LOGIN_RETRIES 3
LOGIN_TIMEOUT 60
ENCRYPT_METHOD SHA512
SHA_CRYPT_ROUNDS 65536
UMASK 077
Account lockout — /etc/pam.d/common-auth (Ubuntu) or /etc/pam.d/system-auth (RHEL):
auth required pam_faillock.so preauth silent deny=5 unlock_time=900 fail_interval=900
auth required pam_faillock.so authfail deny=5 unlock_time=900 fail_interval=900
1.8 Automatic Security Updates
Ubuntu/Debian — /etc/apt/apt.conf.d/20auto-upgrades:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
/etc/apt/apt.conf.d/50unattended-upgrades:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
APT sandboxing — /etc/apt/apt.conf.d/40sandbox:
APT::Sandbox::Seccomp "true";
RHEL — enable automatic security updates:
dnf install dnf-automatic
/etc/dnf/automatic.conf:
[commands]
upgrade_type = security
apply_updates = yes
systemctl enable --now dnf-automatic.timer
1.9 Firewall (UFW / firewalld)
UFW (Ubuntu):
ufw default deny incoming
ufw default allow outgoing
ufw limit ssh
ufw allow 443/tcp
ufw enable
ufw logging on
firewalld (RHEL):
firewall-cmd --set-default-zone=drop
firewall-cmd --zone=drop --add-service=ssh --permanent
firewall-cmd --zone=drop --add-service=https --permanent
firewall-cmd --reload
nftables (modern replacement) — /etc/nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
ct state invalid drop
tcp dport 22 ct state new limit rate 4/minute accept
tcp dport 443 ct state new accept
icmp type echo-request limit rate 1/second accept
counter drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
1.10 Fail2Ban
/etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = nftables-multiport
backend = systemd
ignoreip = 127.0.0.1/8 ::1
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 86400
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
maxretry = 3
bantime = 3600
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
maxretry = 5
bantime = 7200
1.11 Auditd Rules
/etc/audit/rules.d/hardening.rules:
# Delete all existing rules
-D
# Buffer size
-b 8192
# Failure mode (2 = panic, 1 = printk)
-f 1
# Time changes
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
-a always,exit -F arch=b64 -S clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
# User/group changes
-w /etc/group -p wa -k identity
-w /etc/passwd -p wa -k identity
-w /etc/gshadow -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/security/opasswd -p wa -k identity
# Network configuration
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k system-locale
-w /etc/issue -p wa -k system-locale
-w /etc/issue.net -p wa -k system-locale
-w /etc/hosts -p wa -k system-locale
-w /etc/hostname -p wa -k system-locale
# Login/logout events
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock/ -p wa -k logins
-w /var/log/wtmp -p wa -k logins
-w /var/log/btmp -p wa -k logins
# Session initiation
-w /var/run/utmp -p wa -k session
-w /var/log/wtmp -p wa -k session
-w /var/log/btmp -p wa -k session
# Discretionary access control changes
-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -k perm_mod
-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -k perm_mod
-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -k perm_mod
# Unauthorized access attempts
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -k access
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -k access
# Privileged commands (generate per system)
# find / -xdev -type f -perm -4000 -o -type f -perm -2000 2>/dev/null
-a always,exit -F path=/usr/bin/sudo -F perm=x -k privileged
-a always,exit -F path=/usr/bin/su -F perm=x -k privileged
-a always,exit -F path=/usr/bin/passwd -F perm=x -k privileged
-a always,exit -F path=/usr/bin/chage -F perm=x -k privileged
-a always,exit -F path=/usr/bin/gpasswd -F perm=x -k privileged
-a always,exit -F path=/usr/bin/newgrp -F perm=x -k privileged
# Kernel module loading
-a always,exit -F arch=b64 -S init_module -S finit_module -S delete_module -k modules
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules
# Sudoers changes
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# SSH configuration changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
# Cron changes
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /etc/cron.daily/ -p wa -k cron
-w /etc/cron.hourly/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
# Make configuration immutable (must be last)
-e 2
Load: augenrules --load
1.12 Mandatory Access Control
AppArmor (Ubuntu/Debian):
apt install apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra
# Boot parameters:
# apparmor=1 security=apparmor
# Enforce all loaded profiles:
aa-enforce /etc/apparmor.d/*
# Generate profile for new application:
aa-genprof /path/to/application
SELinux (RHEL):
# /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted
# Boot parameters:
# selinux=1 security=selinux
# Verify:
getenforce # Should return "Enforcing"
sestatus # Full status
# Troubleshoot:
sealert -a /var/log/audit/audit.log
1.13 File Integrity Monitoring (AIDE)
# Ubuntu
apt install aide
aideinit
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# RHEL
dnf install aide
aide --init
cp /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
# Check:
aide --check
# Cron (daily):
0 5 * * * /usr/bin/aide --check | mail -s "AIDE Report" admin@example.com
1.14 Systemd Service Sandboxing Template
Apply to all custom services — /etc/systemd/system/<service>.service.d/hardening.conf:
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectHostname=true
ProtectClock=true
ProtectProc=invisible
ProcSubset=pid
PrivateTmp=true
PrivateUsers=true
PrivateDevices=true
PrivateIPC=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
UMask=0077
IPAddressDeny=any
Verify hardening score: systemd-analyze security <service>
1.15 USBGuard (Physical Attack Surface)
# Install
apt install usbguard # or dnf install usbguard
# Generate initial policy from currently connected devices
usbguard generate-policy > /etc/usbguard/rules.conf
# Enable
systemctl enable --now usbguard
# Example policy — allow only keyboard and storage
allow id 1d6b:0002 name "xHCI Host Controller"
reject via-port "*"
1.16 Hardened Memory Allocator
System-wide — /etc/ld.so.preload:
/usr/lib/libhardened_malloc.so
Per-application:
LD_PRELOAD="/usr/lib/libhardened_malloc.so" /usr/bin/application
1.17 Intrusion Detection Stack
| Tool | Purpose | Install |
|---|---|---|
| AIDE | File integrity monitoring | apt install aide |
| Fail2Ban | Brute-force prevention | apt install fail2ban |
| CrowdSec | Community threat intelligence | curl -s https://install.crowdsec.net | bash |
| OSSEC/Wazuh | HIDS + log analysis | Agent-based deployment |
| Lynis | Security auditing | apt install lynis && lynis audit system |
| rkhunter | Rootkit detection | apt install rkhunter && rkhunter --check |
| chkrootkit | Rootkit detection | apt install chkrootkit && chkrootkit |
| ClamAV | Malware scanning | apt install clamav clamav-daemon |
1.18 Automation with Ansible Lockdown
# CIS Benchmark automation
ansible-galaxy install ansible-lockdown.UBUNTU22-CIS
ansible-galaxy install ansible-lockdown.RHEL9-CIS
# DISA STIG automation
ansible-galaxy install ansible-lockdown.UBUNTU22-STIG
ansible-galaxy install ansible-lockdown.RHEL9-STIG
# Audit with Goss
ansible-galaxy install ansible-lockdown.UBUNTU22-CIS-Audit
ansible-galaxy install ansible-lockdown.RHEL9-CIS-Audit
2. Windows Server Hardening
Applicable to: Windows Server 2019/2022/2025 Standards: CIS Benchmark, Microsoft Security Baselines, DISA STIGs, NSA Guidance
2.1 Account Policies
Password Policy (via Group Policy or secpol.msc):
Enforce password history: 24 passwords
Maximum password age: 60 days
Minimum password age: 1 day
Minimum password length: 14 characters
Password must meet complexity requirements: Enabled
Store passwords using reversible encryption: Disabled
Account Lockout Policy:
Account lockout duration: 30 minutes
Account lockout threshold: 5 invalid logon attempts
Reset account lockout counter after: 30 minutes
2.2 Audit Policies
Advanced Audit Policy (via auditpol /set):
auditpol /set /subcategory:"Credential Validation" /success:enable /failure:enable
auditpol /set /subcategory:"Logon" /success:enable /failure:enable
auditpol /set /subcategory:"Logoff" /success:enable
auditpol /set /subcategory:"Account Lockout" /success:enable /failure:enable
auditpol /set /subcategory:"Special Logon" /success:enable
auditpol /set /subcategory:"Process Creation" /success:enable
auditpol /set /subcategory:"Audit Policy Change" /success:enable /failure:enable
auditpol /set /subcategory:"Authentication Policy Change" /success:enable
auditpol /set /subcategory:"Security Group Management" /success:enable
auditpol /set /subcategory:"User Account Management" /success:enable /failure:enable
auditpol /set /subcategory:"Sensitive Privilege Use" /success:enable /failure:enable
auditpol /set /subcategory:"Security System Extension" /success:enable
auditpol /set /subcategory:"System Integrity" /success:enable /failure:enable
Enable command-line process auditing:
Computer Configuration > Administrative Templates > System > Audit Process Creation > Include command line in process creation events: Enabled
2.3 User Rights Assignment
Access this computer from the network: Administrators, Authenticated Users
Allow log on locally: Administrators
Deny access to this computer from the network: Guests, Local account
Deny log on through Remote Desktop Services: Guests, Local account
2.4 Security Options
Accounts: Administrator account status: Disabled (rename first)
Accounts: Guest account status: Disabled
Accounts: Rename administrator account: <custom_name>
Accounts: Rename guest account: <custom_name>
Interactive logon: Do not display last user name: Enabled
Interactive logon: Machine inactivity limit: 900 seconds
Network access: Do not allow anonymous enumeration of SAM accounts: Enabled
Network access: Do not allow anonymous enumeration of SAM accounts and shares: Enabled
Network security: LAN Manager authentication level: Send NTLMv2 response only. Refuse LM & NTLM
Network security: Minimum session security for NTLM SSP: Require NTLMv2 session security, Require 128-bit encryption
User Account Control: Admin Approval Mode for Built-in Administrator: Enabled
User Account Control: Behavior of elevation prompt for administrators: Prompt for consent on the secure desktop
User Account Control: Run all administrators in Admin Approval Mode: Enabled
2.5 Windows Firewall
# Enable for all profiles
Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True -DefaultInboundAction Block -DefaultOutboundAction Allow -LogAllowed True -LogBlocked True -LogMaxSizeKilobytes 16384
# Block inbound by default
Set-NetFirewallProfile -Profile Public -AllowInboundRules False
# Allow specific services
New-NetFirewallRule -DisplayName "RDP" -Direction Inbound -Protocol TCP -LocalPort 3389 -Action Allow -Profile Domain -RemoteAddress "10.0.0.0/8"
New-NetFirewallRule -DisplayName "WinRM" -Direction Inbound -Protocol TCP -LocalPort 5985,5986 -Action Allow -Profile Domain -RemoteAddress "10.0.0.0/8"
2.6 SMB Hardening
# Disable SMBv1
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force
# Enforce SMB signing
Set-SmbServerConfiguration -RequireSecuritySignature $true -Force
Set-SmbClientConfiguration -RequireSecuritySignature $true -Force
# Enforce SMB encryption (SMBv3)
Set-SmbServerConfiguration -EncryptData $true -Force
Set-SmbServerConfiguration -RejectUnencryptedAccess $true -Force
2.7 PowerShell Security
# Enable script block logging
# Group Policy: Administrative Templates > Windows Components > Windows PowerShell
# Turn on PowerShell Script Block Logging: Enabled
# Enable module logging
# Turn on Module Logging: Enabled (all modules: *)
# Enable transcription
# Turn on PowerShell Transcription: Enabled
# Include invocation headers: Enabled
# Constrained Language Mode (for non-admin users)
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '4', 'Machine')
# Execution policy
Set-ExecutionPolicy RemoteSigned -Force
2.8 AppLocker / WDAC
# AppLocker — default deny with whitelisting
# Export baseline rules:
Get-AppLockerPolicy -Effective -Xml > C:\AppLockerPolicy.xml
# WDAC (Windows Defender Application Control) — preferred over AppLocker
New-CIPolicy -Level Publisher -FilePath "C:\WDAC\BasePolicy.xml" -UserPEs -Fallback Hash
ConvertFrom-CIPolicy "C:\WDAC\BasePolicy.xml" "C:\WDAC\BasePolicy.bin"
2.9 Windows Defender Configuration
Set-MpPreference -DisableRealtimeMonitoring $false
Set-MpPreference -MAPSReporting Advanced
Set-MpPreference -SubmitSamplesConsent SendAllSamples
Set-MpPreference -PUAProtection Enabled
Set-MpPreference -EnableNetworkProtection Enabled
Set-MpPreference -AttackSurfaceReductionRules_Ids @(
"BE9BA2D9-53EA-4CDC-84E5-9B1EEEE46550", # Block executable content from email
"D4F940AB-401B-4EFC-AADC-AD5F3C50688A", # Block Office child processes
"3B576869-A4EC-4529-8536-B80A7769E899", # Block Office from creating executables
"75668C1F-73B5-4CF0-BB93-3ECF5CB7CC84", # Block Office injection
"D3E037E1-3EB8-44C8-A917-57927947596D", # Block JS/VBS launching executables
"5BEB7EFE-FD9A-4556-801D-275E5FFC04CC", # Block execution of obfuscated scripts
"92E97FA1-2EDF-4476-BDD6-9DD0B4DDDC7B", # Block Win32 API from Office macros
"01443614-CD74-433A-B99E-2ECDC07BFC25" # Block executable files from running unless they meet criteria
) -AttackSurfaceReductionRules_Actions @(1,1,1,1,1,1,1,1)
# Enable Credential Guard
reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard" /v EnableVirtualizationBasedSecurity /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v LsaCfgFlags /t REG_DWORD /d 1 /f
2.10 Registry Hardening
# Disable LLMNR
New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" -Name "EnableMulticast" -Value 0 -PropertyType DWord -Force
# Disable NetBIOS over TCP/IP (per adapter)
$adapters = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True"
$adapters | ForEach-Object { $_.SetTcpipNetbios(2) }
# Disable WPAD
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Wpad" -Name "WpadOverride" -Value 1 -PropertyType DWord -Force
# Disable WDigest (prevent plaintext credential caching)
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest" -Name "UseLogonCredential" -Value 0
# Disable remote assistance
Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Remote Assistance" -Name "fAllowToGetHelp" -Value 0
# Enable LSA protection
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RunAsPPL" -Value 1 -PropertyType DWord -Force
2.11 Windows Event Log Sizing
wevtutil sl Security /ms:1073741824 # 1 GB
wevtutil sl Application /ms:134217728 # 128 MB
wevtutil sl System /ms:134217728 # 128 MB
wevtutil sl "Windows PowerShell" /ms:134217728
wevtutil sl "Microsoft-Windows-PowerShell/Operational" /ms:134217728
wevtutil sl "Microsoft-Windows-Sysmon/Operational" /ms:1073741824
2.12 BitLocker
Enable-BitLocker -MountPoint "C:" -EncryptionMethod XtsAes256 -UsedSpaceOnly -TpmProtector
Add-BitLockerKeyProtector -MountPoint "C:" -RecoveryPasswordProtector
2.13 Automation
# Ansible Lockdown for Windows
ansible-galaxy install ansible-lockdown.Windows-2022-CIS
ansible-galaxy install ansible-lockdown.Windows-2022-STIG
3. SSH Hardening
Standards: NIST IR 7966, ANSSI OpenSSH Recommendations, Mozilla Guidelines
3.1 Server Configuration — /etc/ssh/sshd_config
# === Protocol ===
Protocol 2
# === Network ===
Port 22
AddressFamily inet
ListenAddress 0.0.0.0
# === Authentication ===
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AuthenticationMethods publickey
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
# === Key Exchange ===
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256
# === Host Keys ===
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
# === Ciphers ===
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
# === MACs ===
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
# === Forwarding ===
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no
PermitTunnel no
GatewayPorts no
DisableForwarding yes
# === Access Control ===
AllowGroups ssh-users
DenyUsers root
DenyGroups root
# === Logging ===
SyslogFacility AUTH
LogLevel VERBOSE
# === Security ===
StrictModes yes
PermitUserEnvironment no
IgnoreRhosts yes
HostbasedAuthentication no
RekeyLimit 512M 1h
# === Session ===
ClientAliveInterval 300
ClientAliveCountMax 2
TCPKeepAlive no
# === Banners ===
Banner /etc/issue.net
PrintMotd no
PrintLastLog yes
# === Misc ===
Compression no
UseDNS no
AcceptEnv LANG LC_*
Subsystem sftp internal-sftp
3.2 Remove Weak DH Keys
awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe
mv /etc/ssh/moduli.safe /etc/ssh/moduli
3.3 Regenerate Host Keys (Ed25519 only for max security)
rm -f /etc/ssh/ssh_host_*
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 ""
chmod 600 /etc/ssh/ssh_host_*_key
chmod 644 /etc/ssh/ssh_host_*_key.pub
3.4 Client Configuration — ~/.ssh/config
Host *
HashKnownHosts yes
IdentitiesOnly yes
ServerAliveInterval 300
ServerAliveCountMax 2
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256
KexAlgorithms sntrup761x25519-sha512@openssh.com,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,umac-128-etm@openssh.com
VisualHostKey yes
3.5 SSH Key Generation
# Preferred
ssh-keygen -t ed25519 -a 100 -C "user@host"
# RSA fallback (when Ed25519 not supported)
ssh-keygen -t rsa -b 4096 -a 100 -C "user@host"
3.6 SSH Certificate Authority (Enterprise)
# Generate CA key
ssh-keygen -t ed25519 -f /etc/ssh/ca_key -C "SSH CA"
# Sign host key
ssh-keygen -s /etc/ssh/ca_key -I "hostname" -h -n hostname.example.com -V +52w /etc/ssh/ssh_host_ed25519_key.pub
# Sign user key
ssh-keygen -s /etc/ssh/ca_key -I "username" -n username -V +24h ~/.ssh/id_ed25519.pub
# sshd_config:
TrustedUserCAKeys /etc/ssh/ca_key.pub
# Client known_hosts:
@cert-authority *.example.com <ca_public_key>
4. TLS/SSL Hardening
Standards: Mozilla Server Side TLS, NIST SP 800-52r2, ANSSI TLS Recommendations, Netherlands NCSC TLS Guidelines
4.1 Mozilla Modern Configuration (TLS 1.3 Only)
| Parameter | Value |
|---|---|
| Protocols | TLS 1.3 |
| Cipher Suites | TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 |
| Certificate | ECDSA P-256 |
| Curves | X25519, prime256v1, secp384r1 |
| HSTS | max-age=63072000 (2 years) |
| Cert Lifespan | 90 days |
| Cipher Preference | Client chooses |
4.2 Mozilla Intermediate Configuration (Recommended)
| Parameter | Value |
|---|---|
| Protocols | TLS 1.2 + TLS 1.3 |
| TLS 1.3 Suites | TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 |
| TLS 1.2 Suites | ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 |
| Certificate | ECDSA P-256 (preferred) or RSA 2048-bit |
| Curves | X25519, prime256v1, secp384r1 |
| DH Params | ffdhe2048 (RFC 7919), 2048-bit minimum |
| HSTS | max-age=63072000 |
| Cert Lifespan | 90-366 days |
| Cipher Preference | Client chooses |
4.3 NGINX TLS Configuration (Intermediate)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Protocols
ssl_protocols TLSv1.2 TLSv1.3;
# Ciphers (Intermediate)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# DH Parameters
ssl_dhparam /etc/ssl/dhparam.pem;
# Curves
ssl_ecdh_curve X25519:prime256v1:secp384r1;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Session
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}
4.4 Apache TLS Configuration (Intermediate)
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# Protocols
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
# Ciphers
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder off
# DH Parameters
SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparam.pem"
# Curves
SSLOpenSSLConfCmd Curves X25519:prime256v1:secp384r1
# OCSP Stapling
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
# HSTS
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</VirtualHost>
SSLStaplingCache shmcb:/var/run/ocsp(128000)
4.5 Generate DH Parameters
openssl dhparam -out /etc/ssl/dhparam.pem 2048
chmod 600 /etc/ssl/dhparam.pem
4.6 Certificate Best Practices
- Use ECDSA P-256 certificates (faster, smaller, modern)
- Automate with ACME (Let's Encrypt / certbot)
- 90-day maximum certificate lifespan
- Enable CAA DNS records:
example.com. CAA 0 issue "letsencrypt.org" - Enable Certificate Transparency monitoring
- Pin backup keys via
Expect-CTheader (deprecated but useful for monitoring)
5. Web Server Hardening
5.1 NGINX Hardening
/etc/nginx/conf.d/hardening.conf:
# === Hide version ===
server_tokens off;
more_clear_headers Server;
# === Security Headers ===
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "0" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
# === Request Limits ===
client_max_body_size 10m;
client_body_buffer_size 1k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 15;
send_timeout 10;
# === Rate Limiting ===
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
# === Buffer Overflow Protection ===
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# === Disable unwanted HTTP methods ===
# In server block:
# if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|PATCH)$ ) {
# return 405;
# }
Per-location rate limiting:
location /login {
limit_req zone=login burst=3 nodelay;
limit_conn addr 5;
proxy_pass http://backend;
}
location / {
limit_req zone=general burst=20 nodelay;
proxy_pass http://backend;
}
Deny access to hidden files:
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
5.2 Apache Hardening
/etc/apache2/conf-enabled/hardening.conf or /etc/httpd/conf.d/hardening.conf:
# === Hide version ===
ServerTokens Prod
ServerSignature Off
TraceEnable Off
# === Security Headers ===
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "0"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
Header always unset X-Powered-By
# === Disable directory listing ===
Options -Indexes -Includes -ExecCGI
<Directory />
Options None
AllowOverride None
Require all denied
</Directory>
# === Restrict HTTP methods ===
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>
# === Request limits ===
LimitRequestBody 10485760
LimitRequestFields 50
LimitRequestFieldSize 8190
LimitRequestLine 8190
Timeout 60
KeepAliveTimeout 5
MaxKeepAliveRequests 100
# === Prevent clickjacking ===
Header always append X-Frame-Options DENY
# === Disable ETag (information leak) ===
FileETag None
# === Deny access to hidden files ===
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# === Disable unnecessary modules ===
# a2dismod autoindex status info cgi
mod_security (WAF):
<IfModule security2_module>
SecRuleEngine On
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecRequestBodyLimitAction Reject
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogType Serial
SecAuditLog /var/log/modsec_audit.log
IncludeOptional /etc/modsecurity/crs/*.conf
</IfModule>
5.3 Shared Configuration
Redirect HTTP to HTTPS (NGINX):
server {
listen 80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
}
Redirect HTTP to HTTPS (Apache):
<VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
6. Database Hardening
6.1 PostgreSQL Hardening
postgresql.conf:
# === Network ===
listen_addresses = 'localhost' # Only local connections, or specific IPs
port = 5432
# === Authentication ===
password_encryption = scram-sha-256 # Never md5
# === SSL ===
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
ssl_ca_file = '/etc/ssl/certs/ca.crt'
ssl_min_protocol_version = 'TLSv1.2'
ssl_ciphers = 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'
ssl_prefer_server_ciphers = on
# === Logging ===
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
log_file_mode = 0600
log_connections = on
log_disconnections = on
log_duration = on
log_line_prefix = '%m [%p] %q%u@%d '
log_statement = 'ddl' # Log DDL; use 'all' for high-security
log_min_duration_statement = 1000 # Log queries > 1s
log_checkpoints = on
log_lock_waits = on
log_temp_files = 0
# === Resource Limits ===
max_connections = 100
statement_timeout = 60000 # 60s query timeout
idle_in_transaction_session_timeout = 600000 # 10min
# === Row-Level Security ===
# Enable per-table with: ALTER TABLE t ENABLE ROW LEVEL SECURITY;
pg_hba.conf (host-based authentication):
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
hostssl app_db app_user 10.0.0.0/24 scram-sha-256
# Deny everything else
host all all 0.0.0.0/0 reject
User privilege hardening:
-- Revoke default public schema permissions
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- Application user with minimal privileges
CREATE ROLE app_user WITH LOGIN PASSWORD 'strong_password_here' VALID UNTIL '2027-01-01';
GRANT CONNECT ON DATABASE app_db TO app_user;
GRANT USAGE ON SCHEMA app_schema TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app_schema TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA app_schema GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
-- Read-only user
CREATE ROLE readonly_user WITH LOGIN PASSWORD 'strong_password_here';
GRANT CONNECT ON DATABASE app_db TO readonly_user;
GRANT USAGE ON SCHEMA app_schema TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA app_schema TO readonly_user;
-- Prevent superuser creation by non-superusers
-- (default behavior, verify)
pgaudit extension (audit logging):
# postgresql.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'write,ddl,role'
pgaudit.log_catalog = off
pgaudit.log_parameter = on
pgaudit.log_statement_once = on
6.2 MySQL/MariaDB Hardening
/etc/mysql/mysql.conf.d/hardening.cnf or /etc/my.cnf.d/hardening.cnf:
[mysqld]
# === Network ===
bind-address = 127.0.0.1
port = 3306
skip-networking = 0 # Set to 1 if only socket connections needed
socket = /var/run/mysqld/mysqld.sock
# === Authentication ===
default_authentication_plugin = caching_sha2_password
# MySQL 8.4+: authentication_policy = caching_sha2_password
# === SSL/TLS ===
require_secure_transport = ON
ssl-ca = /etc/mysql/ssl/ca.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem
ssl-key = /etc/mysql/ssl/server-key.pem
tls_version = TLSv1.2,TLSv1.3
ssl_cipher = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
# === Security ===
local-infile = 0
skip-symbolic-links = 1
secure-file-priv = /var/lib/mysql-files
log-raw = OFF
# === Logging ===
general_log = OFF
general_log_file = /var/log/mysql/general.log
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
log_error = /var/log/mysql/error.log
log_error_verbosity = 3
log_bin = /var/log/mysql/mysql-bin
binlog_format = ROW
# === Audit (Enterprise or plugin) ===
# plugin-load-add = audit_log.so
# audit_log_policy = ALL
# audit_log_format = JSON
# === Limits ===
max_connections = 100
max_connect_errors = 10
wait_timeout = 600
interactive_timeout = 600
max_allowed_packet = 16M
# === Performance Schema (for monitoring) ===
performance_schema = ON
Post-install hardening:
mysql_secure_installation
# Answer yes to all prompts:
# - Set root password
# - Remove anonymous users
# - Disallow root login remotely
# - Remove test database
# - Reload privilege tables
User privilege hardening:
-- Remove default accounts
DROP USER IF EXISTS ''@'localhost';
DROP USER IF EXISTS ''@'%';
DROP USER IF EXISTS 'root'@'%';
-- Application user
CREATE USER 'app_user'@'10.0.0.%' IDENTIFIED BY 'strong_password_here' REQUIRE SSL;
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'10.0.0.%';
FLUSH PRIVILEGES;
-- Verify no accounts without passwords
SELECT User, Host FROM mysql.user WHERE authentication_string = '' OR authentication_string IS NULL;
-- Verify no wildcard hosts
SELECT User, Host FROM mysql.user WHERE Host = '%';
7. Application Framework Hardening
7.1 Node.js Security
Express.js hardened setup:
"use strict";
const express = require("express");
const helmet = require("helmet");
const hpp = require("hpp");
const rateLimit = require("express-rate-limit");
const session = require("express-session");
const app = express();
// === Helmet — Security Headers ===
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
fontSrc: ["'self'"],
connectSrc: ["'self'"],
frameAncestors: ["'none'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"]
}
}));
app.use(helmet.hsts({ maxAge: 63072000, includeSubDomains: true, preload: true }));
// === Request Size Limits ===
app.use(express.json({ limit: "1kb" }));
app.use(express.urlencoded({ extended: true, limit: "1kb" }));
// === HTTP Parameter Pollution ===
app.use(hpp());
// === Rate Limiting ===
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false
});
app.use(generalLimiter);
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: "Too many login attempts"
});
app.use("/login", loginLimiter);
// === Session Configuration ===
app.use(session({
secret: process.env.SESSION_SECRET,
name: "__Host-sessionId",
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: "strict",
maxAge: 1800000, // 30 minutes
path: "/",
domain: "example.com"
}
}));
// === Disable fingerprinting ===
app.disable("x-powered-by");
app.disable("etag");
// === Trust proxy (if behind reverse proxy) ===
app.set("trust proxy", 1);
Event loop protection:
const toobusy = require("toobusy-js");
app.use(function(req, res, next) {
if (toobusy()) {
res.status(503).send("Server Too Busy");
} else {
next();
}
});
Uncaught exception handler:
process.on("uncaughtException", function(err) {
console.error("Uncaught Exception:", err);
process.exit(1);
});
process.on("unhandledRejection", function(reason, promise) {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
Node.js Permissions Model (v20+):
node --permission index.js
node --permission --allow-fs-read=/app/uploads/ --allow-fs-write=/app/uploads/ index.js
# Restrict child processes, workers, addons
# --allow-child-process --allow-worker --allow-addons --allow-wasi
Key packages:
| Package | Purpose |
|---|---|
helmet |
Security headers |
hpp |
HTTP parameter pollution |
express-rate-limit |
Rate limiting |
express-mongo-sanitize |
NoSQL injection prevention |
validator |
Input validation |
escape-html |
Output escaping |
node-esapi |
OWASP ESAPI for Node |
toobusy-js |
Event loop protection |
winston / pino |
Structured logging |
Dangerous functions to NEVER use with user input:
eval()child_process.exec()(useexecFile()with argument arrays)new Function()with user datavm.runInNewContext()without sandbox- Unsanitized
fsoperations (path traversal)
7.2 Ruby on Rails Security
config/environments/production.rb:
Rails.application.configure do
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true, preload: true, expires: 63072000 } }
# Session store — use database, not cookies for sensitive apps
config.session_store :active_record_store
# Log level
config.log_level = :warn
# Filter sensitive parameters from logs
config.filter_parameters += [:password, :password_confirmation, :credit_card, :ssn, :token, :secret]
end
app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Security headers
before_action :set_security_headers
private
def set_security_headers
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '0'
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
response.headers['Permissions-Policy'] = 'camera=(), microphone=(), geolocation=()'
end
end
Security headers gem — Gemfile:
gem 'secure_headers'
config/initializers/secure_headers.rb:
SecureHeaders::Configuration.default do |config|
config.csp = {
default_src: %w('self'),
script_src: %w('self'),
style_src: %w('self'),
img_src: %w('self' data:),
font_src: %w('self'),
connect_src: %w('self'),
frame_ancestors: %w('none'),
base_uri: %w('self'),
form_action: %w('self')
}
end
SQL injection prevention — always use parameterized queries:
# SAFE
Project.where("name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(params[:name])}%")
Project.where(name: params[:name])
# VULNERABLE — never do this
Project.where("name LIKE '#{params[:name]}'")
Mass assignment protection:
class UserController < ApplicationController
def create
@user = User.new(user_params)
end
private
def user_params
params.require(:user).permit(:name, :email) # Whitelist only
end
end
Password complexity with zxcvbn — config/initializers/devise.rb:
config.min_password_score = 4
config.stretches = Rails.env.test? ? 1 : 12
CORS — config/application.rb:
config.middleware.use Rack::Cors do
allow do
origins 'app.example.com'
resource '/api/*',
headers: %w[Origin Accept Content-Type Authorization],
methods: [:get, :post, :put, :patch, :delete],
max_age: 600
end
end
Security scanning tools:
brakeman— static analysis for Railsbundler-audit— dependency vulnerability checkingbearer— code security scanner
7.3 Django Security
settings.py production hardening:
import os
# === Core ===
DEBUG = False
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') # 50+ chars, never hardcode
ALLOWED_HOSTS = ['www.example.com', 'example.com']
# === HTTPS ===
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# === HSTS ===
SECURE_HSTS_SECONDS = 63072000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# === Cookies ===
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_AGE = 1800 # 30 minutes
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Strict'
# === Content Security ===
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# === Middleware (order matters) ===
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'csp.middleware.CSPMiddleware', # django-csp package
]
# === Password Validation ===
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': 12}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# === File Uploads ===
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
# === Logging ===
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
'django.security': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
},
}
# === Database ===
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'OPTIONS': {
'sslmode': 'verify-full',
},
}
}
# === CSP (django-csp) ===
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_IMG_SRC = ("'self'", "data:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FORM_ACTION = ("'self'",)
Deployment check:
python manage.py check --deploy
7.4 .NET Security
Program.cs / Startup.cs hardening (ASP.NET Core):
var builder = WebApplication.CreateBuilder(args);
// === Identity/Password Policy ===
builder.Services.Configure<IdentityOptions>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Password.RequiredUniqueChars = 6;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 3;
options.SignIn.RequireConfirmedEmail = true;
options.User.RequireUniqueEmail = true;
});
// === Anti-Forgery (global) ===
builder.Services.AddMvc(options => {
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
// === CORS ===
builder.Services.AddCors(options => {
options.AddPolicy("Strict", policy => {
policy.WithOrigins("https://www.example.com")
.AllowCredentials()
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization");
});
});
var app = builder.Build();
// === HTTPS ===
app.UseHttpsRedirection();
app.UseHsts();
// === Security Headers ===
app.Use(async (context, next) => {
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Append("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';");
context.Response.Headers.Append("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
context.Response.Headers.Append("Cross-Origin-Opener-Policy", "same-origin");
context.Response.Headers.Remove("X-Powered-By");
context.Response.Headers.Remove("Server");
await next();
});
// === Exception Handling ===
if (app.Environment.IsDevelopment()) {
app.UseDeveloperExceptionPage();
} else {
app.UseExceptionHandler("/Error");
}
app.UseCors("Strict");
app.UseAuthentication();
app.UseAuthorization();
web.config hardening:
<system.web>
<httpRuntime enableVersionHeader="false" maxRequestLength="4096" />
<httpCookies requireSSL="true" httpOnlyCookies="true" />
<compilation debug="false" />
<customErrors mode="RemoteOnly" defaultRedirect="~/Error" />
<authentication>
<forms requireSSL="true" />
</authentication>
</system.web>
<system.webServer>
<security>
<requestFiltering removeServerHeader="true">
<requestLimits maxAllowedContentLength="4194304" />
</requestFiltering>
</security>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="DENY" />
<add name="Content-Security-Policy" value="default-src 'self'" />
<add name="Strict-Transport-Security" value="max-age=63072000; includeSubDomains; preload" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
</customHeaders>
</httpProtocol>
</system.webServer>
Cryptography — always use:
// AES-256-GCM
var key = new byte[32];
RandomNumberGenerator.Fill(key);
var nonce = new byte[AesGcm.NonceByteSizes.MaxSize];
RandomNumberGenerator.Fill(nonce);
var tag = new byte[AesGcm.TagByteSizes.MaxSize];
using var aes = new AesGcm(key);
aes.Encrypt(nonce, plaintextBytes, ciphertext, tag);
// Password hashing — use PBKDF2 from Microsoft.AspNetCore.Cryptography.KeyDerivation
// Or Argon2 via Konscious.Security.Cryptography
SQL Injection prevention — always parameterized:
var sql = "SELECT * FROM Users WHERE Email = @Email";
context.Database.ExecuteSqlRaw(sql, new SqlParameter("@Email", email));
// Or use Entity Framework LINQ (safe by default)
7.5 PHP Hardening
php.ini production configuration:
; === Error Handling ===
expose_php = Off
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/php_error.log
ignore_repeated_errors = Off
html_errors = Off
; === Filesystem Restrictions ===
open_basedir = /var/www/html/:/tmp/
doc_root = /var/www/html/
include_path = /usr/share/php/
allow_url_fopen = Off
allow_url_include = Off
; === Dangerous Functions (disable all) ===
disable_functions = system,exec,shell_exec,passthru,phpinfo,show_source,highlight_file,popen,proc_open,fopen_with_path,dbmopen,dbase_open,putenv,move_uploaded_file,chdir,mkdir,rmdir,chmod,rename,filepro,filepro_rowcount,filepro_retrieve,posix_mkfifo,posix_getpwuid,posix_kill,posix_setuid,posix_setgid,posix_setsid,posix_setpgid,posix_seteuid,posix_setegid,pcntl_exec
; === Session Security ===
session.save_path = /var/lib/php/sessions
session.name = __Host-PHPSESSID
session.auto_start = Off
session.use_trans_sid = 0
session.use_strict_mode = 1
session.use_cookies = 1
session.use_only_cookies = 1
session.cookie_lifetime = 14400
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict
session.cache_expire = 30
session.sid_length = 256
session.sid_bits_per_character = 6
session.gc_maxlifetime = 600
; === File Uploads ===
file_uploads = On
upload_tmp_dir = /var/lib/php/uploads
upload_max_filesize = 2M
max_file_uploads = 2
; === Resource Limits ===
memory_limit = 128M
post_max_size = 20M
max_execution_time = 30
max_input_time = 60
max_input_vars = 1000
; === Miscellaneous ===
enable_dl = Off
variables_order = "GPCS"
report_memleaks = On
zend.exception_ignore_args = On
7.6 Laravel Hardening
.env production:
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:... # Generated via: php artisan key:generate
APP_URL=https://www.example.com
SESSION_DRIVER=database
SESSION_LIFETIME=15
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=strict
config/session.php:
return [
'driver' => env('SESSION_DRIVER', 'database'),
'lifetime' => env('SESSION_LIFETIME', 15),
'expire_on_close' => false,
'encrypt' => true,
'cookie' => env('SESSION_COOKIE', '__Host-laravel_session'),
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => true,
'http_only' => true,
'same_site' => 'strict',
];
app/Http/Kernel.php:
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'throttle:30,1', // Global rate limit
],
'api' => [
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Mass assignment — always whitelist:
// Controller — use validated() or only()
$user = User::create($request->validated());
// OR
$user = User::create($request->only(['name', 'email']));
// NEVER use $request->all() or forceFill() with raw input
SQL injection prevention:
// SAFE — Eloquent parameterizes automatically
User::where('email', $request->input('email'))->first();
// SAFE — Raw with bindings
User::whereRaw('email = ?', [$request->input('email')])->first();
// NEVER use raw string interpolation
User::whereRaw("email = '{$request->input('email')}'")->first(); // VULNERABLE
File upload validation:
$request->validate([
'photo' => 'required|file|max:2048|mimes:jpg,png,gif',
]);
// Store with safe filename
$request->file('photo')->storeAs(auth()->id(), basename($request->input('filename')));
Rate limiting — app/Providers/RouteServiceProvider.php:
RateLimiter::for('login', function ($request) {
return Limit::perMinute(5)->by($request->input('email') . '|' . $request->ip());
});
RateLimiter::for('api', function ($request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
Command injection prevention:
exec('whois ' . escapeshellarg($request->input('domain')));
File permissions:
# Directories: 755 max (775 if web server needs write)
# Files: 644 max
# Storage/cache: 775
find /var/www/app -type f -exec chmod 644 {} \;
find /var/www/app -type d -exec chmod 755 {} \;
chmod -R 775 /var/www/app/storage /var/www/app/bootstrap/cache
8. Docker Container Hardening
Standards: CIS Docker Benchmark, NIST SP 800-190, ANSSI Docker Recommendations
8.1 Docker Daemon Configuration
/etc/docker/daemon.json:
{
"icc": false,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
},
"live-restore": true,
"userns-remap": "default",
"no-new-privileges": true,
"seccomp-profile": "/etc/docker/seccomp/default.json",
"storage-driver": "overlay2",
"tls": true,
"tlscacert": "/etc/docker/tls/ca.pem",
"tlscert": "/etc/docker/tls/server-cert.pem",
"tlskey": "/etc/docker/tls/server-key.pem",
"tlsverify": true,
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
},
"nproc": {
"Name": "nproc",
"Hard": 4096,
"Soft": 4096
}
},
"userns-remap": "dockremap:dockremap"
}
8.2 Hardened Dockerfile Template
# Use specific digest, not :latest
FROM python:3.12-slim@sha256:<digest>
# Labels
LABEL maintainer="security@example.com"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
# Install security updates
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
ca-certificates \
tini && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Create non-root user
RUN groupadd -r appuser && \
useradd -r -g appuser -d /app -s /sbin/nologin appuser
# Set working directory
WORKDIR /app
# Copy and install dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir --no-compile -r requirements.txt
# Copy application code
COPY . .
# Remove setuid/setgid binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
# Set filesystem to read-only where possible
RUN chmod -R 555 /app
# Switch to non-root
USER appuser
# Use tini as PID 1
ENTRYPOINT ["tini", "--"]
# Health check
HEALTHCHECK \
CMD curl -f http://localhost:8080/health || exit 1
# Expose only necessary ports
EXPOSE 8080
CMD ["python", "app.py"]
8.3 Docker Run Security Flags
docker run \
--name app \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=64m \
--tmpfs /run:noexec,nosuid,size=32m \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
--security-opt apparmor=docker-default \
--security-opt seccomp=/etc/docker/seccomp/default.json \
--pids-limit 100 \
--memory 512m \
--memory-swap 512m \
--cpus 1.0 \
--ulimit nofile=64000:64000 \
--ulimit nproc=4096:4096 \
--network app-net \
--restart on-failure:5 \
--health-cmd "curl -f http://localhost:8080/health || exit 1" \
--health-interval 30s \
--health-timeout 5s \
--user 1000:1000 \
-e "APP_ENV=production" \
app:latest
8.4 Docker Compose Security
version: '3.8'
services:
app:
image: app:latest
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=64m
- /run:noexec,nosuid,size=32m
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
- apparmor:docker-default
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pids: 100
reservations:
cpus: '0.25'
memory: 128M
user: "1000:1000"
networks:
- app-internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
environment:
- APP_ENV=production
secrets:
- db_password
restart: on-failure
networks:
app-internal:
driver: bridge
internal: true
secrets:
db_password:
file: ./secrets/db_password.txt
8.5 Image Scanning
# Trivy (recommended)
trivy image --severity HIGH,CRITICAL app:latest
# Grype
grype app:latest
# Docker Scout
docker scout cves app:latest
# Hadolint for Dockerfile linting
hadolint Dockerfile
8.6 Docker Bench Security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
./docker-bench-security.sh
9. Kubernetes Cluster Hardening
Standards: CIS Kubernetes Benchmark, NSA/CISA Kubernetes Hardening Guide, Kubernetes Security Checklist
9.1 API Server Hardening
/etc/kubernetes/manifests/kube-apiserver.yaml critical flags:
spec:
containers:
- command:
- kube-apiserver
# === Authentication ===
- --anonymous-auth=false
- --authentication-token-webhook-config-file=/etc/kubernetes/webhook-config.yaml
# === Authorization ===
- --authorization-mode=Node,RBAC
- --enable-admission-plugins=NodeRestriction,PodSecurityAdmission,AlwaysPullImages,EventRateLimit,ResourceQuota,LimitRanger
# === TLS ===
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
- --tls-min-version=VersionTLS12
- --tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- --client-ca-file=/etc/kubernetes/pki/ca.crt
# === Audit ===
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
# === etcd ===
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
# === Encryption at rest ===
- --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# === Disable insecure options ===
- --insecure-port=0
- --profiling=false
- --enable-bootstrap-token-auth=false
# === Request limits ===
- --request-timeout=60s
- --service-account-lookup=true
9.2 Encryption at Rest
/etc/kubernetes/encryption-config.yaml:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
9.3 Audit Policy
/etc/kubernetes/audit-policy.yaml:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Don't log read-only endpoints
- level: None
nonResourceURLs:
- /healthz*
- /version
- /readyz*
- /livez*
# Log secret access at Metadata level
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
# Log authentication
- level: RequestResponse
resources:
- group: "authentication.k8s.io"
# Log RBAC changes
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
# Log pod exec/attach
- level: RequestResponse
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
# Log all write operations
- level: Request
verbs: ["create", "update", "patch", "delete", "deletecollection"]
# Default — log metadata
- level: Metadata
omitStages:
- RequestReceived
9.4 Pod Security Standards (Restricted)
Namespace enforcement:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
9.5 Hardened Pod Template
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
namespace: production
spec:
automountServiceAccountToken: false
hostNetwork: false
hostPID: false
hostIPC: false
securityContext:
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
fsGroup: 10000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:v1.0.0@sha256:<digest>
imagePullPolicy: Always
ports:
- containerPort: 8080
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10000
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
resources:
limits:
cpu: "500m"
memory: "256Mi"
ephemeral-storage: "128Mi"
requests:
cpu: "100m"
memory: "128Mi"
volumeMounts:
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
serviceAccountName: app-sa
9.6 Network Policies
Default deny all:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow specific traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-traffic
namespace: production
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
- podSelector:
matchLabels:
app: ingress-nginx
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to: # DNS
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
9.7 RBAC — Least Privilege
# Service account for application
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
automountServiceAccountToken: false
---
# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-role
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["app-config"]
verbs: ["get"]
---
# Bind role to service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-rolebinding
namespace: production
subjects:
- kind: ServiceAccount
name: app-sa
namespace: production
roleRef:
kind: Role
name: app-role
apiGroup: rbac.authorization.k8s.io
Dangerous ClusterRoles to audit:
# Find overprivileged bindings
kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name == "cluster-admin") | .subjects'
# Audit all roles for wildcard permissions
kubectl get roles,clusterroles -A -o json | jq '.items[] | select(.rules[]?.resources[]? == "*" or .rules[]?.verbs[]? == "*") | .metadata.name'
9.8 Resource Quotas
apiVersion: v1
kind: ResourceQuota
metadata:
name: production-quota
namespace: production
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
pods: "50"
services: "10"
secrets: "20"
configmaps: "20"
persistentvolumeclaims: "10"
services.loadbalancers: "2"
services.nodeports: "0"
---
apiVersion: v1
kind: LimitRange
metadata:
name: production-limits
namespace: production
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "2"
memory: "2Gi"
min:
cpu: "50m"
memory: "64Mi"
- type: Pod
max:
cpu: "4"
memory: "4Gi"
9.9 etcd Hardening
# Verify etcd uses TLS
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
endpoint health
# File permissions
chmod 600 /etc/kubernetes/pki/etcd/*.key
chmod 644 /etc/kubernetes/pki/etcd/*.crt
chown root:root /etc/kubernetes/pki/etcd/*
# etcd data directory
chmod 700 /var/lib/etcd
chown etcd:etcd /var/lib/etcd
9.10 Kubelet Hardening
/var/lib/kubelet/config.yaml:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
readOnlyPort: 0
protectKernelDefaults: true
makeIPTablesUtilChains: true
eventRecordQPS: 5
tlsCipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
tlsMinVersion: VersionTLS12
rotateCertificates: true
serverTLSBootstrap: true
streamingConnectionIdleTimeout: 5m0s
9.11 Image Security
# ImagePolicyWebhook admission controller
# OR use OPA Gatekeeper / Kyverno
# Kyverno — require signed images from trusted registry
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: require-trusted-registry
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Images must come from registry.example.com"
pattern:
spec:
containers:
- image: "registry.example.com/*"
initContainers:
- image: "registry.example.com/*"
9.12 Runtime Security Tools
| Tool | Purpose |
|---|---|
| Falco | Runtime threat detection via syscall monitoring |
| Trivy | Image vulnerability scanning |
| kube-bench | CIS benchmark audit |
| Kyverno | Policy engine (admission control) |
| OPA Gatekeeper | Policy engine (Rego-based) |
| Cilium | eBPF-based network policies + observability |
| Tetragon | eBPF security observability |
| kubeaudit | Cluster security audit |
Run kube-bench:
docker run --pid=host -v /etc:/etc:ro -v /var:/var:ro \
aquasec/kube-bench run --targets=master,node
Appendix A: Security Scanning & Compliance Automation
| Tool | Platform | Purpose |
|---|---|---|
| Ansible Lockdown | Linux/Windows | CIS/STIG automated remediation |
| OpenSCAP | Linux | SCAP-based compliance scanning |
| Lynis | Linux | Security auditing |
| kube-bench | Kubernetes | CIS benchmark |
| Docker Bench | Docker | CIS Docker benchmark |
| Trivy | Containers | Vulnerability scanning |
| Sigma | SIEM | Generic detection rules |
| osquery | Endpoint | SQL-based OS telemetry |
| Wazuh | Multi | HIDS + compliance monitoring |
| Falco | Kubernetes | Runtime threat detection |
Appendix B: Detection Engineering Stack
| Layer | Tool | Detection Source |
|---|---|---|
| Network | Zeek/Suricata | Full packet analysis, IDS signatures |
| Endpoint | osquery/Sysmon | Process, file, registry, network events |
| SIEM | Elastic/Splunk | Log correlation, Sigma rules |
| Fingerprinting | JA3/JA3S, HASSH, JARM | TLS/SSH client/server profiling |
| Threat Intel | MITRE ATT&CK, MITRE CAR | TTP mapping, analytics |
| Hunting | ThreatHunter-Playbook | Hypothesis-driven detection development |
| ML/Analytics | msticpy | Jupyter-based investigation |
Appendix C: Quick Reference — Hardening Verification Commands
# === Linux ===
lynis audit system # Full system audit
aide --check # File integrity check
sysctl -a | grep -E "kptr|dmesg|ptrace" # Verify kernel params
ss -tulnp # Check listening services
systemd-analyze security # Service hardening scores
ausearch -m AVC -ts recent # SELinux denials
aa-status # AppArmor profile status
find / -perm -4000 -type f 2>/dev/null # Find SUID binaries
grep -r "PermitRootLogin" /etc/ssh/ # SSH root login check
awk -F: '($2 == "") {print $1}' /etc/shadow # Accounts without passwords
# === Docker ===
docker run --rm aquasec/trivy image <image> # Scan image
./docker-bench-security.sh # CIS Docker benchmark
# === Kubernetes ===
kubectl auth can-i --list --as=system:anonymous # Check anonymous access
kubectl get netpol -A # List network policies
kubectl get psp -A 2>/dev/null # Check pod security policies
kube-bench run # CIS K8s benchmark
# === TLS ===
nmap --script ssl-enum-ciphers -p 443 host # Enumerate TLS ciphers
testssl.sh https://example.com # Full TLS audit
curl -sI https://example.com | grep -i strict # HSTS check
# === SSH ===
ssh-audit hostname # Full SSH audit
nmap --script ssh2-enum-algos -p 22 host # Enumerate SSH algorithms
Generated by CIPHER — Claude Integrated Privacy & Hardening Expert Resource Sources: CIS Benchmarks, DISA STIGs, Mozilla Server Side TLS, OWASP Cheat Sheet Series, madaidans-insecurities Linux Hardening Guide, ANSSI Configuration Recommendations, NIST SP 800-series, NSA Cybersecurity Advisories, Ansible Lockdown Project