# 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 ```sh curl -fsSL | bash # run a single step: curl -fsSL | bash -s -- --only packages # run everything non-interactively and create a sudo user 'mk': curl -fsSL | 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: ```sh 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/`: `Defaults: !tty_tickets`, `Defaults: timestamp_timeout=10080` (7-day cache), and ` 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 @` 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](#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/.conf`, then enable it with `angie-enable ` (symlinks `targets/.conf` → `_on/.conf`, validates, reloads). Disable with `angie-disable `. 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.conf` — `real_ip` from private ranges (Cloudflare commented), `proxy_set_header` defaults (inherited by every server/location). - `_on/00-default.conf` — `default_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: ```sh 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.