Go to file
2026-07-28 07:23:10 +03:00
config/zsh zsh: add distro-guarded apt alias set (apt/apti/aptu/apts/aptsh) mirroring pacman 2026-07-28 04:50:08 +03:00
lib packages(arch): route byobu through yay (AUR-only); split official vs AUR lists so pacman --needed can't abort on a missing target; as_user prefers TARGET_USER for AUR builds 2026-07-28 05:44:45 +03:00
modules angie(debian): pin VERSION_ID in apt repo URL — needs angie/<ID>/<VERSION_ID>/ (debian/13/dists/trixie/) not angie/<ID>/ (debian/dists/trixie/ -> 404 'no Release file') on trixie; suite stays VERSION_CODENAME 2026-07-28 07:11:22 +03:00
.gitignore docs: README + gitignore 2026-07-28 04:32:38 +03:00
install.sh skeleton: launcher, entrypoint, distro/log/prompt libs 2026-07-28 04:32:38 +03:00
README.md docs: Angie built-in ACME (no certbot) section + --yes/--username notes 2026-07-28 05:21:23 +03:00
setup.sh create-user: passwordless sudo user (NOPASSWD, !tty_tickets, 7d cache) + retarget config; --create-user/--username flags 2026-07-28 05:05:03 +03:00
todo.md todo md 2026-07-28 07:23:10 +03:00

bootstrap

Turn a fresh Debian/Ubuntu/Arch VPS into a familiar workstation in one command. Detects the distro, installs a curated CLI package set, sets up zsh + oh-my-zsh with your ported config, brings your nvim config, hardens sane system defaults, and reports state.

Usage

curl -fsSL <shortener> | bash
# run a single step:
curl -fsSL <shortener> | bash -s -- --only packages
# run everything non-interactively and create a sudo user 'mk':
curl -fsSL <shortener> | bash -s -- --yes --username mk

The shortener points at the raw install.sh in this repo, which clones the full repo to ~/.bootstrap and runs setup.sh. To update: cd ~/.bootstrap && git pull (or just re-run the one-liner — it pulls existing clones).

Re-run individual steps locally after the initial bootstrap:

bash ~/.bootstrap/setup.sh --only shell-zsh
bash ~/.bootstrap/setup.sh --only hardening

Steps

# step what it does
0 preflight verify distro + network + privileges
1 create-user (when direct root) create a passwordless sudo user, copy root's authorized_keys, retarget config at them
2 packages git, byobu, ufw, mosh, btop, eza, fzf, ripgrep, bat, zoxide, jq, fd, tree, ncdu, lazygit, starship, … (+ yay on Arch)
3 docker Docker engine + compose v2 plugin, $TARGET_USER in docker group
4 angie Angie (nginx fork) web server
5 shell-zsh zsh + oh-my-zsh + plugins, set default shell, ship config (to ~$TARGET_USER/.config/zsh)
6 nvm nvm → ~$TARGET_USER/.nvm + Node LTS
7 nvim clone g.mk.fo/bootstrap/nvim-minimal~$TARGET_USER/.config/nvim
8 hardening ssh (key-only root), ufw, updates, unattended-upgrades, tz, hostname, swapfile, locale, fail2ban, angie config layout + built-in ACME
9 sanity read-only report + recommended manual follow-ups

Every step is idempotent — re-running is safe.

Sudo user (when running as direct root)

