A collection of battle-tested bash scripts for Linux server administration, with a focus on security hardening, WordPress management, and cPanel/WHM server maintenance.
Author: Gabriel Serafini (gserafini@gmail.com)
This repository provides production-ready scripts for system administrators managing:
- WordPress Servers: Automated security monitoring and IP blocking
- cPanel/WHM Environments: Domain management and account maintenance
- SVN-Managed Sites: Version control workflow automation
- General Server Security: Rootkit scanning, permission fixes, process management
These scripts are designed to be installed system-wide and run with root privileges, typically via cron jobs or manual administrative tasks.
- All scripts require root/sudo privileges
- Scripts modify live firewall rules and can affect server connectivity
- Always test in a VM or staging environment first
- Review all scripts before running them on production systems
- Keep backups of firewall configurations before making changes
- The
--initand--clearoperations for csf_ban_wp_login_attackers are particularly sensitive
Use at your own risk. Always understand what a script does before executing it.
- Linux server (tested on RHEL/CentOS and Debian/Ubuntu)
- Bash 4.0 or higher
- Root access
| Script | Dependencies |
|---|---|
| csf_ban_wp_login_attackers | ipset, iptables, csf, whois, conntrack-tools (optional), Apache logs |
| get_nameservers.sh | dig, whois, /etc/userdomains (cPanel) |
| ban_ips.sh | csf, geoiplookup |
| svn_add_remove | svn (Subversion) |
| commit_htaccess_files.sh | svn, /etc/trueuserowners (cPanel) |
| chkrootkit_output_clean | chkrootkit |
Installing conntrack-tools (recommended for csf_ban_wp_login_attackers):
# RHEL/CentOS
sudo yum install -y conntrack-tools ipset
# Debian/Ubuntu
sudo apt-get install -y conntrack ipsetClone the repository and run the install script:
git clone https://github.com/gserafini/useful-server-scripts.git
cd useful-server-scripts/
sudo ./install.shThe install script will:
- Create symlinks for all scripts in
/usr/local/bin/ - Detect and update existing symlinks
- Provide a summary of what was installed
- Show next steps for setup
Alternatively, you can manually symlink scripts to /usr/local/bin/:
cd useful-server-scripts/
for f in `ls scripts/` ; do sudo ln -s $PWD/scripts/$f /usr/local/bin/$f ; doneBoth methods make all scripts available system-wide as commands (e.g., sudo csf_ban_wp_login_attackers --init).
-
Install dependencies:
sudo yum install -y ipset conntrack-tools # or apt-get on Debian/Ubuntu -
Initialize the system (one-time setup):
sudo csf_ban_wp_login_attackers --init
-
Test a manual ban:
sudo csf_ban_wp_login_attackers --blacklist 1.2.3.4 "Test ban" -
Set up automated scanning (add to root's crontab):
# Run every 5 minutes */5 * * * * /usr/local/bin/csf_ban_wp_login_attackers
-
Generate abuse reports:
sudo csf_ban_wp_login_attackers --abuse 1.2.3.4
In a WordPress directory managed by SVN:
svn_add_removeFollow the interactive prompts to schedule additions/deletions.
# Human-readable output
get_nameservers.sh
# TSV format for spreadsheets
get_nameservers.sh --csvPrimary script: High-performance IP banning system using IPSET and CSF firewall integration.
Purpose: Automatically scan Apache logs for malicious activity (wp-login brute force, shell probes, ModSecurity violations) and ban offending IPs at the kernel level using IPSET.
Key Features:
- Handles 50,000+ bans efficiently using IPSET
- Integrates with CSF firewall via
/etc/csf/csfpost.sh - Replays the persistent tracking file automatically after every CSF rebuild
- Excludes assigned local,
csf.allow, andcsf.ignoreIPv4 addresses from replay - Refuses manual, batch, delegated, and LF_DISTATTACK migration bans for exact
csf.alloworcsf.ignoreIPv4 entries - Preserves complete multiword signatures, scans each hard-linked cPanel log only once, and counts one validated source IP per matched event
- Treats only precise missing-script messages and high-confidence blocking signatures as ban triggers, not generic PHP errors or non-blocking OWASP warnings
- Terminates existing connections when banning (requires conntrack-tools)
- Supports safe ModSecurity
execbans through a no-argument CGI adapter - Generates abuse evidence reports with WHOIS integration
- Supports manual whitelist/blacklist management
- Rotatable ban list with permanent ban threshold
One-time initialization (creates IPSET table and firewall rules):
sudo csf_ban_wp_login_attackers --initThis creates:
- IPSET table named
high_volume_bans - iptables DROP rule in
/etc/csf/csfpost.sh - post-rebuild replay command in
/etc/csf/csfpost.sh - Tracking file at
/etc/csf/ipset_tracking_high_volume_bans.log
--blacklist-file validates all nonempty inputs before setup or firewall work, canonicalizes IPv4 addresses, and reports each original input line, including duplicates, protected addresses and errors. It loads protected policy once and normalizes one private tracker snapshot once. Duplicate tracker rows keep the first permanent (DO NOT DELETE) reason when present; unrelated comments and unique reasons remain. Unsupported tracker rows stop the batch before its native add.
There are two complete reads of the live tracker, independent of batch size: the snapshot copy and a final SHA-256 beforeimage guard. Parsing/normalization happens once on the private snapshot. One ipset restore is attempted, followed by individual live membership checks. A native restore error can leave later commands unattempted; it is never replayed automatically. Only verified members receive missing persistent entries. The result for every input separates live/persistence qualification from session-cleanup status. Existing single-IP syntax and existing reasons remain supported.
All eight tracker writer families use a stable private sidecar lock for snapshot/publication: blacklist, distributed-LFD migration, unblacklist, protected reconciliation, local whitelist, initialization, clear, and automatic scan. Publication checks canonical path, device/inode, owner/group, link count, mode, size, timestamps and SHA-256; it atomically renames a same-filesystem candidate while preserving owner/group/mode. Symlinks, multiple links and foreign ownership are refused. Setup, kernel actions and recursive CSF reload hooks run outside this lock. A stale writer stops rather than overwriting another writer. This is file-publication protection, not an atomic firewall transaction. External editors must honor the sidecar lock; an uncoordinated write after the final guard is outside this protocol.
Kernel success followed by persistence failure returns nonzero and retains a root-private transaction directory with source/planned afterimage, hashes, metadata, native results and per-input failure receipt. Do not roll back or retry automatically: concurrent changes and live coverage must be reviewed first. Successful transactions discard their own known temporary files. The automatic scanner retains its existing native kernel planner; this change guards its tracker publication and reports unqualified kernel-plan outcomes rather than asserting every kernel operation succeeded.
Batch session cleanup has a ten-second aggregate scheduling budget and a two-second native KILL timeout per attempted IP. After the budget, remaining cleanup is explicitly deferred_budget, with no background leftovers. Exit 124/137 means bounded timeout; other unexpected exits are errors. Invoking cleanup does not prove active connections closed. Coverage and persistence remain independently reported. The runtime uses installed Bash/coreutils/flock/gawk, including legacy Bash 4.1/coreutils 8.4; Python and strace are fixture-test dependencies only.
Run only the isolated fixtures in a new private directory, never a live ban to test this feature:
fixture_dir=$(mktemp -d "$PWD/.fixture-run.XXXXXX")
chmod 700 "$fixture_dir"
TMPDIR="$fixture_dir" python3.12 -B -m unittest discover -s tests -p 'test_tracking_*.py'
TMPDIR="$fixture_dir" python3.12 -B tests/test_csf_blacklist_batch.py
TMPDIR="$fixture_dir" python3.12 -B tests/test_batch_cleanup.pyManual IP management:
# Ban an IP
sudo csf_ban_wp_login_attackers --blacklist 1.2.3.4 "Brute force wp-login"
# Alias: --block
# Ban a reviewed list after validating the entire input before any mutation
sudo csf_ban_wp_login_attackers --blacklist-file campaign-ips.txt "Distributed crawler campaign"
# One private tracker normalization, one native ipset restore, one guarded publication.
# Inspect every BATCH_INPUT disposition; nonzero may leave verified live coverage.
# Never blindly retry a partial failure: inspect the retained private evidence first.
# Read the reviewed list from stdin
printf '%s\n' 1.2.3.4 5.6.7.8 | sudo csf_ban_wp_login_attackers --blacklist-file - "Distributed crawler campaign"
# Reverse this tool's false-positive deny without globally allowlisting the IP
sudo csf_ban_wp_login_attackers --unblacklist 1.2.3.4 "Reviewed false positive"
# Inspect exact live, tracked, CSF deny, and CSF allow coverage without mutation
sudo csf_ban_wp_login_attackers --check-ip 1.2.3.4
# Whitelist an IP (adds to /etc/csf/csf.allow)
sudo csf_ban_wp_login_attackers --whitelist 1.2.3.4 "Trusted admin"
# Alias: --unblock
# Remove sources protected by local or CSF numeric trust policy from live/tracked bans
# without restarting or reloading CSF
sudo csf_ban_wp_login_attackers --reconcile-allowlist
# Clear all bans (DANGEROUS - requires confirmation)
sudo csf_ban_wp_login_attackers --clearNumeric trust policy covers exact IPv4 addresses and CIDRs in csf.allow and
csf.ignore, recursive Include files, and numeric address operands in s= or
d= advanced-filter fields. Ports, owner fields, comments, IPv6, and hostnames
do not add IPv4 protection; this helper performs no DNS lookups. Address operands
conservatively protect the whole address/range regardless of port or direction;
standalone s=/d= address fields also receive this protection, without claiming
to validate a complete CSF rule.
CSF documents absolute Include paths with no trailing comments. The protection helper also accepts relative paths resolved against the including file's directory as a conservative extension. An Include must name one literal, unquoted path; trailing comments are rejected. Missing/unreadable files (including either root file), include cycles, and malformed numeric IPv4/CIDR entries abort the operation before a partial policy can be used. The builder publishes only after validation; on failure its output stays unchanged, including an already empty output. Builder destinations must be regular private temporary files (or absent); symlinks and directories are rejected.
Automated scanning (default mode, no flags):
sudo csf_ban_wp_login_attackersScans default log paths and bans IPs matching keyword patterns. Add to cron:
*/5 * * * * /usr/local/bin/csf_ban_wp_login_attackersModSecurity persistent bans:
ModSecurity 2.x executes external programs without command-line arguments. Rules
must call modsecurity_persistent_blacklist directly and rely on the CGI
REMOTE_ADDR environment variable:
SecRule REQUEST_URI "@rx malicious-pattern" \
"phase:1,id:999999,deny,status:403,exec:'/usr/local/useful-server-scripts/scripts/modsecurity_persistent_blacklist'"The adapter validates the client IPv4 address, serializes concurrent burst
matches, and invokes csf_ban_wp_login_attackers --blacklist through the
existing sudo policy.
Custom log paths:
sudo csf_ban_wp_login_attackers --logs "/var/log/apache2/access.log /var/log/nginx/access.log"These commands generate evidence for reporting abuse to ISPs. They don't modify firewall state.
Basic report (default threshold: 100 keyword matches):
sudo csf_ban_wp_login_attackers --abuse 1.2.3.4Report with email draft (includes WHOIS abuse contact lookup):
sudo csf_ban_wp_login_attackers --abuse-email --abuse 1.2.3.4Time-limited report (last 7 days):
sudo csf_ban_wp_login_attackers --abuse-days 7 --abuse 1.2.3.4Custom threshold (50 hits instead of 100):
sudo csf_ban_wp_login_attackers --abuse-min 50 --abuse 1.2.3.4Date range filtering:
sudo csf_ban_wp_login_attackers --abuse-since "2025-10-01" --abuse-until "2025-10-15" --abuse 1.2.3.4Raw log search (grep wrapper):
sudo csf_ban_wp_login_attackers --raw-grep '1.2.3.4'
# Alias: --raw-search| Flag | Description |
|---|---|
--init |
Initialize IPSET table and CSF integration (one-time setup) |
--rebuild-live-set |
Bulk-replay tracked bans while excluding local and CSF-protected IPv4 addresses/CIDRs |
--reconcile-allowlist |
Remove local and csf.allow/csf.ignore protected IPv4 addresses/CIDRs from live and tracked bans without a CSF reload |
--resize-live-set MAX |
Atomically resize the live set and persist its capacity |
--whitelist-local-addresses |
Remove assigned local addresses from bans and persist them in csf.allow |
--blacklist IP [MSG] |
Manually ban an IP (alias: --block); refuses exact csf.allow and csf.ignore entries |
| `--blacklist-file FILE | - [MSG]` |
--unblacklist IP [MSG] |
Remove an exact IP from high_volume_bans and its tracking file without adding it to csf.allow or changing independent CSF deny policy |
--check-ip IP |
Report exact live ipset, tracking, CSF deny, parent /24, and CSF allow coverage without mutation |
--promote-cidr CIDR [MSG] |
Atomically replace covered CSF child bans with a canonical IPv4 /24; refuses unsafe capacity changes and rolls back on failure |
--migrate-lfd-distributed |
Preserve recent LFD LF_DISTATTACK blocks in high_volume_bans, then atomically remove only their evictable exact-IP CSF entries |
--whitelist IP [MSG] |
Remove IP from bans and add to CSF allow list (alias: --unblock) |
--clear |
Flush all bans from IPSET and tracking file (DANGEROUS) |
--logs "PATH1 PATH2" |
Override default log paths to scan |
--abuse IP [MIN_HITS] |
Generate abuse report (alias: --abuse-report) |
--abuse-min N |
Set default minimum hits for abuse reports |
--abuse-days N |
Restrict search to last N days (best-effort) |
--abuse-since STRING |
Filter logs by start date substring |
--abuse-until STRING |
Filter logs by end date substring |
--abuse-email |
Include WHOIS lookup and draft email in report |
--raw-grep STRING |
Search all logs for string (alias: --raw-search) |
Edit these at the top of the script if needed:
IP_SET_NAME: IPSET table name (default:high_volume_bans)MAX_BANS: Maximum IPs in IPSET (default: 50,000)MAX_PERMANENT_BANS: Max permanent bans (default: 5,000)MAX_VIOLATION_COUNT: Threshold for permanent bans (default: 20 hits)DEFAULT_LOG_PATHS: Apache log locations to scan
- Reports are non-destructive - they read logs and write evidence to
/tmp - Date-range handling is best-effort based on log timestamp formats
- WHOIS abuse contact lookup is heuristic - always verify before emailing
- Requires CSF installed and configured before
--init - conntrack-tools optional but recommended for immediate session termination
Purpose: Interactive SVN workflow helper for WordPress sites.
Use Case: After WordPress auto-updates, quickly schedule new/modified/removed files for SVN commit.
What it does:
- Runs
svn statusto find unknown (?) and missing (!) files - Excludes cache files and error logs
- Prompts to schedule additions and deletions
- Handles replaced resources (
~) - Fixes file ownership when run as root
Usage:
cd /path/to/wordpress
svn_add_removeFollow interactive prompts to review and schedule changes.
Requirements: Subversion (svn)
Purpose: Extract domain information for all accounts on a cPanel/WHM server.
What it does:
- Reads
/etc/userdomains(cPanel's domain-to-user mapping) - Filters for top-level domains (excludes subdomains)
- Queries DNS for A records using
dig - Performs WHOIS lookups for registrar and nameservers
- Groups output by cPanel user
Usage:
# Human-readable grouped output
get_nameservers.sh
# TSV format (tab-separated for spreadsheets)
get_nameservers.sh --csvOutput includes:
- Domain name
- IP address(es)
- Registrar
- Nameservers
Requirements: dig, whois, cPanel environment (/etc/userdomains)
Purpose: Bulk ban IPs from copy-pasted lists using CSF.
What it does:
- Accepts multi-line input (paste IPs, press Enter, then Ctrl+D)
- Extracts IPs using regex (handles various formats)
- Performs GeoIP lookup for each IP
- Bans via CSF with geographic metadata
Usage:
ban_ips.sh
# Paste IPs (one per line or mixed text)
# Press Ctrl+D to processRequirements: csf, geoiplookup
Purpose: Recursively fix file and directory permissions for web files.
What it does:
- Sets files (
.php,.js,.png,.jpg,.jpeg,.txt,.css) to644 - Sets directories to
755 - Defaults to current directory or accepts path argument
Usage:
# Fix current directory
fix_permissions
# Fix specific path
fix_permissions /home/user/public_htmlUse Case: After manual file uploads or ownership issues.
Auto-commits modified .htaccess files across all cPanel accounts to SVN, then fixes ownership.
Requirements: svn, cPanel environment (/etc/trueuserowners)
sudo commit_htaccess_files.shRuns chkrootkit -q and filters known false positives (e.g., cPanel's port 465).
Requirements: chkrootkit installed at /usr/local/src/chkrootkit
chkrootkit_output_cleanKills all PHP processes running longer than 1 hour.
Usage:
sudo kill_long_php.shUse Case: Terminate runaway PHP scripts consuming resources.
Monitors a file using inotify and adds IPs to CSF whitelist when detected.
Monitors for whitelist requests and processes them.
Sends the owner a whitelist audit message after a WhitelistMyIP request. In
addition to CSF deny files and external blocklists, it captures live and tracked
high_volume_bans evidence on both hosting servers. This preserves the original
automated-ban reason before the next allowlist reconciliation removes the
trusted address from the auxiliary deny set.
The notifier also queues a ClaudeGram wake in the server-operations chat with the client name, site, IP, and captured block evidence. The wake asks the ops agent to reconstruct the original block, classify a false positive versus real abuse, fix a faulty detector when appropriate, and verify the allowlist and site health. Client email addresses are intentionally excluded from the wake prompt. Repeated requests for the same IP are collapsed into one investigation during a 10-minute window, while the existing email remains as a delivery fallback.
sudo ./scripts/whitelist_notify.sh 203.0.113.10 "Client Name" \
"client@example.com" "example.com"Simple wrapper to restart SSH daemon.
Removes old log files (implementation TBD - check script for details).
Counts active SSH connections.
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2014 Gabriel Serafini
Contributions are welcome! Please:
- Test thoroughly in a VM/staging environment
- Document changes in commit messages
- Follow existing code style (bash best practices, shellcheck compliance)
- Add usage examples for new scripts or flags
- Update README.md if adding new scripts or features
Bug Reports: Open an issue with reproduction steps and environment details.
Security Issues: Email gserafini@gmail.com directly for security vulnerabilities.
Repository: https://github.com/gserafini/useful-server-scripts
Built with assistance from: Gemini Advanced (for csf_ban_wp_login_attackers and get_nameservers.sh)
scripts/wp-cpanel is the dc3-1 /usr/local/bin/wp launcher. It uses cPanel's
CLI PHP selector with --ea-reference-dir=. so maintenance commands use the
PHP configured for the current WordPress directory, instead of the system
default selected from the root-owned PHAR directory. Always change into the
intended WordPress root before plain wp commands. For commands from elsewhere,
use the verified explicit phpXX /usr/local/bin/wp --path=... form.
The unmodified vendor PHAR is /usr/local/lib/wp-cli/wp-cli.phar. Both argv
arrays name that file so wp cli update updates the PHAR without replacing
the launcher. Explicit PHP invocations continue to work. This does not change
cPanel's system default or any site's web PHP configuration.
Before deployment, preserve/hash the installed PHAR, install an identical copy
at that library path, and test the staged launcher as the account owner with
--info and a read-only plugin-loading command. Only then atomically replace
/usr/local/bin/wp with the reviewed launcher. Keep the original PHAR as a
rollback receipt. Do not run the generic all-script symlink installer on this
launcher; deployment is explicit and dc3-1-only.
Test: python3.12 tests/test_wp_cpanel.py (requires cPanel EA PHP 8.2/8.5 and
the Share root). The fixture runs outside cPanel's noexec securetmp.
This repository uses master; publish the checked-out branch with
git push origin HEAD rather than assuming a main branch.