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. - HTTPS-only package transport, enforced before every download. A
plaintext
http://mirror lets anyone on the path swap a.deb/.rpm/wheel for their own. The guard audits every app store on the box — apt, dnf, yum, zypper, pacman, apk, flatpak, snap, docker, brew, pip, npm, cargo, gem, nix, fwupd — rewrites apt automatically with rollback if a mirror can't speak TLS, and reports the rest. See Package transport security. - Three security profiles + a 21-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 |
| HTTPS-only package transport | enforced before every apt/dnf/yum/zypper/pacman/apk/pip/npm/… download — --apt-https |
| Disconnect-safe | SSH runs auto-wrap in tmux; subscripts get their own session; the reattach command is always printed |
| Rollback log | /var/log/linux-install-rollback.log — original\tbackup per file |
| SSH self-heal | --install-self-heal — systemd timer or cron, every 60s |
| 21-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.
A plaintext http:// mirror means anyone who can intercept the route — a
hostile Wi-Fi, a compromised router, an upstream CDN node — can swap the
.deb / .rpm / .pkgz / wheel you just downloaded for one of theirs.
Signature checks catch forged packages, but they do not stop a
downgrade to an older, genuinely-signed, vulnerable build. Only TLS on
the transport closes that gap.
So every entry point in this repo runs a guard before anything is
fetched. It is the first workflow step (on every profile, including
Custom), it runs once per process, and it is idempotent, so re-runs and
--auto are cheap.
# Apply it on its own (normally automatic)
sudo bash linuxinstall.sh --apt-https
# Report only; never modifies anything. Exit 1 if anything is plaintext.
sudo bash linuxinstall.sh --apt-https-audit
# Undo: restore every backup and remove the policy drop-in.
sudo bash linuxinstall.sh --apt-https-off
# Or standalone, no installer needed:
sudo bash lib/apt-https.sh # enforce
sudo bash lib/apt-https.sh --report # audit
sudo bash lib/apt-https.sh --revert # undoThis is not apt-specific. Every distro and language has its own "app store", and several ship plaintext HTTP by default. The audit spans all of them and only lists the ones actually installed:
| Store | Where the transport is configured |
|---|---|
apt |
/etc/apt/sources.list, sources.list.d/*.list, DEB822 *.sources |
dnf / yum |
/etc/yum.repos.d/*.repo (baseurl, metalink, mirrorlist, gpgkey) |
zypper |
/etc/zypp/repos.d/*.repo |
pacman |
/etc/pacman.d/* (Server=) |
apk (Alpine) |
/etc/apk/repositories — plaintext http:// by default on many images |
flatpak |
flatpak remotes |
snap |
store is snapd-managed; no operator-configurable transport |
docker |
/etc/docker/daemon.json (registry-mirrors, insecure-registries) |
brew |
HOMEBREW_BREW_GIT_REMOTE, HOMEBREW_API_DOMAIN, … |
pip |
PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, pip.conf |
npm |
NPM_CONFIG_REGISTRY, .npmrc |
cargo |
CARGO_REGISTRIES_CRATES_IO_INDEX, ~/.cargo/config.toml |
gem |
GEM_SOURCE, .gemrc |
nix |
nix.conf (substituters, channel) |
fwupd |
/etc/fwupd/remotes.d/*.conf (LVFS UpdateURI) |
Detection is deliberately format-agnostic: any non-comment line carrying an
http:// URL is reported. A per-dialect key list would miss gpgkey=,
metalink=, and whatever the next release adds — and a miss is exactly the
failure this is meant to prevent. The one exception is JSON, which has no
comment syntax: there, http:// must sit at the start of a JSON string, so a
"_comment": "see http://docs.internal" note is not mistaken for a registry.
--apt-https-audit prints a per-store verdict:
━━━ App-store transport security (HTTPS) ━━━
Package manager: apt
[x] apt policy drop-in active: /etc/apt/apt.conf.d/99neohiro-force-https
Store Result
apt (Debian/Ubuntu/Mint/Pop!/Kali) https only
apk (Alpine) 2 plaintext endpoint(s)
/etc/apk/repositories:1: http://dl-cdn.alpinelinux.org/alpine/v3.19/main
pip index 1 plaintext endpoint(s)
/etc/pip.conf:2: index-url = http://pypi.internal/simple
Only apt is rewritten unattended, because it is the one store where the
result can be verified: after rewriting, a real apt-get update runs, and if
a mirror turns out not to serve the same paths over TLS the rewrite is rolled
back automatically. You are never left with a broken package manager.
-
Installs
/etc/apt/apt.conf.d/99neohiro-force-https:Acquire::https::AllowRedirect "true"; // https -> https redirects are fine Acquire::http::AllowRedirect "false"; // https -> http is REFUSED, not followed Acquire::https::Verify-Peer "true"; Acquire::https::Verify-Host "true"; Acquire::Retries "3";The anti-downgrade line is the important one: without it, an
https://mirror can silently bounce you down tohttp://. -
Rewrites
http://→https://on active lines in/etc/apt/sources.list,sources.list.d/*.list(classic) andsources.list.d/*.sources(DEB822URIs:field only). Commented-out lines and DEB822 structural fields (Suites:,Components:,Signed-By:) are left byte-identical. -
Backs every file up to
/var/backups/neohiro-apt-https/before the first edit and records each in the rollback log, sobash linuxinstall.sh --rollback --applycan undo it too. -
Applies each change with a staging file in the same directory followed by
rename(2), notcpinto place.cptruncates first, so a crash mid-copy leaves a truncatedsources.list; a rename is atomic, so a reader sees either the whole old file or the whole new one. Original mode and ownership are preserved. -
Warns if no CA trust store is present, since HTTPS verification is worthless without one.
For every other store the guard reports but does not rewrite. Whether
https://<same host><same path> actually exists cannot be known without a
network round trip, and silently breaking a working mirror is worse than the
threat it prevents. Two options:
# Repoint the endpoint at an https-capable mirror (recommended), or
NEOHIRO_APT_HTTPS_REWRITE=1 sudo bash linuxinstall.sh --apt-httpsThe opt-in sweeps every installed store (apk, pip, npm, cargo, gem, nix, docker, fwupd, and the RPM/Arch/SUSE repo formats) in one pass, backing each up and honouring the same atomic replace. Re-audit afterwards, because some mirrors genuinely do not serve the same paths over TLS. Stores whose tool is not installed are left alone.
NEOHIRO_APT_HTTPS_STRICT=1 turns any leftover plaintext endpoint into a
hard error, which is what you want in CI.
pkg_update / pkg_install / pkg_upgrade / pkg_autoremove,
update_system, update_kernel, updates_only_mode, restore_ssh.sh
(before installing openssh-server), DeepClean.sh (before
apt-get autoremove --purge), the Maintenance submenu option 1, the
--step apt_https mode, and the --apt-https* flags.
In lib/updater.sh it guards the _run_all_updates dispatcher plus every
sub-step that touches the network: _update_apt, _update_dnf,
_update_yum, _update_zypper, _update_pacman, _update_snap,
_update_flatpak, _update_docker, _update_brew, _update_firmware,
_update_geoip, _update_pihole. (_update_virsh, _update_suse_snapper
and _update_btrfs_balance only read or write local state, so they are
deliberately not guarded.)
Two subtleties worth knowing:
- Fetched subscripts get the guard too.
run_remote_scriptpullsDeepClean.sh/OptimizeLinuxASR.shinto a temp directory, and a script resolves its helpers relative to its own location — so without help it would find nolib/, and itsapt-get autoremove --purgewould run unguarded even though the parent had already enforced. The installer therefore prefetcheslib/apt-https.shnext to the subscript. If a subscript iscurl | bash'd entirely on its own and finds no library, it says so out loud rather than skipping the precaution silently. GEOIP_URLmust behttps://. It is the one operator-supplied download target in the update engine, and a GeoIP database decides which country a packet counts as being in — a tampered copy is a traffic-tunneling primitive. Anhttp://value is refused outright.
OptimizeLinuxASR.sh does not download packages, so it needs no guard.
curl ... | sudo bash has no lib/ directory next to it. Rather than carry
a second copy of this logic (which is how fixes silently fail to reach the
most common install path), the script resolves lib/apt-https.sh from disk
if present, otherwise fetches it from the same raw base it already trusts for
DeepClean.sh, and sources that. If neither is possible it says so loudly
and runs with the guard inactive — it never pretends to be enforcing.
| Variable | Effect |
|---|---|
NEOHIRO_APT_HTTPS=1 |
enforce (default) |
NEOHIRO_APT_HTTPS=audit |
report only, never modify |
NEOHIRO_APT_HTTPS=0 |
disable the guard entirely |
NEOHIRO_APT_HTTPS_REWRITE=1 |
also rewrite the non-apt stores |
NEOHIRO_APT_HTTPS_NOVERIFY=1 |
skip the post-rewrite apt-get update check |
NEOHIRO_APT_HTTPS_STRICT=1 |
treat leftover plaintext as a hard error |
NEOHIRO_APT_BLOCK_PORT80=1 |
also ufw deny out 80/tcp (opt-in, see below) |
NEOHIRO_APT_FAMILY=apt|dnf|yum|zypper|pacman|none |
pin the detected family (CI containers, testing) |
If you want belt-and-braces so that no process can open an unencrypted package connection, add an outbound deny rule:
sudo bash linuxinstall.sh --apt-https # make sure every repo is https first
sudo ufw deny out 80/tcp && sudo ufw reload
# or: NEOHIRO_APT_BLOCK_PORT80=1 sudo bash linuxinstall.sh --apt-httpsThis is opt-in because a blanket outbound block also breaks unrelated plaintext protocols (local registries, metrics endpoints, captive-portal checks). The guard refuses to add the rule while any plaintext repo is still configured, since that would only break those repos.
Verify:
sudo ufw status | grep -E '80/tcp|Status' # outbound DENY present
sudo bash linuxinstall.sh --apt-https-audit # exit 0 = no plaintext repos leftlinuxinstall.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.
This is the property that matters when something scary happens mid-run, so it is enforced rather than hoped for:
- The reattach command is printed before the wrap, not after. The script
execs into tmux, so anything printed afterwards would never be seen. You get the exact command — including a PID-suffixed name if the standard one is taken — on screen at the moment you need it. - A leftover session is never hijacked. If a
linux-setupsession already exists (a previous run that did not exit cleanly), the script says so and startslinux-setup-<pid>instead. You are never silently dropped into an old, differently-flagged run while yours never starts. - Your flags survive the wrap. The re-exec carries
--auto,--step,--dry-runand friends through, shell-quoted, so wrapping cannot change what the run does. - Sessions are reaped on success. A clean exit tears the session down, so the next run starts clean. A failed run leaves it alive — that is your inspection point.
- Script hops get their own session.
DeepClean.sh/OptimizeLinuxASR.shrun in a dedicatedneohiro-sub-<name>-<pid>tmux session with the reattach command printed, plus a heartbeat every 30s so a long step does not look frozen (a frozen screen invites a Ctrl-C that orphans the work). If there is no tmux to protect the hop, the script says plainly that the step cannot be recovered.
tmux ls lists sessions; tmux attach -t <name> reattaches; Ctrl-b then
d detaches without stopping anything.
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 | Force HTTPS for package repos | Enforces TLS for every repo before any download; prints the resulting state |
| 2–14 | system / dns / firewall / tor / ssh / fail2ban / unattended / ipv6 / sysctl / apparmor / pam / OptimizeLinuxASR / DeepClean | Re-run any step on demand |
| 15 | SSH diagnostics & lockout fix | Same routine as --restore-ssh |
| 16 | Authorized keys | List all keys in every user's authorized_keys |
| 17 | SSH config review | Print every key directive from sshd_config and drop-ins |
| 18 | SSH self-heal guard | Install / remove / status of the per-minute watchdog |
| 19 | Logs | Tail /var/log/linux-install-rollback.log and /var/log/neohiro-ssh-watchdog.log |
| 20 | System info | Uptime, load, memory, disk, CPU, listening ports |
| 21 | 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 statusThe sysctl step writes /etc/sysctl.d/99-hardening.conf and runs
sudo sysctl --system. The profile below is what the script actually emits, in
the same order. It is identical on every distro except for the two server-only
keys noted at the end.
# Kernel security hardening
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
kernel.panic=60
kernel.panic_on_oops=1
kernel.perf_event_paranoid=2
vm.mmap_rnd_bits=32
vm.mmap_rnd_compat_bits=16
# Network hardening - reverse path filtering
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
# SYN flood protection
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_max_syn_backlog=2048
net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_syn_retries=5
# Source route / redirects
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.secure_redirects=0
net.ipv4.conf.default.secure_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
# Martian logging & ICMP
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.default.log_martians=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
# Time-wait assassination protection
net.ipv4.tcp_rfc1337=1
# TCP tuning
net.ipv4.tcp_fin_timeout=15
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=3
net.ipv4.tcp_no_metrics_save=1
net.ipv4.tcp_moderate_rcvbuf=1
# ARP hardening
net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.all.proxy_arp=0
# Forwarding (disable if not a router)
net.ipv4.conf.all.forwarding=0
net.ipv4.conf.default.forwarding=0
# IPv6 hardening
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.default.accept_ra=0
net.ipv6.conf.all.autoconf=0
net.ipv6.conf.default.autoconf=0
# Congestion control & buffers
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
net.core.rmem_max=134217728
net.core.wmem_max=134217728
net.ipv4.tcp_rmem=4096 87380 134217728
net.ipv4.tcp_wmem=4096 65536 134217728
net.core.somaxconn=4096
Server-only (added on servers, skipped when a container runtime is detected):
user.max_user_namespaces=0
Apply manually:
sudo sysctl --systemNotes:
net.ipv4.conf.*.forwarding=0replaces the oldernet.ipv4.ip_forward=0; per-interface forwarding is what modern network namespaces and containers use.tcp_fin_timeout,tcp_tw_reuse, keepalive and buffer sizes are capacity tuning, not exploit mitigations. They are safe defaults, but on a busy host tune them to your link rather than treating them as security settings.net.ipv4.tcp_congestion_control=bbris ignored on kernels without BBR; the rest of the file still applies.
There are two attack-surface controls, and they do not overlap:
| Part | Entry point | Mechanism | Writes config? |
|---|---|---|---|
| Services | OptimizeLinuxASR.sh (profile 9, maintenance, run_remote_script) |
systemctl stop/disable/mask per daemon |
No |
| Kernel + config | attack_surface step (--step attack_surface) |
writes sysctl/modprobe/sshd/login/limits files | Yes |
OptimizeLinuxASR.sh removes running software (rpcbind, samba, ftp, telnet,
snapd, …) and touches no configuration file. The attack_surface step
hardens the kernel and configuration surface and touches no service. Run
both for full coverage; neither substitutes for the other.
The attack_surface step applies, all backed up and logged to the rollback log
first:
- Filesystem blacklist —
cramfs,freevxfs,jffs2,hfs,hfsplus,udf, plussquashfswhen no snap runtime is present. Written to/etc/modprobe.d/blacklist-filesystems.conf. - SSH crypto — restricts
Ciphers,MACsandKexAlgorithmsto modern AEAD/curve255 suites. Validates withsshd -t, backs up and restoressshd_config.ddrop-ins, then reloads (never restarts, so the session survives). /etc/login.defs—PASS_MAX_DAYS 90,PASS_MIN_DAYS 7,PASS_WARN_AGE 14. An existingENCRYPT_METHOD(yescrypt/bcrypt) is kept; it is never downgraded toSHA512.- Core dumps —
* hard core 0and* soft core 0in/etc/security/limits.conf.
Ordering note: the
squashfscarve-out keys off a snap runtime being present. If you disablesnapdviaOptimizeLinuxASR.shfirst, a laterattack_surfacerun will blacklistsquashfs. That is intended (no snap, no need for squashfs) but breaks live media and some container storage — review/etc/modprobe.d/blacklist-filesystems.confif you boot ISOs or use squashfs images.
| 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/run-all.sh # every suite, with a summary
bash tests/test_linuxinstall.sh # 67 tests: parse, logic, UX coverage, snapshot
bash tests/test_apt_https.sh # 275 tests: transport guard, run continuity, encoding
bash tests/test_updater.sh # 45 tests: dispatcher, race safety, version floor
shellcheck -S warning *.sh lib/*.sh tests/*.sh # linttest_apt_https.sh is hermetic: it relocates the whole /etc tree into a
temp sandbox, stubs sudo/ufw/apk on PATH, and never makes a network
call. It also enforces repository encoding hygiene (valid UTF-8, no
double-encoding artifacts, LF endings), because these scripts are full of
box-drawing characters and a silent re-encode is otherwise invisible until a
snapshot diff catches it.
To 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 listenersNew in this release: DeepClean.sh now enforces a consistent 1MB maximum log size across all logging subsystems, persistently and reboot-proof.
| Subsystem | Configuration | Drop-in file |
|---|---|---|
| systemd-journald | SystemMaxUse=1M, SystemMaxFileSize=1M, MaxRetentionSec=1day |
/etc/systemd/journald.conf.d/99-neohiro-1mb.conf |
| systemd-coredump | MaxUse=1M, ExternalSizeMax=1M |
/etc/systemd/coredump.conf.d/99-neohiro-1mb.conf |
| logrotate (global) | size 1M, rotate 1, daily, compress, delaycompress |
/etc/logrotate.conf (modified in-place) |
| logrotate (syslog) | /var/log/syslog, messages, auth.log, kern.log, daemon.log, user.log, ufw.log |
/etc/logrotate.d/99-neohiro-syslog |
| logrotate (package managers) | dpkg.log, apt/history.log, pacman.log, zypper.log, dnf.log, yum.log |
/etc/logrotate.d/99-neohiro-pkg |
| logrotate (fail2ban) | /var/log/fail2ban.log |
/etc/logrotate.d/99-neohiro-fail2ban |
| logrotate (SSH) | /var/log/sshd.log, ssh.log |
/etc/logrotate.d/99-neohiro-ssh |
| Docker | json-file driver, max-size=1m, max-file=1 |
/etc/docker/daemon.json |
| containerd / CRI-O | container_max_log_size=1048576, container_max_log_files=1 |
/etc/containerd/config.toml |
| kubelet | --container-log-max-size=1Mi --container-log-max-files=1 |
/etc/systemd/system/kubelet.service.d/99-neohiro-log-limit.conf |
| auditd | max_log_file=1, num_logs=2 |
/etc/audit/auditd.conf (modified in-place) |
| rsyslog rate limit | System: 200/5sec; imuxsock: 500/5sec | /etc/rsyslog.d/99-neohiro-rate-limit.conf |
| syslog-ng rate limit | log-fifo-size(1000), flush-lines(100) |
/etc/syslog-ng/conf.d/99-neohiro-rate-limit.conf |
- Prevents disk exhaustion from log floods (e.g., misbehaving services, DDoS, kernel oops storms)
- Consistent across distros — same limits on Ubuntu, RHEL, SUSE, Arch
- Reboot-proof — all limits use drop-in configs (
/etc/*/*.d/99-neohiro-*.conf) that package managers never overwrite - Auditable — run
DeepClean.shanytime to see before/after log footprint and re-apply
# Full deep clean + 1MB log limit enforcement
sudo ./DeepClean.sh
# Or via the main installer (option 12 in Maintenance menu)
curl -fsSL https://raw.githubusercontent.com/neohiro/linux/main/linuxinstall.sh | sudo bash
# → Select "Maintenance" → "12) DeepClean"
# Verify current log footprint
sudo journalctl --disk-usage
du -sh /var/logThe 1MB limits are applied automatically when:
- Running
DeepClean.sh(standalone or vialinuxinstall.shMaintenance menu) - Running
linuxinstall.shwith the Full profile (includes DeepClean) - Re-running on a live system — idempotent, safe to run daily via cron/timer
# Daily cron (recommended for servers)
0 3 * * * root /path/to/DeepClean.sh >/var/log/deepclean.log 2>&1
# Or systemd timer (included in the repo as an example)
# See tests/ for validation patterns- 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