179 lines
8.7 KiB
Markdown
179 lines
8.7 KiB
Markdown
# 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 <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:
|
|
|
|
```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/<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](#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.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.
|