One script. Every distro. Safe to run over SSH.
Auto-detects Ubuntu, Debian, RHEL, AlmaLinux, Rocky, CentOS, Fedora, Amazon Linux, openSUSE, SLES, Arch, and Manjaro from
/etc/os-release— then picks the right package manager, firewall, MAC system, and service unit for that family. Same prompt, same outcome, on any box you own.
A fresh server or VPS comes with a long list of defaults that are wrong for anything exposed to the internet — password SSH, no firewall, unattended-upgrades off, no MAC, the running kernel from the install ISO, no log rotation. Fixing that by hand means reading 7 manpages and getting the package name right per distro. This script:
- One-liner, zero install.
curl | bashwith no prerequisites; the script fetches its own helpers on demand. - Resumable over SSH. Auto-wraps itself in a detached
tmuxsession on the first prompt, so a dropped connection never aborts a longdnf upgrade. Reattach withtmux attach -t linux-setup. - Refuses to lock you out. SSH hardening will not touch
PasswordAuthenticationuntil a fresh ed25519 key has been validated, and it never changes the port. A self-heal watchdog (systemd timer or cron) re-opens the port and re-enables password auth if sshd ever dies. - Rollback log per file. Every config it edits is backed up to a
timestamped copy; the index lives at
/var/log/linux-install-rollback.logand is onecpaway from a full undo. - Three security profiles + a 20-tool maintenance suite. From "Recommended" (firewall + updates, 6 steps, no SSH risk) to "Full" (Tor + IPv6 disable + ASR + deep clean, 12 steps). Maintenance menu re-runs any step on a live box without re-hardening.
| What you get | How |
|---|---|
| Firewall (UFW on apt, firewalld everywhere else) | Default-deny incoming; opens SSH only if you say so |
| Kernel + full system update | apt full-upgrade / dnf upgrade / zypper update / pacman -Syu — auto-detected |
| Old-kernel prune | Keeps running kernel + one spare; prints names before removing |
| SSH hardening | PasswordAuthentication no gated on validated pubkey; port never changed |
| Fail2ban, sysctl profile, AppArmor/SELinux check | per-distro package names |
| Tor, dnscrypt-proxy, unattended-upgrades, DeepClean | optional per profile |
| Rollback log | /var/log/linux-install-rollback.log — original\tbackup per file |
| SSH self-heal | --install-self-heal — systemd timer or cron, every 60s |
| 20-tool maintenance suite | Re-runs any step, lists keys, tails logs, dumps config |
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | sudo bash -s --Read it first:
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | less
The script prompts you per category. Full profile on a server runs in
auto mode — SSH hardening is applied without the interactive lockout-
prone prompts (it never disables PasswordAuthentication unless it
detects a working pubkey, and it never changes the port), so the only
way to get locked out is the OpenSSH config breaking — in which case
the in-script restore_ssh routine or Tailscale SSH gets you back in.
| Family | Distros | Package manager | Firewall | Notes |
|---|---|---|---|---|
| Debian | Ubuntu (incl. 24.04 LTS, 22.04, 20.04), Debian 12/11 | apt |
ufw |
full feature set (unattended-upgrades, AppArmor) |
| RHEL | RHEL 8/9, AlmaLinux 8/9, Rocky 8/9, CentOS Stream | dnf |
firewalld |
AppArmor replaced by SELinux |
| Legacy RHEL | CentOS 7, RHEL 7 | yum |
firewalld |
legacy; no dnf |
| Amazon Linux | Amazon Linux 2023 | dnf |
firewalld |
RHEL-compatible; SELinux enforcing by default |
| Fedora | Fedora 39+ | dnf |
firewalld |
AppArmor not on by default — uses SELinux |
| SUSE | openSUSE Leap 15, SLES 15 | zypper |
firewalld |
AppArmor profile packages available |
| Arch | Arch Linux, Manjaro | pacman |
firewalld |
AppArmor / fail2ban via AUR |
Distribution is detected from
/etc/os-release(withID_LIKEfallback). The package manager is then selected from the orderpacman → zypper → dnf → yum → apt, so Arch derivatives pickpacman, SUSE pickszypper, RHEL/Fedora pickdnf, Debian/Ubuntu pickapt. No manual flag required.
Run the general interactive script directly from the repo — it prompts you per category (environment type, SSH lockout-prone steps, ambiguous DNS/Tor/ IPv6 choices, and the new helper scripts are fetched on-demand):
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | sudo bashReview it first:
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | less
Profiles: the script asks which profile to apply — Recommended (safe), Standard (full hardening + SSH), Full (everything including Tor/IPv6/ASR/ DeepClean), or Custom (you confirm every step). Risky actions (SSH hardening, IPv6, DNS method, Tor, attack-surface reduction) always prompt individually before touching anything.
Full profile on a server runs in "auto" mode: SSH hardening is applied
without the interactive lockout-prone prompts (it never disables
PasswordAuthentication unless it detects a working pubkey, and it never
changes the port), so the only way to get locked out is the OpenSSH config
breaking — in which case the in-script restore_ssh routine or Tailscale
SSH can get you back in.
Progress checklist: the script prints a colored bar chart (e.g.
━━━ PROGRESS ████████████░░░░ 12/17 (70%) ━━━) before every step, so you
always see what's already done and what's coming.
origin/main
linuxinstall.sh auto-detects the package manager and updates the kernel
and all system packages in one pass. The mapping is:
| Family | Command |
|---|---|
| Debian / Ubuntu | apt full-upgrade -y |
| RHEL 8+ / AlmaLinux 8/9 / Rocky | dnf upgrade --refresh -y |
| Fedora | dnf upgrade --refresh -y |
| Legacy CentOS 7 / RHEL 7 | yum update -y |
| openSUSE Leap 15 / SLES 15 | zypper update -y |
| Arch / Manjaro | pacman -Syu --noconfirm |
After updating, the script:
- Runs the package manager's built-in autoremove/orphan cleanup.
- On
aptonly: also runspurge-old-kernels(if present) and prunes the oldest installedlinux-image-*/linux-headers-*packages, keeping the running kernel and one spare. Pruned package names are printed before removal so you can cancel by re-running withNto the prune prompt. - Compares the newest installed kernel in
/boot/vmlinuz-*touname -r; if they differ, sets_KERNEL_UPDATE_PENDING=1. - The end-of-run summary offers a reboot (never auto-reboots mid-run).
No HWE, no mainline, no edge kernels. The script does not change the running kernel — a reboot is the user's choice.
At the end of the run the script prints a colored bar-chart summary of what it actually did (packages upgraded/installed, services hardened, sysctls applied, firewall rules, auth keys, Tor services, config files backed up, approximate disk freed). Every config file it modifies is copied to a timestamped backup and appended to a single log:
cat /var/log/linux-install-rollback.log
# format: original_path<TAB>backup_path
# restore any file with: sudo cp <backup_path> <original_path>Before touching anything, the script also scans for existing SSH public
keys, prints a recovery ed25519 key it generates on the server (so you
can scp it to your laptop), and refuses to disable
PasswordAuthentication until a fresh key has been validated.
If you launch the script over SSH, the very first thing it does is detect
the SSH session and automatically re-exec itself inside a detached tmux
session named linux-setup, so a transient network blip won't abort the
run.
Before you do anything that might disconnect (dnf upgrade, firewalld reload, SSH restart, etc.) copy this line — you'll need it to re-attach after a disconnect:
tmux attach -t linux-setupIf you were disconnected entirely, log back in over SSH and run
tmux attach -t linux-setup to rejoin the session. If you started the
one-liner from a local terminal (not over SSH), the tmux wrap is skipped
automatically and there's nothing to re-attach to. When the script
finishes successfully, the tmux session closes itself; if it fails, the
session is left intact for inspection.
Tailscale SSH bypasses OpenSSH settings — it authenticates via the
Tailscale identity layer, so it works even when PasswordAuthentication=no
or the sshd service is down. Prefer Tailscale SSH for recovery.
Automatic recovery (SSH self-heal guard): the script can install a self-heal guard that runs at every boot and every 60 seconds. If sshd ever becomes unreachable, the guard:
- re-validates
sshd -t - re-opens the SSH port in firewalld / UFW if it was dropped
- restarts sshd if it stopped
- re-enables
PasswordAuthentication yesif a lockout is detected (only when no pubkeys are installed)
It is offered automatically at the end of harden_ssh when you answer
"yes" to the "use remote SSH?" prompt. You can also install it
standalone, remove it, or trigger a check manually:
sudo bash linuxinstall.sh --install-self-heal # install
sudo bash linuxinstall.sh --self-heal # trigger now (used by cron)
sudo bash linuxinstall.sh --no-self-heal # removeOn systemd systems the guard is a systemd timer (neohiro-ssh-watchdog.timer)
that fires 30s after boot and every 60s thereafter. On systems without
systemd (e.g. some minimal images) it installs as a cron.d job with
@reboot and * * * * * entries. Every action is logged to
/var/log/neohiro-ssh-watchdog.log.
Quick recovery (from any working session — console, Tailscale SSH, or out-of-band):
# 1. Diagnose and auto-fix most lockout causes
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/restore_ssh.sh | sudo bash -s --
# or, equivalently, via the main script's first-class menu entry
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | sudo bash -s -- --restore-ssh
# 2. Or undo every config change the script made (dry-run):
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | sudo bash -s -- --rollback
# 3. Or do it manually — re-enable password auth, restart sshd
sudo sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl restart sshdPackage names differ by distro:
aptusesopenssh-server,dnf/yumalso useopenssh-server, butzypperandpacmanuseopenssh. The script'srestore_sshroutine handles this automatically.
Specific causes and fixes:
| Symptom | Likely cause | Fix |
|---|---|---|
Connection refused after reboot |
sshd not running or listening on wrong port | sudo systemctl restart sshd; sudo ss -tulnp | grep sshd |
No route to host |
firewalld / UFW blocking | sudo firewall-cmd --add-service=ssh --permanent && sudo firewall-cmd --reload (RHEL/Fedora) — or sudo ufw allow ssh (Debian/Ubuntu) |
Permission denied (publickey) |
Port changed to non-22 | ssh -p 2222 user@host |
| OpenSSH lockout (no pubkey, PasswordAuth=no) | Only possible if you have Tailscale SSH or console access | restore_ssh routine above, or out-of-band console |
Out-of-band console only (no SSH at all): boot cloud provider rescue ISO or use Hetzner/DO/Vultr recovery console, mount root, then:
sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /mnt/etc/ssh/sshd_config
sed -i 's/^Port .*/Port 22/' /mnt/etc/ssh/sshd_config
# or restore a backup: ls /mnt/etc/ssh/sshd_config.bak.* && cp <latest> /mnt/etc/ssh/sshd_configThe script's main() tree offers a Maintenance suite (distinctive
magenta header) and a Restore SSH entry (above Maintenance). The
Restore-SSH entry calls the same diagnostic routine that the
--restore-ssh flag and the standalone restore_ssh.sh script use.
The Maintenance suite itself is expanded to include:
| # | Option | What it does |
|---|---|---|
| 1–13 | system / dns / firewall / tor / ssh / fail2ban / unattended / ipv6 / sysctl / apparmor / pam / OptimizeLinuxASR / DeepClean | Re-run any step on demand |
| 14 | SSH diagnostics & lockout fix | Same routine as --restore-ssh |
| 15 | Authorized keys | List all keys in every user's authorized_keys |
| 16 | SSH config review | Print every key directive from sshd_config and drop-ins |
| 17 | SSH self-heal guard | Install / remove / status of the per-minute watchdog |
| 18 | Logs | Tail /var/log/linux-install-rollback.log and /var/log/neohiro-ssh-watchdog.log |
| 19 | System info | Uptime, load, memory, disk, CPU, listening ports |
| 20 | Back to main menu | — |
The self-heal guard runs as root via systemd or cron and never
modifies authorized_keys or any credentials — it only fixes config
and service state, so it cannot open the system to a new attacker.
The interactive script covers everything below, but the equivalent commands per distribution family are listed for reference.
Same logic as update_kernel inside linuxinstall.sh. Auto-detects the
package manager and updates kernel + system packages, then prunes old
kernels (keeps 2 newest on apt).
Debian / Ubuntu:
sudo apt update
sudo apt full-upgrade -y
sudo apt autoremove --purge -yRHEL 8+ / AlmaLinux / Rocky / Fedora:
sudo dnf upgrade --refresh -y
sudo dnf autoremove -yLegacy CentOS 7 / RHEL 7:
sudo yum update -y
sudo yum autoremove -yopenSUSE Leap 15 / SLES 15:
sudo zypper refresh
sudo zypper update -yArch / Manjaro:
sudo pacman -Syu
sudo pacman -Qdtq | xargs -r sudo pacman -RnsVerify and reboot (if kernel changed):
uname -r
# verify per-family:
dpkg -l 'linux-image-*' | grep '^ii' # apt
rpm -q kernel # dnf / yum
rpm -q kernel-default # zypper
pacman -Q linux # pacman
sudo systemctl rebootDebian / Ubuntu (UFW):
sudo apt install ufw -y
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow ssh # for servers
sudo ufw enable
sudo ufw status verboseRHEL / Fedora / SUSE / Arch (firewalld):
sudo dnf install firewalld -y # or yum / zypper / pacman
sudo systemctl enable --now firewalld
sudo firewall-cmd --add-service=ssh --permanent
sudo firewall-cmd --reload
sudo firewall-cmd --list-allInstall:
# apt
sudo apt install dnscrypt-proxy -y
# dnf / yum
sudo dnf install dnscrypt-proxy -y
# zypper
sudo zypper install dnscrypt-proxy
# pacman
sudo pacman -S dnscrypt-proxyPoint your system resolver at 127.0.0.2:53 (the listen address the
script writes). This is intentional — it avoids the systemd-resolved
stub on 127.0.0.53:53 and direct queries on 127.0.0.1.
# apt / dnf / yum / zypper
sudo <pkgmgr> install -y tor
# pacman (not in core — build from AUR)
yay -S tor
sudo systemctl enable --now torApt-based distros (Ubuntu / Debian):
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgradesRHEL / Fedora / AlmaLinux / Rocky:
sudo dnf install dnf-automatic -y
sudo systemctl enable --now dnf-automatic.timer
# or dnf-automatic-install.timer for install-onlyopenSUSE:
sudo zypper install yast2-online-update-configuration
# configure: YaST2 → Online Update ConfigurationArch:
yay -S aur-auto-update # AUR helper
linuxinstall.shinstalls and configuresunattended-upgradesonly on apt-based distros. On other families it prints a one-line suggestion and skips.
sudo fwupdmgr refresh && sudo fwupdmgr update # LVFS firmware
mokutil --sb-state # Secure Boot stateFull-disk encryption (LUKS) must be chosen at install time — on the next reinstall tick it; it protects all data when the machine is powered off or stolen. Verify clock sync:
timedatectl statusSave as /etc/sysctl.d/99-hardening.conf (identical on every distro):
# information disclosure
kernel.dmesg_restrict=1
kernel.kptr_restrict=2
kernel.unprivileged_bpf_disabled=1
net.core.bpf_jit_harden=2
kernel.yama.ptrace_scope=1
kernel.kexec_load_disabled=1
kernel.sysrq=0
kernel.randomize_va_space=2
fs.suid_dumpable=0
fs.protected_symlinks=1
fs.protected_hardlinks=1
fs.protected_fifos=2
fs.protected_regular=2
# network stack
net.ipv4.ip_forward=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_syncookies=1
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
Apply:
sudo sysctl --system| Family | MAC system | Status | Script action |
|---|---|---|---|
| Debian / Ubuntu | AppArmor | default on | install apparmor + apparmor-utils, enable service |
| openSUSE Leap / SLES | AppArmor | profiles available | install apparmor-profiles + apparmor-utils, enable service |
| Arch / Manjaro | AppArmor | AUR | print AUR hint (yay -S apparmor apparmor-utils) |
| RHEL / Fedora / Alma / Rocky / CentOS | SELinux | default enforcing | skip AppArmor; check getenforce is Enforcing |
On RHEL/Fedora, set permissive → enforcing with:
sudo setenforce 1
# permanent: /etc/selinux/config -> SELINUX=enforcing (then reboot)Check AppArmor profiles:
sudo aa-status
sudo aa-enforce /etc/apparmor.d/<profile>Prefer keys over passwords:
ssh-keygen -t ed25519Then in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30s
X11Forwarding no
AllowUsers <youruser>
Validate before you disconnect:
sudo sshd -t && sudo systemctl restart sshdThe service unit is
sshon Debian/Ubuntu andsshdon RHEL/Fedora/ SUSE/Arch. The script detects both.
Stronger password quality — the script installs the right package per
distro (libpam-pwquality on apt, libpwquality on dnf/yum/zypper/
pacman). Then in /etc/security/pwquality.conf:
minlen = 14
minclass = 3
maxrepeat = 3
Lock accounts after failed logins — /etc/security/faillock.conf:
deny = 5
unlock_time = 900
Auto-close idle shells — /etc/profile.d/99-tmout.sh:
TMOUT=900; readonly TMOUT; export TMOUTTighten default umask (UMASK 027 in /etc/login.defs) and forbid core
dumps — add to /etc/security/limits.conf:
* hard core 0
bash tests/test_linuxinstall.sh # 65 tests: parse, logic, UX coverage, snapshot
bash tests/test_updater.sh # 43 tests: dispatcher, race safety, version floor
shellcheck -S warning *.sh lib/*.sh tests/*.sh # lintTo regenerate snapshot fixtures after a deliberate UX change:
bash tests/gen_snapshots.sh # re-captures print_welcome + print_metrics_summaryThe snapshot test normalises host-specific lines (hostname, OS, kernel, arch) before comparison so fixtures are portable. On macOS (bash 3.2 default) the lib/updater.sh version guard fires cleanly — this is verified by the CI matrix entry bash:3.2-alpine3.18.
sudo ufw status verbose # apt
sudo firewall-cmd --list-all # everything else
sudo rkhunter --check # rootkit sweep
sudo aide --check # file integrity
ss -tulnp # re-check listeners- Corrade.md — Docker-based IR bot gateway (Docker required; works on all distros with
dockerinstalled). - DNSPROXY.md — AdGuard dnsproxy in Docker, with cross-distro firewall commands (UFW for apt, firewalld for dnf/yum/zypper/pacman).
- SHADOWSOCKS-LIBEV.md — Shadowsocks-libev SOCKS5 proxy, with cross-distro package names and firewall commands.
⭐ Stargaze to help others secure their Linux install
- 💖 Sponsor neohiro on GitHub — covers API + hosting costs
- 🌐 neohiro.github.io — main site
- 🎬 FrenzyPenguin Media — video deep-dives
- 🧬 transhumanists — companion dashboard for human progress