Aggregates v4.7.0 through v4.7.8. Roughly 1850 lines added to init.sh
(2206 -> 4059); 10 CI jobs (was 1); README +113/+115 lines (zh/en).
CRIT/HIGH fixes across iterations:
- existing-user passwd-d regression (v4.7.1)
- rollback artifact tracking with provenance (v4.7.0/4/5)
- restart_sshd direct-spawn fallback for no-init envs (v4.7.1)
- chroot pkill defense (v4.7.6/8: both direct-spawn and systemctl paths)
- PATH inheritance hardening (v4.7.8): /usr/local/sbin:/usr/local/bin:
/usr/sbin:/usr/bin:/sbin:/bin set at script top
- audit_log newline-injection defense (v4.7.6)
- script-level mutex with stale-PID + kill -0 liveness (v4.7.6/7)
- ARTIFACT_FILE mirror to RUNTIME_DIR for post-SIGKILL recovery (v4.7.6)
- SELinux NOOP-vs-ADD provenance (v4.7.5) + cross-run cleanup (v4.7.4)
- leading-zero port bypass of is_hard_reserved (v4.7.6, e.g. --port=03306
silently shadowing MySQL)
- setup_rollback ENOSPC verification (v4.7.7): empty snapshot would
blank live sshd_config on rollback
- record_last_applied atomic write via tmp+mv (v4.7.7)
- audit_log stderr fallback when /var/log is unwritable (v4.7.7)
- duplicate-argv hard-fail (v4.7.6): --user/--port/--key-* duplicates
now refused instead of silent last-wins
- validate_username + --key-gh per-line grep bypass (v4.7.6): newline-
containing values now rejected via case-match before grep
- leading-zero port (v4.7.6): rejected to prevent sshd parsing 03306
as decimal 3306, shadowing MySQL
41 audit_log actions for SIEM ingestion. README documents all of them
with bold-marked high-priority alerts (ROLLBACK_RM_FAILED /
STRANDED_MUTATIONS_DETECTED / SUDO_FAIL / etc).
10 CI jobs cover: shellcheck (POSIX -s sh), 9-distro matrix with 18
assertions + idempotency re-run, live restart with banner check,
existing-user safety (passwd -S = P), cross-user sudoers glob safety,
legacy v4.6.x sudoers cleanup, CLI flag smoke (--version/-V/-h/
--no-ip-probe/--strict), --no-ip-probe verification, ACCOUNT_KEPT_LOCKED
path, restore.sh checksum gate + FORCE=1 bypass.
Branch convention: dev/v4.7.8 — not yet promoted to main. Awaiting
real-machine deployment validation before tag + release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
257 lines
14 KiB
Markdown
257 lines
14 KiB
Markdown
# Linux Server Init & SSH Hardening Script
|
|
|
|
[](https://github.com/247like/linux-ssh-init-sh/actions/workflows/test.yml)
|
|

|
|
[](LICENSE)
|
|
[](https://github.com/247like/linux-ssh-init-sh/releases)
|
|
[](https://github.com/247like/linux-ssh-init-sh/stargazers)
|
|
|
|
[](README.md) [](README_EN.md)
|
|
|
|
---
|
|
|
|
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`. No `bash` dependency. Runs perfectly on `dash` (Debian) and `ash` (Alpine/Busybox).
|
|
* **Security Architecture (Fortress Pro)**:
|
|
* **Managed Config Block**: Inserts configuration at the **top** of `sshd_config` to strictly override vendor defaults (bypasses the Debian 12 `Include` trap).
|
|
* **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.conf` to prevent OOM kills and ensures SSHD restarts automatically on failure.
|
|
* **Deadlock Prevention**: Intelligently detects authentication states to prevent "Password Disabled + No Key" lockouts.
|
|
* **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)
|
|
```bash
|
|
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.sh` before executing. Avoid `curl … | sh`.
|
|
|
|
#### 2. Force English UI
|
|
```bash
|
|
./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)*
|
|
|
|
```bash
|
|
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)*
|
|
|
|
```bash
|
|
./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+).<br>👉 *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): `curl` or `wget`, `sudo`, `openssh-server`, `ss` or `netstat`, `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 like `server-init-<user>-special-policy` are preserved and sibling accounts that share a hyphen-prefix (e.g. `admin` vs `admin-bot`) cannot be collaterally cleaned. Each removal is recorded in audit log as `LEGACY_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`
|
|
* Stderr fallback format (when `/var/log` is unwritable): `AUDIT-FALLBACK <timestamp> ACTION=<name> DETAILS=<...>`
|
|
* Designed for SIEM ingestion
|
|
* **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.
|
|
|
|
1. Find the latest backup directory:
|
|
```bash
|
|
ls -ld /var/backups/ssh-config/*
|
|
```
|
|
2. Enter the directory and run the restore script:
|
|
```bash
|
|
cd /var/backups/ssh-config/<TIMESTAMP>/
|
|
sh restore.sh
|
|
```
|
|
`restore.sh` will: ① verify checksums → ② restore `sshd_config` + `sshd_config.d/` → ③ run `sshd -t` → ④ restart sshd. Any failure aborts with a non-zero exit code.
|
|
|
|
**Override**: if `checksums.sha256` is missing or `sha256sum` is unavailable, `restore.sh` **REFUSES** to run (defends against restoring a tampered backup). If you're certain the backup is good and need to bypass:
|
|
```bash
|
|
FORCE=1 sh restore.sh
|
|
```
|
|
Use only when you **trust the backup contents** (e.g. you manually pruned `checksums.sha256` yourself).
|
|
|
|
> ⚠ `restore.sh` only restores **`sshd_config` and 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:
|
|
|
|
```bash
|
|
# 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](LICENSE).
|
|
|
|
---
|
|
|
|
<div align="center">
|
|
|
|
If you found this tool helpful, please give it a ⭐ Star!
|
|
|
|
[Report Bug](https://github.com/247like/linux-ssh-init-sh/issues) · [Request Feature](https://github.com/247like/linux-ssh-init-sh/issues)
|
|
|
|
</div>
|