A layered hardening toolkit for Ubuntu/Debian servers. Two profiles: baseline (any server) and web-server (Apache/PHP/MySQL addendum). One config file, one command.
Each script is idempotent, validates preconditions before making changes, supports --dry-run, and backs up any files it modifies.
Tested on: Ubuntu 24.04 LTS (Noble Numbat) · Apache 2.4
This toolkit came out of necessity. Managing a growing number of projects across multiple VPS providers made it clear that ad-hoc server setup wasn't sustainable. Every new deployment meant repeating the same hardening steps from memory, with inconsistent results and no audit trail.
The goal was a standardized, repeatable baseline: cookie-cutter deployments that stand up a secure server quickly, while staying flexible enough to handle the custom, one-off requirements that come with running a variety of distinct projects. Each server has its own quirks. Different domain configurations, monitoring needs, access patterns. The toolkit is structured to handle both the common baseline and those edge cases without diverging into a tangle of server-specific scripts.
| Component | Status |
|---|---|
| Core hardening scripts (01–08) | Complete |
| Web hardening scripts (01–10) | Complete |
config.env / config.web.env configuration |
Complete |
bootstrap.sh with --profile support |
Complete |
scripts/audit/audit.sh baseline checker |
Complete |
scripts/core/audit/ extended audit tools |
Complete |
scripts/web/audit/ web audit tools |
Complete |
| Nginx support | Planned (Phase 2) |
| Multi-server fleet tooling | Planned (Phase 2) |
See the open issues for the full phased roadmap.
| Area | Coverage |
|---|---|
| Firewall | UFW — deny all inbound, allow SSH / 80 / 443 |
| SSH | Key-only auth, no root password login, no X11 forwarding |
| Intrusion Prevention | fail2ban with SSH + Apache jails, 3 strikes / 1 hour ban; recidive jail for repeat offenders |
| Kernel Network | ICMP redirect blocking, martian packet logging |
| Rootkit Detection | rkhunter with scheduled scans |
| Syscall Auditing | auditd with baseline ruleset |
| Filesystem Integrity | AIDE — baseline snapshot + scheduled diff |
| Apache | ServerTokens Prod, ServerSignature Off, security headers, HSTS, CSP, block .git/.svn, disable mod_status (web-server profile) |
| TLS | Modern cipher suite, HSTS preload, cert expiry monitoring (web-server profile) |
| Admin User | Non-root sudo user with SSH key access |
| Automatic Updates | unattended-upgrades + monthly full upgrade with email report |
| Log Monitoring | Logwatch daily digest + GoAccess traffic reports, password-protected (web-server profile) |
| Malware Scanning | ClamAV with scheduled scans (web-server profile) |
| WAF | ModSecurity with OWASP Core Rule Set (web-server profile) |
- Ubuntu 22.04+ or Debian 12+
- Root access
- SSH public key already in
/root/.ssh/authorized_keysbefore running — scriptcore/01disables password authentication and aborts if no key is present
For the web-server profile, Apache 2.4 must be installed and running. PHP and MySQL/MariaDB hardening scripts are optional and will skip cleanly if those services are not present.
1. Clone and configure
git clone https://github.com/davidwhittington/linux-security.git
cd linux-security
cp config.env config.env.local # or edit config.env directlyEdit config.env and set your values: admin username, email address, SMTP relay, and SSH port.
For the web-server profile, also fill in config.web.env (CSP domains, cert warn threshold, web roots path).
See docs/customization.md for details on every variable.
2. Run
Option A — single command (recommended):
# As root on the target server
bash bootstrap.sh --profile baseline # core controls only (any server)
bash bootstrap.sh --profile web-server # core + Apache/PHP/MySQL hardening
bash bootstrap.sh --dry-run # preview all changes first
bash bootstrap.sh --profile baseline --dry-run
bash bootstrap.sh --confirm # skip the AGREE prompt (automation/CI)Option B — run scripts individually in order:
# Core layer
bash scripts/core/hardening/01-immediate-hardening.sh # Firewall · SSH · fail2ban · sysctl
bash scripts/core/hardening/02-setup-admin-user.sh # Non-root admin with sudo + SSH keys
bash scripts/core/hardening/03-monthly-updates-setup.sh # Scheduled apt upgrades + email report
# Web layer (web-server profile only)
bash scripts/web/hardening/01-apache-hardening.sh # Apache headers · TLS · mod_status
bash scripts/web/hardening/02-log-monitoring-setup.sh # Logwatch + GoAccess traffic reports
bash scripts/web/hardening/03-cert-monitor-setup.sh # Cert expiry monitoring3. Verify
bash scripts/audit/audit.sh # web-server checks (default)
bash scripts/audit/audit.sh --profile baseline # core checks onlyAfter running
core/01: Open a second terminal and verify SSH access before closing your current session. Password authentication will be disabled.
Available in both profiles. No Apache or web-server dependency.
Addresses the highest-risk issues found on most freshly provisioned servers.
- Installs and configures fail2ban with SSH jail (3 strikes, 1h ban) and Apache jails
- Enables UFW with deny-all inbound policy; opens SSH port, 80, 443
- Disables SSH password authentication and root password login
- Disables X11 forwarding
- Hardens kernel sysctl: disables ICMP redirects, enables martian logging
Safe to re-run. Aborts if no SSH authorized key is found.
Promotes an existing user to sudo admin and removes the cloud-init NOPASSWD sudoers rule.
- Sets login shell to
/bin/bash - Adds user to
sudogroup - Copies root's
authorized_keysso SSH access works immediately - Removes
/etc/sudoers.d/90-cloud-init-users
Admin username is set from $ADMIN_USER in config.env. The user must exist before running.
After verifying SSH access and sudo -v work, set PermitRootLogin no in /etc/ssh/sshd_config.
Sets up a monthly full system update with an emailed report.
- Installs and configures
msmtpwith your SMTP relay settings - Creates
/usr/local/sbin/monthly-apt-report.sh— runsapt upgrade, checks kernel version, disk usage, uptime, fail2ban status, and cert expiry - Schedules a cron job at 3:00 AM on the 1st of each month
Email address and SMTP settings read from config.env.
Additional hardening applied after the baseline is established:
| Script | Installs / Configures |
|---|---|
04-rkhunter-setup.sh |
rkhunter rootkit scanner with scheduled scans and email alerts |
05-auditd-setup.sh |
auditd syscall auditing with a baseline ruleset |
06-fail2ban-recidive.sh |
fail2ban recidive jail — escalating bans for repeat offenders |
07-aide-setup.sh |
AIDE filesystem integrity baseline and nightly diff |
08-disk-alert-setup.sh |
Disk usage cron — emails when any partition crosses threshold |
Applied only with --profile web-server. Requires Apache 2.4 running.
Reduces information disclosure and adds browser security headers.
- Enables
mod_headers - Sets
ServerTokens ProdandServerSignature Off - Disables TRACE method
- Blocks access to
.gitand.svndirectories - Adds security headers:
X-Content-Type-Options,Referrer-Policy,Permissions-Policy, HSTS, CSP - Disables
mod_status - Backs up existing
security.confbefore overwriting; restores on failure
CSP frame-ancestors is set from $CSP_FRAME_ANCESTORS in config.web.env.
Installs daily log digest and traffic reporting.
- Installs Logwatch — configures a daily HTML email digest of all services
- Installs GoAccess — generates a daily HTML traffic report from Apache access logs
- Password-protects the reports directory with HTTP Basic Auth
- Schedules GoAccess at 4:00 AM daily
Reports are served from /var/www/html/reports/.
Configures automated TLS cert expiry alerts.
- Installs a daily cron that checks cert expiry via
certbot certificates - Emails a warning when any cert is within
$CERT_WARN_DAYSdays of expiry (default: 30)
| Script | Installs / Configures |
|---|---|
04-clamav-setup.sh |
ClamAV with scheduled scans of web roots |
05-modsecurity-setup.sh |
ModSecurity WAF with OWASP Core Rule Set |
06-vhost-hardener.sh |
Per-vhost security headers and directory restrictions |
07-apache-tls-hardening.sh |
Modern cipher suite, HSTS preload, OCSP stapling |
08-apache-dos-mitigation.sh |
mod_evasive and mod_reqtimeout tuning |
09-php-hardening.sh |
PHP ini hardening (skips cleanly if PHP not installed) |
10-mysql-hardening.sh |
MySQL/MariaDB secure defaults (skips cleanly if not installed) |
linux-security/
├── config.env # Core configuration — fill in before running
├── config.web.env # Web-layer configuration — needed for web-server profile
├── bootstrap.sh # Single-command provisioner
├── profiles/
│ ├── baseline.conf # Core-only script list
│ └── web-server.conf # Core + web script list
├── docs/
│ ├── security/
│ │ └── README.md # Security baseline, requirements, audit cadence
│ ├── architecture.md # How the toolkit fits together
│ ├── customization.md # config.env and config.web.env variables explained
│ ├── TEMPLATE.md # Blank audit report template
│ └── VPS_HARDENING_GUIDE.html # Standalone HTML knowledge base (offline reference)
├── scripts/
│ ├── core/
│ │ ├── hardening/ # Core hardening scripts (01–08)
│ │ └── audit/ # Core read-only checkers
│ ├── web/
│ │ ├── hardening/ # Web hardening scripts (01–10)
│ │ └── audit/ # Web read-only checkers
│ └── audit/
│ └── audit.sh # Profile-aware baseline checker
├── lib/ # Shared shell libraries
├── logs/ # Per-run bootstrap logs (gitignored)
├── config/ # Config snippets and templates (planned)
└── private/ # Git submodule — server-specific data (not public)
Run the built-in checker after hardening to verify every control is active:
bash scripts/audit/audit.sh # web-server checks (default)
bash scripts/audit/audit.sh --profile baseline # core checks only
bash scripts/audit/audit.sh --json # machine-readable output
bash scripts/audit/audit.sh --report html # full HTML reportFor a full manual audit, use the template:
- Copy
docs/TEMPLATE.mdto your private repo asprivate/servers/<hostname>/AUDIT_REPORT.md - Work through each finding category against your server
- Use the checklist at the bottom to track remediation progress
See docs/security/README.md for the full security baseline.
This repo is structured to keep generic, reusable scripts public and server-specific data private. The private/ directory is a separate private git submodule holding actual audit reports, inventory, and network data.
To adopt this pattern for your own infrastructure:
# Fork or clone this repo
gh repo fork davidwhittington/linux-security
# Create your own private companion repo
gh repo create my-linux-private --private
# Add it as a submodule
git submodule add https://github.com/<you>/my-linux-private private/
git commit -m "Add private submodule"- Customization Guide — config.env and config.web.env variables explained
- Architecture — how the toolkit fits together
- Security Baseline — requirements, headers, audit cadence
- Audit Report Template — blank template for documenting findings
- VPS Hardening Guide — standalone offline reference
- Changelog — version history
MIT — use freely, adapt for your own infrastructure.