Linux Server Init & SSH Hardening Script
A production-ready, POSIX-compliant shell script designed to initialize Linux servers and harden SSH security in minutes.
It safely handles SSH key deployment, port changing, user creation, TCP BBR enablement, and system updates, while ensuring compatibility across Debian, Ubuntu, CentOS, RHEL, and Alpine Linux.
✨ Key Features
- Universal Compatibility: Works flawlessly on Debian, Ubuntu, CentOS/RHEL, Alma/Rocky, and Alpine Linux.
- POSIX Compliant: Written in pure
/bin/sh. Nobashdependency. Runs perfectly ondash(Debian) andash(Alpine/Busybox). - Security Architecture (Fortress Pro):
- Managed Config Block: Inserts configuration at the top of
sshd_configto strictly override vendor defaults (bypasses the Debian 12Includetrap). - Auto-Rollback: If SSHD validation fails, port is not listening, or connection test fails during execution, the script automatically reverts all system changes.
- Service Protection (Anti-Kill): Adds a systemd
override.confto prevent OOM kills and ensures SSHD restarts automatically on failure. - Deadlock Prevention: Intelligently detects authentication states to prevent "Password Disabled + No Key" lockouts.
- Managed Config Block: Inserts configuration at the top of
- Automation Friendly:
- Supports Headless Mode allowing zero-interaction unattended installation.
- Audit & Reporting: Automatically generates detailed operation audit logs and system health reports.
🚀 Quick Start
Run the following command as root.
1. Interactive Run (Recommended)
curl -fsSL https://raw.githubusercontent.com/247like/linux-ssh-init-sh/main/init.sh -o init.sh && chmod +x init.sh && ./init.sh
🛡️ Audit tip: this script modifies SSH config — prefer the "download then run" form above so you can inspect
init.shbefore executing. Avoidcurl … | sh.
2. Force English UI
./init.sh --lang=en
🤖 Automation (Headless Mode)
Suitable for CI/CD pipelines or bulk provisioning. Use command line arguments to pass configurations and --yes to skip confirmation.
Full Automatic Example
(Configure Root user, random port, fetch key from GitHub, enable BBR, update system, auto-confirm)
curl -fsSL https://raw.githubusercontent.com/247like/linux-ssh-init-sh/main/init.sh | sh -s -- \
--user=root \
--port=random \
--key-gh=247like \
--bbr \
--update \
--strict \
--yes
Semi-Automatic Example
(Specify key URL, choose other options manually)
./init.sh --key-url=https://my-server.com/id_ed25519.pub
⚙️ Arguments
The script supports rich command-line arguments to control its behavior:
| Category | Argument | Description |
|---|---|---|
| Control | --lang=en |
Force English UI |
--yes |
Auto Confirm: Skip the final "Proceed?" prompt | |
--strict |
Strict Mode: Exit immediately on error (see below) | |
--delay-restart |
Delay Restart: Apply config but do not restart SSHD immediately | |
--no-ip-probe |
Skip the external IP lookup (api.ipify.org) — for offline/air-gapped hosts |
|
-V / --version |
Print version and exit | |
-h / --help |
Show full help | |
| User/Port | --user=root |
Specify login user (root or username) |
--port=22 |
Keep default port 22 | |
--port=random |
Generate random high port (49152-65535) | |
--port=2222 |
Specify a specific port number | |
| Keys | --key-gh=username |
Fetch public key from GitHub |
--key-url=url |
Download public key from URL | |
--key-raw="ssh-..." |
Pass public key string directly | |
| System | --update |
Enable system package update |
--no-update |
Skip system update | |
--bbr |
Enable TCP BBR congestion control | |
--no-bbr |
Disable BBR |
⚙️ Normal Mode vs. Strict Mode
| Feature | Normal Mode (Default) | Strict Mode (--strict) |
|---|---|---|
| Philosophy | "Don't Lockout" (Best Effort) | "Compliance First" (Zero Tolerance) |
| Key Failure | Password auth remains enabled with a warning, and any newly-created user stays locked (v4.7.0+). 👉 Existing root/password users can still log in to repair; the new account never becomes passwordless+open. |
Script exits immediately. |
| Existing-user password | v4.7.1+: the script does NOT clear an existing user's password; passwd -d runs only for accounts newly created in this run |
Same |
| Port Failure | Falls back to port 22. | Script exits immediately. |
| HTTP key URL | Warn-and-continue | Reject http://, require https:// |
| Firewall rule add failed | Warn only | Warn + log (non-22 ports) |
📦 Requirements
- Privileges: must run as
root - Mandatory commands:
cat / grep / awk / sed / cp / mv / chmod / chown / mkdir / rm / id - Recommended commands (auto-installed if missing):
curlorwget,sudo,openssh-server,ssornetstat,nc,base64,ssh-keygen - Verified distros (full CI matrix):
- Debian 11 / 12
- Ubuntu 22.04 / 24.04
- Alpine (latest)
- AlmaLinux 9 / Rocky Linux 9 / CentOS Stream 9
- CentOS 7 (EOL — best effort, allow-failure in CI)
📁 Files the script creates / modifies
| Path | Purpose |
|---|---|
/etc/ssh/sshd_config |
Managed block injected; conflicting drop-ins renamed *.bak_server_init |
/etc/ssh/sshd_config.d/*.conf |
Conflicting drop-ins backed up/disabled |
/etc/sudoers.d/server-init-<user> |
Stable filename (v4.7.0+) — delete to revoke |
/etc/systemd/system/sshd.service.d/override.conf |
Anti-kill / restart policy |
/etc/profile.d/z99-ssh-init-banner.sh |
Login banner |
/etc/motd + /etc/motd.bak |
Strips stale server-init lines; first run preserves real .bak |
/etc/sysctl.conf |
BBR lines appended when --bbr is enabled |
| Firewall rules (ufw/firewalld/iptables/ip6tables) | Opens TCP port when --port != 22; rollback only undoes the backend the script used |
| SELinux port labels | When system is Enforcing, applies semanage port -a -t ssh_port_t; -m (modify) path cannot be cleanly rolled back |
/home/<user>/.ssh/authorized_keys |
Deployed keys; symlinks rejected; home owner verified (v4.7.3) |
/var/log/server-init.log |
Runtime log |
/var/log/server-init-audit.log |
Audit trail (redacts --key-raw / --key-url) |
/var/log/server-init-health.log |
Post-run snapshot |
/var/backups/ssh-config/<TS>/ |
Persistent backup + restore.sh + checksums.sha256 |
/var/lib/server-init/last-applied |
(v4.7.3+) records the previous successful port + firewall backend; v4.7.4 also records SELINUX_PORT=N so port-change runs clean the prior SELinux label; v4.7.5 adds SELINUX_OWNED=y/n distinguishing "label this script installed" from "label that pre-existed" — only the former is cleaned across runs |
/run/server-init/script.lock/ or /var/lib/server-init/script.lock/ |
(v4.7.6+) Script-level mutex directory (holds PID file). Auto-removed on normal exit; if the script was kill -9'd the dir may linger — the next start auto-detects whether the holder PID is still alive and reclaims if not |
/run/server-init/last-artifacts-<UID> or /var/lib/server-init/last-artifacts-<UID> |
(v4.7.6+) Artifact mirror appended during run; cleared on normal exit. If the script crashed, the next start surfaces this list to alert the operator to stranded mutations |
🔁 Idempotency / re-runs
- The managed block is removed and re-inserted on every run — no duplicate stacking
- Sudoers file uses a stable per-user name; re-runs overwrite instead of accumulating
- Upgrading from < v4.7.0: older versions left timestamped sudoers files (
server-init-<user>-YYYYMMDD...). v4.7.1 added auto-cleanup on the next run; v4.7.2 narrows the match to digits-only suffixes so admin-created files likeserver-init-<user>-special-policyare preserved and sibling accounts that share a hyphen-prefix (e.g.adminvsadmin-bot) cannot be collaterally cleaned. Each removal is recorded in audit log asLEGACY_SUDOERS_REMOVED - Backups rotate by timestamp; the 10 most recent are retained, older ones pruned automatically
🚪 Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic error / user cancel / config failure |
| 130 | SIGINT (Ctrl-C) |
| 143 | SIGTERM |
| 129 | SIGHUP |
| ≠ 0 (mid-deploy) | Triggers automatic rollback — inspect /var/log/server-init.log + audit.log |
Note: Ctrl-C during the confirmation prompt does NOT enter the rollback path (no destructive changes have happened yet).
📂 Logs & Audit
After execution, the following files are generated for troubleshooting and auditing:
📋 Log-rotation recommendation: the three log files below are append-only with no built-in rotation. For frequent runs (CI / fleet installs) drop in a logrotate config:
# /etc/logrotate.d/server-init /var/log/server-init*.log { weekly rotate 12 compress delaycompress missingok notifempty create 0600 root root }
- Run Log:
/var/log/server-init.log(Detailed debug information) - Audit Log:
/var/log/server-init-audit.log(Records key actions, timestamps, and operators)- Action categories (recommended-to-alert in bold):
- Lifecycle:
START/DONE/ROLLBACK/RESTART_REFUSED - Accounts:
USER_CREATED/PASSWORD_CLEARED/KEYS_DEPLOYED/ACCOUNT_KEPT_LOCKED/USER_REMOVE_FAILED - Sudo:
SUDOERS_WRITTEN/SUDO_FAIL/LEGACY_SUDOERS_REMOVED - System config:
MANAGED_BLOCK_INSTALLED/DROPIN_RENAMED/SYSTEMD_OVERRIDE_WRITTEN/MOTD_BANNER_WRITTEN/BBR_ENABLED/SYSTEM_UPDATE_START/SYSTEM_UPDATE_DONE - Firewall:
FIREWALL_OPENED/FIREWALL_NOOP/STALE_FIREWALL_REMOVED/LAST_APPLIED_CLEARED - SELinux:
SELINUX_PORT_ADDED/SELINUX_PORT_NOOP/SELINUX_PORT_MODIFIED/SELINUX_PORT_FAIL/STALE_SELINUX_REMOVED/STALE_SELINUX_REMOVE_FAILED/STALE_SELINUX_KEPT/STALE_SELINUX_SKIP - Incomplete-rollback alerts (high-priority):
ROLLBACK_RM_FAILED/ROLLBACK_MOTD_FAILED/ROLLBACK_MOTD_BAK_MISSING/STRANDED_MUTATIONS_DETECTED - Test / security:
LOGIN_TEST_PASSED/LOGIN_TEST_INCONCLUSIVE/INSECURE_KEY_URL/KEY_FETCH_FAILED/DIRECT_SPAWN/DIRECT_SPAWN_REFUSED
- Lifecycle:
- Stderr fallback format (when
/var/logis unwritable):AUDIT-FALLBACK <timestamp> ACTION=<name> DETAILS=<...> - Designed for SIEM ingestion
- Action categories (recommended-to-alert in bold):
- Health Report:
/var/log/server-init-health.log(Snapshot of the final system configuration state)
🆘 Disaster Recovery & Restore
The script features a dual-layer safety mechanism: Runtime Auto-Rollback and Persistent Backup Restore.
If you cannot connect to your server via SSH after the script finishes (after seeing "DONE"), log in via your Cloud Provider's VNC / Console and use one of the following methods.
Method A: One-Click Restore Script (Recommended)
Each run creates a backup with a SHA256 checksums file. restore.sh verifies checksums.sha256 before applying anything — a tampered backup is rejected.
-
Find the latest backup directory:
ls -ld /var/backups/ssh-config/* -
Enter the directory and run the restore script:
cd /var/backups/ssh-config/<TIMESTAMP>/ sh restore.shrestore.shwill: ① verify checksums → ② restoresshd_config+sshd_config.d/→ ③ runsshd -t→ ④ restart sshd. Any failure aborts with a non-zero exit code.Override: if
checksums.sha256is missing orsha256sumis unavailable,restore.shREFUSES to run (defends against restoring a tampered backup). If you're certain the backup is good and need to bypass:FORCE=1 sh restore.shUse only when you trust the backup contents (e.g. you manually pruned
checksums.sha256yourself).
⚠
restore.shonly restoressshd_configand its drop-ins. Users / sudoers / systemd overrides / firewall rules created during the run are best undone by the runtime auto-rollback (which fires on any failure during the script run); see the "Files the script creates / modifies" table above for manual cleanup.
Method B: Manual Restore
If restore.sh is unavailable, copy files by hand:
# 1. Verify (recommended)
cd /var/backups/ssh-config/<TIMESTAMP>/ && sha256sum -c checksums.sha256
# 2. Overwrite configuration
cp /var/backups/ssh-config/<TIMESTAMP>/sshd_config /etc/ssh/sshd_config
# 3. Restart service
systemctl restart sshd || service sshd restart
⚠️ Disclaimer
This script modifies critical system configurations (SSH). While it includes multiple safety checks and automatic rollback mechanisms, please ensure you have an alternative access method (such as a VNC/KVM Console) to your server to prevent lockout in case of network interruptions or unexpected configuration errors.
📄 License
This project is released under the MIT License.
If you found this tool helpful, please give it a ⭐ Star!