Running the one-liner as direct root (no SUDO_USER) is the common provider-provisioned-VPS case. The create-user step then:

  • Arch: auto-creates a non-root sudoer (AUR/makepkg can't run as root), with a y/N confirm on the username (default mk).
  • Debian/Ubuntu: asks y/N whether to create one (optional — apt works as root).
  • Always when sudoed-root (SUDO_USER set): uses that invoking user as the target, syncing their sudoers + ssh keys instead of creating a new account.
  • Never when not root: you're already a normal user; yourself is the target.

Flags:

  • --create-user force the prompt regardless of distro,
  • --username NAME use that name and skip the name prompt.

The created user:

  • is added to wheel (Arch) or sudo+adm (Debian), shell set to zsh,
  • has no password (passwd -d) — SSH key login only (you'll disable ssh password auth in the hardening step),
  • gets /root/.ssh/authorized_keys copied to ~user/.ssh/ (chmod 600/700), with the fingerprints printed so you can sanity-check they're yours,
  • gets /etc/sudoers.d/<user>: Defaults:<user> !tty_tickets, Defaults:<user> timestamp_timeout=10080 (7-day cache), and <user> ALL=(ALL) NOPASSWD:ALL. Validated with visudo -c before install.

All later config steps (zsh/omz, nvim, nvm, docker group) target ~$TARGET_USER. After bootstrap, test ssh <user>@<host> from a new terminal before closing the root session.

Zsh plugins enabled (oh-my-zsh)

git sudo command-not-found colored-man-pages extract copypath dirhistory plus, cloned into $ZSH_CUSTOM: zsh-autosuggestions, zsh-syntax-highlighting (loaded last), zsh-completions, history-substring-search, fzf-tab. Prompt: starship.

Config layout (shipped to ~/.config/zsh)

config/zsh/
  zshrc              # main config (sources the pieces below)
  path.zsh           # PATH + env (XDG, EDITOR, LANG, nvm, pnpm)
  aliases.zsh        # ported from fish + curated defaults
  aliases-extras.zsh # your personal additions (preserved across re-runs)
  functions.zsh      # mcd, conf, so, ssh-wrapper, dot-expansion
  plugins.zsh        # omz plugin list + load oh-my-zsh

~/.zshrc is a tiny shim that sets ZDOTDIR=~/.config/zsh and sources the real config there — keeps omz "happy" while your config lives under XDG.

Hardening choices

  • SSH: PermitRootLogin prohibit-password + PasswordAuthentication no. Root key login stays available; password login is disabled. A backup is made and sshd -t runs before reload. Always keep your current session open and test a new login before closing it.
  • ufw: deny incoming, allow 22/tcp, 60000:61000/udp (mosh), 80,443/tcp (angie).
  • updates + unattended-upgrades (Debian), system upgrade on Arch.
  • timezone (default UTC), hostname (optional).
  • swapfile 2G if RAM < 2G and no swap present.
  • locale en_US.UTF-8.
  • fail2ban with an sshd jail.
  • angie config — see Angie config layout below.

Supported hosts

  • Debian / Ubuntu (via apt)
  • Arch (via pacman + yay for AUR)
  • Architectures: amd64, arm64

Angie config layout

The hardening step sets up a sane /etc/angie/ structure (Angie is an nginx fork; same config syntax):

/etc/angie/
  angie.conf          # managed by bootstrap (written once; not overwritten on re-runs)
  mime.types          # shipped by the package — left alone
  _on/*.conf          # enabled hosts (sites-enabled alt.)   ← included from http{}
  targets/*.conf      # all host definitions (sites-available alt.)
  modules/*.conf       # top-level context snippets (stream{}, env, load_module, ...)
  modules/http/*.conf  # http{}-level snippets (upstreams, maps, real_ip, proxy defaults)

Convention: write each host in targets/<name>.conf, then enable it with angie-enable <name> (symlinks targets/<name>.conf_on/<name>.conf, validates, reloads). Disable with angie-disable <name>. Both helpers are installed to /usr/local/bin/.

The root angie.conf ships: worker_processes auto, server_tokens off, gzip, client_max_body_size 16m, a main log_format, and includes modules/http/*.conf then _on/*.conf. user/pid/error_log/load_module directives are extracted from the original packaged angie.conf and preserved (so Debian's user angie vs Arch's user http is carried over). A backup is kept at /etc/angie/angie.conf.orig.bootstrap.

Defaults shipped:

  • modules/http/00-common.confreal_ip from private ranges (Cloudflare commented), proxy_set_header defaults (inherited by every server/location).
  • _on/00-default.confdefault_server returning 444 for requests that match no enabled host (drops drive-by scanners / bare-IP probes).
  • targets/example-https.conf.example — a TLS server template using Angie's built-in ACME.
  • angie-issue helper — create a TLS host with a Let's Encrypt cert (no certbot).

TLS / Let's Encrypt (Angie built-in ACME — no certbot)

Angie ships the http_acme module, so it obtains and renews TLS certificates itself — no certbot, no separate renewal cron/timer. The hardening step ships an example TLS host and an angie-issue helper:

angie-issue example.com you@email.com   # first call: writes the shared
                                        # acme_client (Let's Encrypt) + email
angie-issue www.example.com             # later domains reuse the same client

All enabled hosts using acme default share one certificate covering every server_name that references it; Angie requests it on reload and renews before expiry. The shared client lives at /etc/angie/modules/http/acme.conf (with a resolver, required by acme_client). HTTP→HTTPS redirect, http2, and the cert are wired into the target template — replace location / with your app/proxy.

Notes

  • Distro-guarded package-manager aliases ship in aliases.zsh: pac/pacs/pacu/pacy/pa (Arch, sudo pacman …) and apt/apti/aptu/apts/aptsh (Debian/Ubuntu, sudo apt …).
  • aliases-extras.zsh is seeded once and then left alone — re-running the shell-zsh step won't clobber your personal aliases.
  • On Debian, fd/bat ship as fdfind/batcat; symlinks to the canonical names are created in ~/.local/bin (which is on PATH via path.zsh).
  • Some Go/Rust CLIs (dust, duf, procs, lazygit) are fetched from the latest GitHub release tarball when the distro package is missing; this is best-effort and warned (not fatal) on failure.