Compare commits

...
22 Commits
Author SHA1 Message Date
mk ce5041bc50 todo md 2026-07-28 07:23:10 +03:00
mk 9f164f61ef 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
packages(debian): rm stale /etc/apt/sources.list.d/eza.list + gierens.gpg left by the pre-fix run that used the now-dead apt.fury.io PPA — otherwise 'apt-get update' keeps hitting the 401 forever even though we now install eza from GH releases
2026-07-28 07:11:22 +03:00
mk 0cfe10e387 packages(debian): fix eza+starship+zoxide installs
- eza: apt.fury.io PPA is dead (401 Unauthorized); install the single binary from the upstream GitHub release tarball (eza_x86_64-unknown-linux-gnu.tar.gz) into /usr/local/bin — no apt repo, system-wide
- starship: scrub $ARCH/$PLATFORM from the installer env — our lib/detect.sh exports ARCH=amd64, which the starship installer reuses (it checks ${ARCH-}) and skips its own amd64->x86_64 mapping, producing an unsupported 'amd64-unknown-linux-musl' target. Default bin dir is /usr/local/bin anyway
- zoxide: install system-wide (--bin-dir /usr/local/bin) instead of ~/.local/bin — when run as root the default landed in /root (wrong user for the create-user path) and wasn't on $PATH
2026-07-28 07:08:04 +03:00
mk efdf056352 hardening(angie): default_server needs a cert on its 443 listener or 'angie -t' fails with 'no ssl_certificate defined' — generate a one-time self-signed dummy (CN=bootstrap-default, 10y) in /etc/angie/ssl so unknown-SNI clients handshake then get 444; mark listen 443 as ssl explicitly 2026-07-28 06:55:31 +03:00
mk 435d66aad2 shell-zsh: derive ZSH/ZDOTDIR from $TARGET_HOME unconditionally — ignore inherited $ZSH env var (omz exports it, which pinned paths to the old home e.g. /root while the chown step retargeted to the new user, causing 'insecure completion dirs' owned by the wrong user) 2026-07-28 06:16:45 +03:00
mk 94a0bac9f4 hardening(angie): resolve mime.types from whichever path the package ships (angie-bin=/etc/angie, Arch source=/etc/nginx via mailcap dep) — fixes 'angie -t' 'open() mime.types failed' on source path; fallback ships a minimal mime.types; ensure modules/ include dirs exist before glob 2026-07-28 06:04:57 +03:00
mk 26185978ac hardening(angie): source-angie bridge — symlink /etc/nginx/nginx.conf -> /etc/angie/angie.conf so the _on/targets layout actually loads under the source AUR package's '/usr/bin/nginx' service (angie-bin/Debian already read /etc/angie directly); backup original once 2026-07-28 05:59:08 +03:00
mk cb0e517a7d hardening(angie): fix 'angie_user: unbound variable' under set -u — init extraction locals to empty; also read source AUR package's /etc/nginx/nginx.conf as alt stock config path 2026-07-28 05:57:44 +03:00
mk b653191de2 angie(arch): prefer angie-bin (repackaged upstream .deb, instant) over source build; verified http_acme/ssl/v2/v3/realip all compiled in; fall back to aur/angie if bin unavailable 2026-07-28 05:53:43 +03:00
mk ca8f01083c 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
mk ff9fc4db4e fix(sanity): hostname missing on minimal Arch (not in coreutils) — use uname -n with fallbacks; refresh TLS follow-up to angie-issue 2026-07-28 05:35:40 +03:00
mk f7e4bb96d7 fix(zsh plugins): correct clone orgs — zsh-users/zsh-history-substring-search, Aloxaf/fzf-tab (were typos causing github auth prompts) 2026-07-28 05:28:20 +03:00
mk 146c79a57e docs: Angie built-in ACME (no certbot) section + --yes/--username notes 2026-07-28 05:21:23 +03:00
mk 8930e7aa76 non-interactive --yes: ask() assumes prompt default (no read); timezone defaults UTC; --username implies force-create; create-user name defaults to mk under --yes 2026-07-28 05:21:23 +03:00
mk 34137d185e docs: document sudo-user creation + retarget + new flags 2026-07-28 05:05:03 +03:00
mk e7a9349292 modules: target $TARGET_USER (docker group, zsh/nvim/nvm homes, chown tree) 2026-07-28 05:05:03 +03:00
mk 8a516f039c create-user: passwordless sudo user (NOPASSWD, !tty_tickets, 7d cache) + retarget config; --create-user/--username flags 2026-07-28 05:05:03 +03:00
mk a9fff32255 docs: document apt/pac aliases + Angie config layout 2026-07-28 04:50:08 +03:00
mk 0116ac60b3 hardening: angie config layout (_on/targets/modules) + sane root config + enable/disable helpers 2026-07-28 04:50:08 +03:00
mk 247d33ae8a zsh: add distro-guarded apt alias set (apt/apti/aptu/apts/aptsh) mirroring pacman 2026-07-28 04:50:08 +03:00
mk 3b25d8d4a4 docs: README + gitignore 2026-07-28 04:32:38 +03:00
mk 8bb8a44dbd zsh config: zshrc, path, aliases (ported from fish), extras, functions, plugins 2026-07-28 04:32:38 +03:00
21 changed files with 1077 additions and 80 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
*.bak.*
+178
View File
@@ -0,0 +1,178 @@
# 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.
+11
View File
@@ -0,0 +1,11 @@
# aliases-extras.zsh — YOUR personal additions go here.
# This file is preserved across re-runs of the shell-zsh step (not overwritten
# from the repo). Add your taskwarrior / neomutt / cargo / eso aliases here.
# Examples (uncomment + edit):
# alias tw='taskwarrior-tui'
# alias ta='task add'
# alias tl='task list'
# alias neomutt='neomutt -f ~/.local/share/mail/you/All\ Mail/'
# alias carwash="cargo-watch -x run"
# alias wttr="curl v2.wttr.in/59.95,30.35?FQM" # your city coords
+76
View File
@@ -0,0 +1,76 @@
# aliases.zsh — ported from ~/.config/fish/config.fish (server-relevant subset)
# Plus curated defaults. Edit aliases-extras.zsh for personal additions.
# ── editor ──────────────────────────────────────────────────────────────────
alias v="$EDITOR"
alias vi="$EDITOR"
alias vim="$EDITOR"
# ── ls / eza ───────────────────────────────────────────────────────────────
alias lso="/bin/ls" # plain ls escape hatch
if command -v eza >/dev/null 2>&1; then
alias ls="eza -l --group-directories-first --icons"
alias ll="eza -la --group-directories-first --icons"
alias la="eza -a --group-directories-first --icons"
alias lt="eza --tree --level=2 --icons"
else
alias ls="ls --color=auto -l"
alias ll="ls -la --color=auto"
alias la="ls -a --color=auto"
fi
# ── cat / bat ───────────────────────────────────────────────────────────────
if command -v bat >/dev/null 2>&1; then
alias cat="bat --paging=never"
fi
alias catn="/bin/cat"
# ── file ops (verbose + interactive safety) ────────────────────────────────
alias cp="cp -iv"
alias mv="mv -iv"
alias rm="rm -iv"
alias ln="ln -sv"
alias mkdir="mkdir -pv"
alias md="mkdir"
# ── git ─────────────────────────────────────────────────────────────────────
alias g="git"
alias gcl="git clone"
alias gst="git status"
alias gd="git diff"
alias gco="git checkout"
# ── systemd ──────────────────────────────────────────────────────────────────
alias sy="sudo systemctl"
alias srv="sudo systemctl"
# ── sudo / misc ──────────────────────────────────────────────────────────────
alias sudoo="sudo -E -s"
alias chmox="chmod +x"
# ── network ─────────────────────────────────────────────────────────────────
alias whoamip="curl --silent https://ipinfo.io"
alias wttr="curl v2.wttr.in"
alias wtr="wttr"
# ── byobu ────────────────────────────────────────────────────────────────────
alias byb="byobu"
alias bybou="byobu"
# ── pacman (Arch only) ──────────────────────────────────────────────────────
if command -v pacman >/dev/null 2>&1; then
alias pac="sudo pacman"
alias pacs="sudo pacman -Sy"
alias pacu="sudo pacman -Syyu"
alias pacy="sudo pacman -Sy"
alias pa="sudo pacman"
fi
# ── apt (Debian/Ubuntu only) ───────────────────────────────────────────────
if command -v apt-get >/dev/null 2>&1; then
alias apt="sudo apt"
alias apti="sudo apt install"
alias aptu="sudo apt update && sudo apt upgrade"
alias apts="sudo apt search"
alias aptsh="sudo apt show"
fi
+28
View File
@@ -0,0 +1,28 @@
# functions.zsh — ported from fish functions (server-relevant subset)
# make dir + cd into it
mcd() { mkdir -p "$1" && cd "$1"; }
# reload zsh config
so() { source "${ZDOTDIR:-$HOME/.config/zsh}/zshrc" 2>/dev/null || source "$HOME/.zshrc"; }
# cd into ~/.config, edit/open the named file/dir (interactive)
conf() {
local base="${XDG_CONFIG_HOME:-$HOME/.config}"
cd "$base" || return
if [[ -f "$1" ]]; then
echo "Editing $1"; $EDITOR "$1"
elif [[ -d "$1" ]]; then
echo "Opening config dir $1"; cd "$1" && ls -al
else
read "confirm?Can't find $1, open in \$EDITOR? (Y/n) "
if [[ -z "$confirm" || "$confirm" =~ ^[Yy]$ ]]; then
$EDITOR "$1"
else
echo "Exiting…"
fi
fi
}
# ssh onward with a sane TERM (avoids terminfo breakage on older boxes)
ssh() { TERM=xterm command ssh "$@"; }
+35
View File
@@ -0,0 +1,35 @@
# path.zsh — PATH + environment (ported from ~/.config/fish/config.fish, server subset)
# XDG
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
export XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
export EDITOR="nvim"
export LANG="en_US.UTF-8"
export LC_COLLATE="C"
export PAGER="less"
export LESS="-R" # render ANSI colors in less
export BAT_THEME="base16"
export LESSHISTFILE="$XDG_STATE_HOME/less/history"
[[ -d "$XDG_STATE_HOME/less" ]] || mkdir -p "$XDG_STATE_HOME/less"
# user-local binaries (fd/bat shims, gh-release binaries, pip installs, ...)
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac
case ":$PATH:" in
*":/usr/local/bin:"*) ;;
*) export PATH="/usr/local/bin:$PATH" ;;
esac
# nvm (installed by the nvm step) — loaded lazily via its init snippet below
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
[[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh"
# pnpm (if present)
if [[ -d "$HOME/.local/share/pnpm" ]] && ! [[ ":$PATH:" == *":$HOME/.local/share/pnpm:"* ]]; then
export PATH="$HOME/.local/share/pnpm:$PATH"
fi
+23
View File
@@ -0,0 +1,23 @@
# plugins.zsh — enable oh-my-zsh builtins + external plugins (cloned by the
# shell-zsh module into $ZSH_CUSTOM/plugins).
plugins=(
git
sudo
command-not-found
colored-man-pages
extract
copypath
dirhistory
zsh-autosuggestions
zsh-syntax-highlighting # must be last among syntax plugins
history-substring-search
fzf-tab
zsh-completions
)
source "$ZSH/oh-my-zsh.sh"
# history-substring-search keybindings (after omz is loaded)
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
+59
View File
@@ -0,0 +1,59 @@
# ~/.zshrc shim (managed by bootstrap) sources this file via ZDOTDIR.
# Edit freely; re-run the shell-zsh step to re-sync from the repo if needed.
# ── oh-my-zsh bootstrap ────────────────────────────────────────────────────
export ZSH="${ZSH:-$HOME/.oh-my-zsh}"
export ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
# theme disabled — we use starship (init at the bottom)
ZSH_THEME=""
# casewise history
HISTFILE="$HOME/.local/state/zsh/history"
HISTSIZE=100000
SAVEHIST=100000
setopt EXTENDED_HISTORY SHARE_HISTORY HIST_IGNORE_DUPS HIST_IGNORE_SPACE HIST_VERIFY
setopt AUTO_CD PUSHD_IGNORE_DUPS PROMPT_SUBST
setopt INTERACTIVE_COMMENTS
# load the modular pieces (order matters)
[[ -f "$ZDOTDIR/path.zsh" ]] && source "$ZDOTDIR/path.zsh"
[[ -f "$ZDOTDIR/aliases.zsh" ]] && source "$ZDOTDIR/aliases.zsh"
[[ -f "$ZDOTDIR/aliases-extras.zsh" ]] && source "$ZDOTDIR/aliases-extras.zsh"
[[ -f "$ZDOTDIR/functions.zsh" ]] && source "$ZDOTDIR/functions.zsh"
# ── completion styling (set before omz runs compinit) ───────────────
autoload -Uz compinit
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'
zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS}
# oh-my-zsh + plugins (omz runs compinit internally; lists our plugins)
[[ -f "$ZDOTDIR/plugins.zsh" ]] && source "$ZDOTDIR/plugins.zsh"
# ── standard zsh niceties ─────────────────────────────────────────────
autoload -U edit-command-line
zle -N edit-command-line
bindkey '^X^E' edit-command-line
# ... -> ../.. expansion (ported from fish)
rationalise-dot() {
if [[ $LBUFFER = *.. ]]; then LBUFFER+=/; else LBUFFER+=.; fi
}
zle -N rationalise-dot
bindkey . rationalise-dot
# ── keychain (commented) — fill in the key labels you use on this box ────
# if command -v keychain >/dev/null 2>&1; then
# keychain --quiet --nogui id_ed25519 # <- your server-relevant key(s) here
# [[ -f "$HOME/.keychain/$(hostname)-sh" ]] && source "$HOME/.keychain/$(hostname)-sh"
# fi
# ── integrations ────────────────────────────────────────────────────────────
command -v zoxide >/dev/null 2>&1 && eval "$(zoxide init zsh)"
command -v fzf >/dev/null 2>&1 && [[ -f "$HOME/.fzf.zsh" ]] && source "$HOME/.fzf.zsh"
# ── starship prompt ─────────────────────────────────────────────────────────
if command -v starship >/dev/null 2>&1; then
eval "$(starship init zsh)"
fi
+5 -4
View File
@@ -9,12 +9,13 @@ ok() { printf '\033[1;32mok\033[0m %s\n' "$*"; }
# run a command as root when not already root # run a command as root when not already root
sudo_() { if (( EUID == 0 )); then "$@"; else sudo "$@"; fi; } sudo_() { if (( EUID == 0 )); then "$@"; else sudo "$@"; fi; }
# run a command AS the invoking (non-root) user — used for AUR builds, etc. # run a command AS the target non-root user — used for AUR builds, nvm, etc.
# when root: drop to SUDO_USER (or USER) via runuser; when not root: run directly. # precedence (when running as root): TARGET_USER (set by create-user) > SUDO_USER > USER.
# when not root: run directly as ourselves.
as_user() { as_user() {
local u="$USER" local u="${SUDO_USER:-$USER}"
if (( EUID == 0 )); then if (( EUID == 0 )); then
u="${SUDO_USER:-$USER}" u="${TARGET_USER:-${SUDO_USER:-$USER}}"
command -v runuser >/dev/null 2>&1 && runuser -u "$u" -- "$@" || su "$u" -c "$*" command -v runuser >/dev/null 2>&1 && runuser -u "$u" -- "$@" || su "$u" -c "$*"
else else
"$@" "$@"
+6
View File
@@ -2,9 +2,15 @@
# prompt.sh — plain y/N prompts. # prompt.sh — plain y/N prompts.
# ask "prompt?" [default y|n] -> sets REPLY # ask "prompt?" [default y|n] -> sets REPLY
# Under ${ALL:-0} (the --yes flag) assume the stated default without
# reading stdin, so a fully non-interactive run never blocks on a read.
ask() { ask() {
local p="$1" d="${2:-y}" hint local p="$1" d="${2:-y}" hint
if [[ "$d" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi if [[ "$d" == "y" ]]; then hint="[Y/n]"; else hint="[y/N]"; fi
if (( ${ALL:-0} )); then
REPLY="$d"
return
fi
read -rp "$p $hint " REPLY read -rp "$p $hint " REPLY
[[ -z "$REPLY" ]] && REPLY="$d" [[ -z "$REPLY" ]] && REPLY="$d"
} }
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env bash
# 05-create-user — create a daily-driver sudo user when running as direct root.
#
# Why this exists: `makepkg` (yay / AUR) refuses to run as root. On a fresh VPS
# that lands you in as root with no SUDO_USER, we create a non-root sudoer,
# copy root's authorized_keys to it, configure passwordless sudo with
# !tty_tickets + a long timestamp_timeout, and retarget the config steps
# (zsh, nvim, nvm, docker group) at that user's $HOME.
#
# Behaviour:
# - Not root -> skip (you're already a normal user).
# - Root + SUDO_USER set -> skip (you sudoed in; that user is the target).
# - Root + no SUDO_USER + Arch -> auto-create (AUR needs it), confirm name.
# - Root + no SUDO_USER + Debian/Ubuntu -> ask y/N (optional, apt works as root).
# - --create-user flag -> force prompt regardless.
# - --username NAME -> override default name, no prompt.
# - --yes (no --username) -> default to 'mk' silently (no stall).
#
# Passwordless, no user password (NOPASSWD). Idempotent: re-runs re-sync
# authorized_keys + sudoers rather than failing.
step_name="create-user"
step_desc="create a sudo user (passwordless) + copy root's ssh keys + retarget config"
step_run="run_create_user"
run_create_user() {
log "create-user step"
# not root -> nothing to do, target stays as current user
if (( EUID != 0 )); then
echo " not root — target stays $USER ($HOME)"
export TARGET_USER="$USER" TARGET_HOME="$HOME"
return
fi
# root but came in via sudo -> use that invoking user, don't create a new one
if [[ -z "${SUDO_USER:-}" ]]; then _handle_direct_root; else _handle_sudoed_root; fi
}
# ── direct root (logged in as root; the case that actually needs a new user) ─
_handle_direct_root() {
local should_create=0
if (( CREATE_USER )) || [[ -n "${USERNAME_ARG:-}" ]]; then
[[ -n "${USERNAME_ARG:-}" ]] && log "--username given: forcing user creation"
should_create=1
elif [[ "$DISTRO" == "arch" ]]; then
# AUR/makepkg cannot run as root — we genuinely need a non-root sudoer.
echo " running as direct root on Arch: a non-root sudoer is needed for yay/AUR."
yn "Create a sudo user now? (recommended)" y && should_create=1
else
# Debian/Ubuntu: apt works as root, so this is convenience (key-first login).
yn "Create a daily-driver sudo user? (optional — apt works as root)" n && should_create=1
fi
if (( ! should_create )); then
echo " skipping user creation — config will target root ($HOME)"
warn "on Arch you won't be able to build AUR packages (yay/angie) as root"
export TARGET_USER="root" TARGET_HOME="$HOME"
return
fi
local user="${USERNAME_ARG:-}"
if [[ -z "$user" ]]; then
local default="mk"
read -rp " username [$default]: " user
[[ -z "$user" ]] && user="$default"
fi
_create_or_sync_user "$user"
}
# ── root via sudo: use the invoking user, don't create another ───────────────
_handle_sudoed_root() {
local user="$SUDO_USER"
local home; home="$(getent passwd "$user" | cut -d: -f6)"
log "running as root via sudo — using invoking user '$user' as target"
# ensure they have passwordless sudo + the ssh key + sudo group, then retarget
_create_or_sync_user "$user" skip_create
export TARGET_USER="$user" TARGET_HOME="$home"
}
# ── the meat: create if missing, sync sudoers, sync authorized_keys ──────────
_create_or_sync_user() { # username [skip_create]
local user="$1"
local skip_create="${2:-}"
if [[ "$skip_create" != "skip_create" ]]; then
if id "$user" >/dev/null 2>&1; then
ok "user '$user' already exists — syncing config only"
else
local shell_bin; shell_bin="$(command -v zsh 2>/dev/null || echo /bin/bash)"
local groups
case "$DISTRO" in
arch) groups="wheel" ;;
debian) groups="sudo,adm" ;;
esac
log "creating user '$user' (shell=$shell_bin, groups=$groups)"
useradd -m -s "$shell_bin" -G "$groups" "$user"
# passwordless account: clear the password field entirely
passwd -d "$user" >/dev/null 2>&1 || true
# unlock the account so SSH key login still works after passwd -d
usermod -p "" "$user" 2>/dev/null || true
ok "user '$user' created (no password set; ssh-key login only)"
fi
fi
local home; home="$(getent passwd "$user" | cut -d: -f6)"
_sync_authorized_keys "$user" "$home"
_install_sudoers "$user"
export TARGET_USER="$user" TARGET_HOME="$home"
ok "config target retargeted: user=$user home=$home"
}
# ── copy root's authorized_keys to the target user (idempotent) ──────────────
_sync_authorized_keys() { # user home
local user="$1" home="$2"
local src="/root/.ssh/authorized_keys"
if [[ ! -f "$src" ]]; then
warn "no /root/.ssh/authorized_keys — add your key manually:"
echo " install -d -m 700 -o $user -g $user $home/.ssh"
echo " # then: paste your pubkey into $home/.ssh/authorized_keys (chmod 600)"
return
fi
install -d -m 700 -o "$user" -g "$user" "$home/.ssh"
install -m 600 -o "$user" -g "$user" "$src" "$home/.ssh/authorized_keys"
local n; n=$(grep -cE '^[^#]' "$src" 2>/dev/null || echo 0)
ok "copied $n authorized key(s) to $home/.ssh/authorized_keys"
warn "sanity-check these are YOUR keys (fingerprints):"
ssh-keygen -lf "$src" 2>/dev/null | sed 's/^/ /' || true
}
# ── write /etc/sudoers.d/<user>: NOPASSWD, !tty_tickets, 7-day cache ─────────
_install_sudoers() { # user
local user="$1" file="/etc/sudoers.d/${user}"
log "configuring sudoers for '$user' (passwordless, !tty_tickets, 7d timeout)"
# write to a temp, validate with visudo -c, then move into place atomically
local tmp; tmp="$(mktemp)"
cat > "$tmp" <<EOF
# managed by bootstrap — passwordless sudo for $user
Defaults:$user !tty_tickets
Defaults:$user timestamp_timeout=10080
$user ALL=(ALL) NOPASSWD:ALL
EOF
# visudo must see proper perms (0440) to be happy
chmod 0440 "$tmp"
if visudo -cf "$tmp" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$tmp" "$file"
ok "sudoers installed: $file"
else
err "visudo -c rejected the sudoers snippet — NOT installed, inspect aborted"
cat "$tmp" | sed 's/^/ /'
fi
rm -f "$tmp"
}
+62 -19
View File
@@ -20,32 +20,54 @@ run_packages() {
} }
# ── arch ───────────────────────────────────────────────────────────────────── # ── arch ─────────────────────────────────────────────────────────────────────
_pkg_arch() { # official-repo packages (verified in extra/core). Any name not in the official
sudo_ pacman -Sy --noconfirm --needed base-devel openssh # repos would abort the whole `pacman --needed` batch, so keep this list clean
# most tools live in extra/community (the merged repos); names map 1:1 # and route AUR-only tools through _install_aur_packages instead.
local p=(git byobu ufw mosh btop fzf ripgrep fd bat dust procs jq yq tree ARCH_OFFICIAL=(git ufw mosh btop fzf ripgrep fd bat dust procs jq yq tree
ncdu duf tldr httpie rsync lazygit neovim curl unzip less starship ncdu duf tldr httpie rsync lazygit neovim curl unzip less starship
eza zoxide openssh) eza zoxide openssh)
sudo_ pacman -Sy --noconfirm --needed "${p[@]}"
_install_yay
# AUR-only packages (not in official repos) — installed through yay after build.
ARCH_AUR=(byobu)
_pkg_arch() {
sudo_ pacman -Sy --noconfirm --needed base-devel openssh
sudo_ pacman -Sy --noconfirm --needed "${ARCH_OFFICIAL[@]}"
_install_yay
_install_aur_packages
# fd/bat ship with their real names on arch — no shims needed # fd/bat ship with their real names on arch — no shims needed
} }
_install_yay() { _install_yay() {
if command -v yay >/dev/null 2>&1; then ok "yay already installed"; return; fi if command -v yay >/dev/null 2>&1; then ok "yay already installed"; return; fi
log "building yay from AUR (requires a non-root sudoer)" log "building yay from AUR (requires a non-root sudoer)"
if (( EUID == 0 )) && [[ -z "${SUDO_USER:-}" ]]; then if (( EUID == 0 )) && [[ -z "${TARGET_USER:-${SUDO_USER:-}}" ]]; then
err "running as root with no SUDO_USER — yay cannot build. Re-run bootstrap as a normal user with sudo." err "running as root with no target/sudo user — yay cannot build. Run the create-user step first, or re-run bootstrap as a normal user with sudo."
return 1 return 1
fi fi
local tmp; tmp="$(mktemp -d)" local tmp; tmp="$(mktemp -d)"
# chown the build dir so the non-root build user can write into it
if (( EUID == 0 )); then
chown -R "${TARGET_USER:-${SUDO_USER}}" "$tmp"
fi
as_user git clone --depth 1 https://aur.archlinux.org/yay.git "$tmp/yay" as_user git clone --depth 1 https://aur.archlinux.org/yay.git "$tmp/yay"
( cd "$tmp/yay" && as_user makepkg -si --noconfirm ) ( cd "$tmp/yay" && as_user makepkg -si --noconfirm )
rm -rf "$tmp" rm -rf "$tmp"
command -v yay >/dev/null 2>&1 && ok "yay installed" || warn "yay install may need a manual pass" command -v yay >/dev/null 2>&1 && ok "yay installed" || warn "yay install may need a manual pass"
} }
_install_aur_packages() {
[[ "${#ARCH_AUR[@]}" -eq 0 ]] && return
if ! command -v yay >/dev/null 2>&1; then
warn "yay not available — skipping AUR packages: ${ARCH_AUR[*]}"
return
fi
log "installing AUR-only packages: ${ARCH_AUR[*]}"
# yay -S as the non-root target user (makepkg refuses root; TARGET_USER via as_user).
as_user yay -S --noconfirm --needed "${ARCH_AUR[@]}" \
|| warn "some AUR packages failed: ${ARCH_AUR[*]} (install manually with: yay -S ${ARCH_AUR[*]})"
}
# ── debian/ubuntu ──────────────────────────────────────────────────────────── # ── debian/ubuntu ────────────────────────────────────────────────────────────
_pkg_debian() { _pkg_debian() {
sudo_ apt-get update -y sudo_ apt-get update -y
@@ -56,24 +78,45 @@ _pkg_debian() {
# rename packages: fd -> fdfind, bat -> batcat (fix via aliases in zsh) # rename packages: fd -> fdfind, bat -> batcat (fix via aliases in zsh)
sudo_ apt-get install -y fdfind batcat 2>/dev/null || true sudo_ apt-get install -y fdfind batcat 2>/dev/null || true
# eza via the gierens PPA (debs not in base apt) # eza: the old apt.fury.io/gierens PPA is dead (401 Unauthorized). Install the
# single binary from the upstream GitHub release tarball into /usr/local/bin
# (system-wide — works for root and every sudo user; no HOME dependency).
# Remove a stale eza.list left by the old (pre-fix) run that used the now-dead
# apt.fury.io PPA — otherwise 'apt-get update' keeps hitting the 401 forever.
sudo_ rm -f /etc/apt/sources.list.d/eza.list /etc/apt/keyrings/gierens.gpg
if ! command -v eza >/dev/null 2>&1; then if ! command -v eza >/dev/null 2>&1; then
sudo_ install -m 0755 -d /etc/apt/keyrings local eza_arch; case "$ARCH" in
curl -fsSL https://raw.githubusercontent.com/eza-community/eza/main/deb.asc \ amd64) eza_arch=x86_64-unknown-linux-gnu ;;
| sudo_ tee /etc/apt/keyrings/gierens.gpg >/dev/null arm64) eza_arch=aarch64-unknown-linux-gnu ;;
echo "deb [signed-by=/etc/apt/keyrings/gierens.gpg] http://apt.fury.io/eza/ /" \ *) eza_arch="${ARCH}-unknown-linux-gnu" ;;
| sudo_ tee /etc/apt/sources.list.d/eza.list >/dev/null esac
sudo_ apt-get update -y && sudo_ apt-get install -y eza || warn "eza PPA failed" local url="https://github.com/eza-community/eza/releases/latest/download/eza_${eza_arch}.tar.gz"
local tmp; tmp="$(mktemp -d)"
if wget -qO "$tmp/eza.tgz" "$url" && tar xzf "$tmp/eza.tgz" -C "$tmp"; then
sudo_ install -m 0755 "$tmp/eza" /usr/local/bin/eza
else
warn "eza download failed ($url)"
fi
rm -rf "$tmp"
fi fi
# starship: official installer # starship: official installer. NOTE — scrub $ARCH/$PLATFORM from the env first:
# the installer reuses a pre-set ARCH (our lib/detect.sh exports ARCH=amd64) and
# skips its own amd64->x86_64 mapping, yielding an unsupported 'amd64-unknown-linux-
# musl' target ('builds for amd64-unknown-linux-musl are not yet available').
# Default bin dir is /usr/local/bin (system-wide).
if ! command -v starship >/dev/null 2>&1; then if ! command -v starship >/dev/null 2>&1; then
curl -sS https://starship.rs/install.sh | sh -s -- -y || warn "starship install failed" curl -sS https://starship.rs/install.sh \
| env -u ARCH -u PLATFORM sh -s -- -y || warn "starship install failed"
fi fi
# zoxide: official installer # zoxide: official installer. Install system-wide (/usr/local/bin) instead of the
# default ~/.local/bin — when run as root the default lands in /root (wrong user
# for the create-user path) and isn't on $PATH; /usr/local/bin is on PATH for all.
if ! command -v zoxide >/dev/null 2>&1; then if ! command -v zoxide >/dev/null 2>&1; then
curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash || warn "zoxide install failed" curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh \
| env -u ARCH sh -s -- --bin-dir /usr/local/bin --man-dir /usr/local/share/man \
|| warn "zoxide install failed"
fi fi
# yq: direct binary from mikefarah/yq releases (stable URL) # yq: direct binary from mikefarah/yq releases (stable URL)
+5 -5
View File
@@ -24,13 +24,13 @@ run_docker() {
warn "systemctl unavailable — start docker manually if needed" warn "systemctl unavailable — start docker manually if needed"
fi fi
# add current user to docker group # add the config target user to the docker group (created-user or self)
if getent group docker >/dev/null 2>&1; then if getent group docker >/dev/null 2>&1; then
if id -nG "$USER" | grep -qw docker; then if id -nG "${TARGET_USER:-$USER}" | grep -qw docker; then
ok "$USER already in docker group" ok "${TARGET_USER:-$USER} already in docker group"
else else
sudo_ usermod -aG docker "$USER" sudo_ usermod -aG docker "${TARGET_USER:-$USER}"
ok "$USER added to docker group (log out/in or 'newgrp docker' to use it)" ok "${TARGET_USER:-$USER} added to docker group (log out/in or 'newgrp docker' to use it)"
fi fi
fi fi
} }
+23 -6
View File
@@ -27,12 +27,25 @@ run_angie() {
} }
_angie_arch() { _angie_arch() {
if command -v yay >/dev/null 2>&1; then if ! command -v yay >/dev/null 2>&1; then
as_user yay -S --noconfirm angie 2>/dev/null \ warn "yay not available — install angie manually from AUR: yay -S angie-bin"
|| as_user yay -S --noconfirm aur/angie 2>/dev/null \ return
|| warn "angie AUR build failed — try: yay -S angie" fi
# Prefer angie-bin: it repackages the upstream official Ubuntu .deb, so it's
# instant (no source compile) and carries the same compiled-in modules our
# config relies on — http_acme, http_ssl, http_v2/v3, http_realip, stream*
# (verified via the .deb's 'angie -V'). Drop back to the source 'angie'
# package only if the binary package is unavailable.
log "AUR install: angie-bin first, fall back to angie (source build)"
if as_user yay -S --noconfirm --needed angie-bin 2>/dev/null; then
ok "angie-bin installed from AUR"
return
fi
log "angie-bin unavailable/failed — building angie from source (may take a few minutes)"
if as_user yay -S --noconfirm --needed aur/angie 2>/dev/null; then
ok "angie (source) installed from AUR"
else else
warn "yay not available — install angie manually from AUR: yay -S angie" warn "angie install failed — try manually: yay -S angie-bin (or: yay -S angie)"
fi fi
} }
@@ -41,7 +54,11 @@ _angie_debian() {
sudo_ install -m 0755 -d /etc/apt/keyrings sudo_ install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://angie.software/keys/angie-signing.gpg \ curl -fsSL https://angie.software/keys/angie-signing.gpg \
| sudo_ tee /etc/apt/keyrings/angie-signing.gpg >/dev/null | sudo_ tee /etc/apt/keyrings/angie-signing.gpg >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/angie-signing.gpg] https://download.angie.software/angie/${ID}/ ${VERSION_CODENAME} main" \ # Repo path is angie/<ID>/<VERSION_ID>/ with the codename as the apt suite, e.g.
# debian/13/dists/trixie/ or ubuntu/22.04/dists/jammy/. Omitting VERSION_ID (just
# angie/debian/) fetches debian/dists/trixie -> 404 'no Release file' — must pin the
# numeric version in the URL path.
echo "deb [signed-by=/etc/apt/keyrings/angie-signing.gpg] https://download.angie.software/angie/${ID}/${VERSION_ID}/ ${VERSION_CODENAME} main" \
| sudo_ tee /etc/apt/sources.list.d/angie.list >/dev/null | sudo_ tee /etc/apt/sources.list.d/angie.list >/dev/null
sudo_ apt-get update -y sudo_ apt-get update -y
sudo_ apt-get install -y angie sudo_ apt-get install -y angie
+34 -14
View File
@@ -5,18 +5,35 @@ step_name="shell-zsh"
step_desc="zsh + oh-my-zsh + plugins, set default shell, ship config" step_desc="zsh + oh-my-zsh + plugins, set default shell, ship config"
step_run="run_zsh" step_run="run_zsh"
ZSH="${ZSH:-$HOME/.oh-my-zsh}" # All paths resolve against the config TARGET (set by 05-create-user).
ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}" # NOTE: we deliberately do NOT honor an inherited $ZSH env var — oh-my-zsh exports
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$HOME/.config/zsh}" # ZSH when it loads, so a setup.sh run from an omz-equipped shell would otherwise
# pin these to the *old* home (e.g. /root) while the chown step retargets to the
# *new* user, mis-owning the wrong tree (the 'insecure completion dirs' bug).
ZSH="$TARGET_HOME/.oh-my-zsh"
ZSH_CUSTOM="$ZSH/custom"
ZDOTDIR_TARGET="$TARGET_HOME/.config/zsh"
run_zsh() { run_zsh() {
log "installing zsh + oh-my-zsh" log "installing zsh + oh-my-zsh"
_install_zsh _install_zsh
_install_omz _install_omz
_install_plugins _install_plugins
_chown_target_tree
_ship_config _ship_config
_set_default_shell _set_default_shell
if [[ "$TARGET_USER" != "$USER" ]]; then
log "zsh ready. Start it as $TARGET_USER: sudo -iu $TARGET_USER"
else
ok "zsh ready — start it with: exec zsh" ok "zsh ready — start it with: exec zsh"
fi
}
_chown_target_tree() {
# when running as root for a created user, make omz + plugins + config owned by them
if [[ "$TARGET_USER" == "$USER" ]]; then return; fi
local grp; grp="$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")"
chown -R "$TARGET_USER:$grp" "$ZSH" "$ZDOTDIR_TARGET" "$TARGET_HOME/.zshrc" 2>/dev/null || true
} }
_install_zsh() { _install_zsh() {
@@ -40,8 +57,8 @@ _install_plugins() {
_clone_plugin zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions" _clone_plugin zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
_clone_plugin zsh-users/zsh-syntax-highlighting "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting" _clone_plugin zsh-users/zsh-syntax-highlighting "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
_clone_plugin zsh-users/zsh-completions "$ZSH_CUSTOM/plugins/zsh-completions" _clone_plugin zsh-users/zsh-completions "$ZSH_CUSTOM/plugins/zsh-completions"
_clone_plugin Aloofenlore/history-substring-search "$ZSH_CUSTOM/plugins/history-substring-search" _clone_plugin zsh-users/zsh-history-substring-search "$ZSH_CUSTOM/plugins/history-substring-search"
_clone_plugin Aloof98/fzf-tab "$ZSH_CUSTOM/plugins/fzf-tab" _clone_plugin Aloxaf/fzf-tab "$ZSH_CUSTOM/plugins/fzf-tab"
} }
_clone_plugin() { # repo dest _clone_plugin() { # repo dest
@@ -55,7 +72,6 @@ _ship_config() {
log "shipping zsh config to $ZDOTDIR_TARGET" log "shipping zsh config to $ZDOTDIR_TARGET"
mkdir -p "$ZDOTDIR_TARGET" mkdir -p "$ZDOTDIR_TARGET"
local f local f
local f
for f in zshrc aliases.zsh functions.zsh path.zsh plugins.zsh; do for f in zshrc aliases.zsh functions.zsh path.zsh plugins.zsh; do
local src="$BOOT_DIR/config/zsh/$f" local src="$BOOT_DIR/config/zsh/$f"
[[ -f "$src" ]] || continue [[ -f "$src" ]] || continue
@@ -67,8 +83,8 @@ _ship_config() {
if [[ ! -f "$ZDOTDIR_TARGET/aliases-extras.zsh" ]]; then if [[ ! -f "$ZDOTDIR_TARGET/aliases-extras.zsh" ]]; then
cp "$BOOT_DIR/config/zsh/aliases-extras.zsh" "$ZDOTDIR_TARGET/aliases-extras.zsh" cp "$BOOT_DIR/config/zsh/aliases-extras.zsh" "$ZDOTDIR_TARGET/aliases-extras.zsh"
fi fi
# ~/.zshrc shim that loads our real config via ZDOTDIR (so omz $ZSH + plugins resolve) # ~/.zshrc shim that loads our real config via ZDOTDIR
local shim="$HOME/.zshrc" local shim="$TARGET_HOME/.zshrc"
if ! grep -q 'BOOTSTRAP ZDOTDIR' "$shim" 2>/dev/null; then if ! grep -q 'BOOTSTRAP ZDOTDIR' "$shim" 2>/dev/null; then
cat >> "$shim" <<'EOF' cat >> "$shim" <<'EOF'
@@ -77,7 +93,11 @@ export ZDOTDIR="${ZDOTDIR:-$HOME/.config/zsh}"
[[ -f "$ZDOTDIR/zshrc" ]] && source "$ZDOTDIR/zshrc" [[ -f "$ZDOTDIR/zshrc" ]] && source "$ZDOTDIR/zshrc"
EOF EOF
fi fi
ok "zsh config installed ($ZDOTDIR_TARGET) + ~/.zshrc shim" # ensure the whole shipped tree is owned by the target user
if [[ "$TARGET_USER" != "$USER" ]]; then
chown -R "$TARGET_USER:$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")" "$ZDOTDIR_TARGET" "$shim"
fi
ok "zsh config installed ($ZDOTDIR_TARGET) + $shim shim"
} }
_set_default_shell() { _set_default_shell() {
@@ -87,13 +107,13 @@ _set_default_shell() {
if ! grep -qx "$zsh_bin" /etc/shells 2>/dev/null; then if ! grep -qx "$zsh_bin" /etc/shells 2>/dev/null; then
echo "$zsh_bin" | sudo_ tee -a /etc/shells >/dev/null echo "$zsh_bin" | sudo_ tee -a /etc/shells >/dev/null
fi fi
if [[ "$SHELL" != "$zsh_bin" ]] && [[ "$(getent passwd "$USER" | cut -d: -f7)" != "$zsh_bin" ]]; then if [[ "$(getent passwd "$TARGET_USER" | cut -d: -f7)" != "$zsh_bin" ]]; then
if yn "Set zsh as default login shell for $USER?" y; then if yn "Set zsh as default login shell for $TARGET_USER?" y; then
sudo_ chsh -s "$zsh_bin" "$USER" && ok "default shell set to zsh" sudo_ chsh -s "$zsh_bin" "$TARGET_USER" && ok "default shell set to zsh for $TARGET_USER"
else else
warn "zsh not set as default — run 'chsh -s $zsh_bin' later" warn "zsh not set as default — run 'chsh -s $zsh_bin' for $TARGET_USER' later"
fi fi
else else
ok "zsh already your default shell" ok "zsh already the default shell for $TARGET_USER"
fi fi
} }
+10 -15
View File
@@ -2,30 +2,25 @@
# 35-nvm — Node Version Manager + Node LTS. # 35-nvm — Node Version Manager + Node LTS.
step_name="nvm" step_name="nvm"
step_desc="nvm into ~/.nvm, install Node LTS" step_desc="nvm into ~$TARGET_USER/.nvm, install Node LTS"
step_run="run_nvm" step_run="run_nvm"
run_nvm() { run_nvm() {
log "installing nvm" log "installing nvm for $TARGET_USER"
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" local nvm_home="$TARGET_HOME/.nvm"
export NVM_DIR="$nvm_home"
if [[ -s "$NVM_DIR/nvm.sh" ]]; then if [[ -s "$NVM_DIR/nvm.sh" ]]; then
ok "nvm already present at $NVM_DIR" ok "nvm already present at $NVM_DIR"
else else
# fetch the install script at a pinned NVM version (update here to upgrade) # fetch the install script at a pinned NVM version (update here to upgrade)
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \ as_user env NVM_DIR="$nvm_home" bash -c 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash' \
|| { warn "nvm install failed"; return; } || { warn "nvm install failed"; return; }
ok "nvm installed to $NVM_DIR" ok "nvm installed to $NVM_DIR"
fi fi
# shellcheck disable=SC1091 # run node/lts install as the target user so binary ownership + default are theirs
source "$NVM_DIR/nvm.sh" as_user bash -lc 'export NVM_DIR='"$NVM_DIR"'; source "$NVM_DIR/nvm.sh"; nvm install --lts && nvm use --lts && nvm alias default lts/*' \
if command -v nvm >/dev/null 2>&1; then || warn "node lts install had issues (continue; run 'nvm install --lts' as $TARGET_USER)"
log "installing Node LTS via nvm" as_user bash -lc 'export NVM_DIR='"$NVM_DIR"'; source "$NVM_DIR/nvm.sh"; command -v node >/dev/null && echo " node \$(node -v) active for $TARGET_USER"' 2>/dev/null || true
nvm install --lts || warn "nvm install --lts failed"
nvm use --lts 2>/dev/null || true
nvm alias default 'lts/*' 2>/dev/null || true
command -v node >/dev/null 2>&1 && ok "node $(node -v) active"
else
warn "nvm.sh sourced but nvm not available — check $NVM_DIR"
fi
} }
+9 -11
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 40-nvim — clone your nvim-minimal config from gitea to ~/.config/nvim. # 40-nvim — clone your nvim-minimal config from gitea to ~$TARGET_USER/.config/nvim.
step_name="nvim" step_name="nvim"
step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~/.config/nvim" step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~$TARGET_USER/.config/nvim"
step_run="run_nvim" step_run="run_nvim"
NVIM_REPO_URL="${NVIM_REPO_URL:-https://g.mk.fo/bootstrap/nvim-minimal.git}" NVIM_REPO_URL="${NVIM_REPO_URL:-https://g.mk.fo/bootstrap/nvim-minimal.git}"
@@ -11,24 +11,22 @@ run_nvim() {
log "installing nvim config" log "installing nvim config"
command -v nvim >/dev/null 2>&1 || warn "nvim binary not found — install the packages step first" command -v nvim >/dev/null 2>&1 || warn "nvim binary not found — install the packages step first"
local target="${XDG_CONFIG_HOME:-$HOME/.config}/nvim" local target="$TARGET_HOME/.config/nvim"
if [[ -d "$target/.git" ]]; then if [[ -d "$target/.git" ]]; then
log "existing nvim config is a git repo — pulling" log "existing nvim config is a git repo — pulling"
git -C "$target" pull --ff-only || warn "could not pull existing nvim config" as_user git -C "$target" pull --ff-only || warn "could not pull existing nvim config"
ok "nvim config updated at $target" ok "nvim config updated at $target"
return return
fi fi
if [[ -e "$target" ]]; then if [[ -e "$target" ]]; then
local bak="${target}.bak.$(date +%Y%m%d-%H%M%S)" local bak="${target}.bak.$(date +%Y%m%d-%H%M%S)"
mv "$target" "$bak" as_user mv "$target" "$bak" 2>/dev/null || sudo_ mv "$target" "$bak"
warn "backed up existing $target -> $bak" warn "backed up existing $target -> $bak"
fi fi
if git clone "$NVIM_REPO_URL" "$target"; then as_user git clone "$NVIM_REPO_URL" "$target" && ok "nvim config cloned to $target" \
ok "nvim config cloned to $target" || { err "failed to clone $NVIM_REPO_URL"; sudo_ git clone "$NVIM_REPO_URL" "$target" \
else && chown -R "$TARGET_USER:$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")" "$target" \
err "failed to clone $NVIM_REPO_URL" && ok "nvim config cloned to $target (as root, then chowned)"; }
return 1
fi
} }
+335 -1
View File
@@ -2,7 +2,7 @@
# 50-hardening — system defaults. Each sub-step y/N, all idempotent. # 50-hardening — system defaults. Each sub-step y/N, all idempotent.
step_name="hardening" step_name="hardening"
step_desc="ssh(ufw-safe), ufw, updates, unattended, tz, hostname, swap, locale, fail2ban" step_desc="ssh, ufw, updates, unattended, tz, hostname, swap, locale, fail2ban, angie config + built-in ACME"
step_run="run_hardening" step_run="run_hardening"
run_hardening() { run_hardening() {
@@ -14,6 +14,8 @@ run_hardening() {
_h_swap _h_swap
_h_locale _h_locale
_h_fail2ban _h_fail2ban
_h_angie_config
_h_angie_acme
ok "hardening pass complete" ok "hardening pass complete"
} }
@@ -103,8 +105,12 @@ _h_tz_hostname() {
if command -v timedatectl >/dev/null 2>&1; then if command -v timedatectl >/dev/null 2>&1; then
if yn "Set timezone (default UTC)?" y; then if yn "Set timezone (default UTC)?" y; then
local tz="${REPLY_TZ:-}" local tz="${REPLY_TZ:-}"
if (( ${ALL:-0} )); then
tz="UTC"; ok "--yes: timezone defaulting to UTC"
else
read -rp " timezone [UTC]: " tz read -rp " timezone [UTC]: " tz
[[ -z "$tz" ]] && tz="UTC" [[ -z "$tz" ]] && tz="UTC"
fi
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz" sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
fi fi
else else
@@ -186,3 +192,331 @@ EOF
fi fi
ok "fail2ban installed + sshd jail enabled" ok "fail2ban installed + sshd jail enabled"
} }
# ── angie config: _on/ targets/ modules/ + sane root config ────────────────
_h_angie_config() {
if ! command -v angie >/dev/null 2>&1; then
echo " skip angie config (angie not installed — run the angie step first)"; return
fi
if ! yn "Set up Angie config layout (/etc/angie/_on targets modules) + sane root config?" y; then
echo " skip angie config"; return
fi
local d=/etc/angie
sudo_ install -d -m 0755 "$d/_on" "$d/targets" "$d/modules" "$d/modules/http"
# ── extract packaging-specific values from the stock angie.conf ──
# Different Angie packages set user/pid/error_log differently (Arch source
# package → /etc/nginx/nginx.conf with 'user http'; angie-bin / Debian .deb
# → /etc/angie/angie.conf with 'user angie'). Preserve them when present,
# fall back to sane defaults otherwise (defaults cover all distros here).
local oldcfg="$d/angie.conf"
local altcfg="/etc/nginx/nginx.conf" # the source AUR package's path
local stock="$oldcfg"
[[ -f "$oldcfg" ]] || stock="$altcfg"
local angie_user="" angie_pid="" angie_err="" angie_modules=""
if [[ -f "$stock" ]]; then
angie_user=$(grep -E '^\s*user\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
angie_pid=$(grep -E '^\s*pid\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
angie_err=$(grep -E '^\s*error_log\s+' "$stock" | head -1 | awk '{print $2}' | tr -d ';')
# collect any load_module lines (dynamic modules)
angie_modules=$(grep -E '^\s*load_module\s+' "$stock" || true)
fi
# sane defaults if not found
[[ -z "$angie_user" ]] && angie_user=$({ [[ "${DISTRO:-}" == "arch" ]] && echo http || echo angie; })
[[ -z "$angie_pid" ]] && angie_pid=/run/angie.pid
[[ -z "$angie_err" ]] && angie_err=/var/log/angie/error.log
# locate mime.types: angie-bin/Debian ship /etc/angie/mime.types; the Arch source
# 'angie' package instead pulls it from the 'mailcap' dep at /etc/nginx/mime.types
# (and deletes its own). Pick whichever exists so 'angie -t' doesn't fail on include.
local angie_mime=""
for m in "$d/mime.types" /etc/nginx/mime.types /etc/mime.types; do
[[ -f "$m" ]] && { angie_mime="$m"; break; }
done
[[ -z "$angie_mime" ]] && { warn "no mime.types found; installing a minimal one"; _angie_install_mime "$d/mime.types"; angie_mime="$d/mime.types"; }
# the modules/ include dir must exist or the glob fails the config test
sudo_ install -d -m 0755 "$d/modules" "$d/modules/http"
# backup the original once
if [[ -f "$oldcfg" && ! -f "$oldcfg.orig.bootstrap" ]]; then
sudo_ cp -a "$oldcfg" "$oldcfg.orig.bootstrap"
fi
# ── write our root angie.conf (once — guarded, won't clobber later edits) ──
if grep -q '# managed by bootstrap' "$oldcfg" 2>/dev/null; then
ok "$oldcfg already bootstrap-managed — leaving it (keeps your edits)"
else
sudo_ tee "$oldcfg" >/dev/null <<EOF
# angie.conf — managed by bootstrap. Original backup: angie.conf.orig.bootstrap
# Edit freely; re-running the hardening step will NOT overwrite (it writes once).
user $angie_user;
worker_processes auto;
pid $angie_pid;
error_log $angie_err warn;
# dynamic modules carried over from the original package config (if any)
$angie_modules
# top-level context snippets (stream{}, env, etc.)
include /etc/angie/modules/*.conf;
events {
worker_connections 1024;
}
http {
include $angie_mime;
default_type application/octet-stream;
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" "\$http_user_agent"';
access_log /var/log/angie/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
client_max_body_size 16m;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript;
# http-level snippets: upstreams, maps, real_ip, proxy defaults
include /etc/angie/modules/http/*.conf;
# enabled hosts (symlinks from targets/*). _on/*.conf only — targets/ is staging.
include /etc/angie/_on/*.conf;
}
EOF
fi
# ── http-level common snippet (real_ip + proxy defaults) ──
if [[ ! -f "$d/modules/http/00-common.conf" ]]; then
sudo_ tee "$d/modules/http/00-common.conf" >/dev/null <<'EOF'
# real_ip — trust private ranges + (commented Cloudflare). Uncomment/edit for your upstream.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 169.254.0.0/16;
# set_real_ip_from 173.245.48.0/20; # Cloudflare, see https://www.cloudflare.com/ips/
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# proxy defaults (inherited by every server/location unless overridden)
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
EOF
fi
# ── default catch-all server: unmatched Host -> drop ──
if [[ ! -f "$d/_on/00-default.conf" ]]; then
# The default_server also listens on 443 to catch unknown-SNI TLS clients.
# It needs SOME cert to complete the handshake before `return 444` drops them;
# angie won't start a 443 listener without ssl_certificate. Generate a self-signed
# dummy once (CN='bootstrap-default', 10y) so unknown-SNI clients get dropped
# after connecting while real vhosts are picked by SNI.
if [[ ! -f "$d/ssl/bootstrap-default.crt" ]]; then
sudo_ install -d -m 0755 "$d/ssl"
sudo_ openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -keyout "$d/ssl/bootstrap-default.key" -out "$d/ssl/bootstrap-default.crt" -subj "/CN=bootstrap-default/O=bootstrap" 2>/dev/null
fi
sudo_ tee "$d/_on/00-default.conf" >/dev/null <<'EOF'
# default_server: drop requests that match no enabled host (drive-by scanners, bare-IP)
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/angie/ssl/bootstrap-default.crt;
ssl_certificate_key /etc/angie/ssl/bootstrap-default.key;
return 444;
}
EOF
fi
# ── angie-enable / angie-disable helpers ──
_angie_helpers
# ── source-angie bridge: point /etc/nginx/nginx.conf at our /etc/angie layout ─
# The source AUR 'angie' package compiles --conf-path=/etc/nginx/nginx.conf and
# its service runs '/usr/bin/nginx' with no -c flag, so it ignores /etc/angie.
# When that path is where angie actually reads, back it up once and symlink it to
# our managed angie.conf so the _on/targetsmodules layout takes effect. The
# angie-bin / Debian packages already read /etc/angie/angie.conf directly.
if [[ -f /etc/nginx/nginx.conf && -f "$d/angie.conf" ]]; then
local nginxconf=/etc/nginx/nginx.conf
if [[ "$(readlink -f "$nginxconf" 2>/dev/null)" == "$d/angie.conf" ]]; then
ok "$nginxconf already symlinks to $d/angie.conf"
else
if [[ ! -f "${nginxconf}.orig.bootstrap" && ! -L "$nginxconf" ]]; then
sudo_ cp -a "$nginxconf" "${nginxconf}.orig.bootstrap"
fi
sudo_ ln -sfn "$d/angie.conf" "$nginxconf"
ok "symlinked $nginxconf -> $d/angie.conf (source-angie bridge); backup at ${nginxconf}.orig.bootstrap"
fi
fi
if sudo_ angie -t 2>&1; then
ok "angie config valid"
if command -v systemctl >/dev/null 2>&1 && systemctl is-active angie >/dev/null 2>&1; then
sudo_ systemctl reload angie 2>/dev/null && ok "angie reloaded"
else
echo " start angie when ready: sudo systemctl enable --now angie"
fi
else
err "angie -t failed — review $d/angie.conf (backup at $oldcfg.orig.bootstrap)"
fi
ok "angie layout ready: $d/{angie.conf,_on/,targets/,modules/,modules/http/}"
echo " enable a site: angie-enable <name> (after writing $d/targets/<name>.conf)"
echo " disable a site: angie-disable <name>"
# ufw already opened 80/443 in _h_ufw
}
_angie_install_mime() { # dest — write a minimal mime.types if none exists on the host
sudo_ tee "$1" >/dev/null <<'EOF'
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
image/png png;
image/svg+xml svg svgz;
application/javascript js;
application/json json;
application/pdf pdf;
application/zip zip;
application/octet-stream bin exe dll so deb dmg iso img;
}
EOF
}
_angie_helpers() {
sudo_ tee /usr/local/bin/angie-enable >/dev/null <<'EOF'
#!/usr/bin/env bash
# angie-enable <name> — symlink targets/<name>.conf into _on/ and validate.
set -euo pipefail
name="${1:-}"
[ -n "$name" ] || { echo "usage: angie-enable <name>" >&2; exit 2; }
target="/etc/angie/targets/${name}.conf"
link="/etc/angie/_on/${name}.conf"
[ -f "$target" ] || { echo "no such target: $target" >&2; exit 1; }
ln -sfn "../targets/${name}.conf" "$link"
echo "enabled: $name -> $link"
if command -v angie >/dev/null 2>&1; then
sudo angie -t || { echo "config invalid — review before reload" >&2; exit 1; }
sudo systemctl reload angie && echo "reloaded"
fi
EOF
sudo_ tee /usr/local/bin/angie-disable >/dev/null <<'EOF'
#!/usr/bin/env bash
# angie-disable <name> — remove the _on/<name>.conf symlink and reload.
set -euo pipefail
name="${1:-}"
[ -n "$name" ] || { echo "usage: angie-disable <name>" >&2; exit 2; }
link="/etc/angie/_on/${name}.conf"
[ -L "$link" ] || [ -e "$link" ] || { echo "$name not enabled ($link missing)" >&2; exit 1; }
rm -f "$link"
echo "disabled: $name"
if command -v angie >/dev/null 2>&1; then
sudo angie -t || true
sudo systemctl reload angie && echo "reloaded"
fi
EOF
sudo_ chmod +x /usr/local/bin/angie-enable /usr/local/bin/angie-disable
}
# ── angie built-in ACME (Let's Encrypt) ────────────────────────────────────
# No certbot: Angie ships http_acme and fetches/renews certs itself.
# Bootstrap can't know your domain/email, so we ship an example TLS server
# target + an `angie-issue` helper that creates the live acme_client on demand.
_h_angie_acme() {
if ! command -v angie >/dev/null 2>&1; then
echo " skip angie ACME (angie not installed)"; return
fi
if ! yn "Ship Angie built-in ACME template + angie-issue helper?" y; then
echo " skip angie ACME"; return
fi
local d=/etc/angie
# example TLS server target — NOT auto-enabled (.example, so angie-enable won't glob it)
if [[ ! -f "$d/targets/example-https.conf.example" ]]; then
sudo_ tee "$d/targets/example-https.conf.example" >/dev/null <<'EOF'
# Template for a TLS host using Angie built-in ACME.
# Use it via the helper: angie-issue <domain> [you@email]
# Or by hand: copy to targets/<domain>.conf, fill <DOMAIN>, then: angie-enable <domain>
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name <DOMAIN>;
acme default; # add this server_name to the shared 'default' ACME certificate
ssl_certificate $acme_cert_default;
ssl_certificate_key $acme_cert_key_default;
# http -> https redirect
if ($scheme = http) { return 301 https://$host$request_uri; }
location / {
# replace with your app / proxy_pass upstream;
return 200 "angie + builtin ACME ok\n";
add_header Content-Type text/plain;
}
}
EOF
fi
sudo_ tee /usr/local/bin/angie-issue >/dev/null <<'EOF'
#!/usr/bin/env bash
# angie-issue <domain> [email] — create a TLS host backed by Angie built-in ACME.
# First call (needs email) writes /etc/angie/modules/http/acme.conf with the
# shared 'default' acme_client; every call writes targets/<domain>.conf and enables it.
# All enabled domains using `acme default` share one cert covering all their server_names.
set -euo pipefail
domain="${1:-}"; email="${2:-}"
[ -n "$domain" ] || { echo "usage: angie-issue <domain> [email]" >&2; exit 2; }
acme_conf=/etc/angie/modules/http/acme.conf
if [ ! -f "$acme_conf" ]; then
[ -n "$email" ] || { echo "first-time: also pass your email for Let's Encrypt" >&2; exit 2; }
cat > "$acme_conf" <<ACME
# managed by angie-issue — Angie built-in ACME (Let's Encrypt)
# resolver is required by the acme_client directive (http context).
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
acme_client default https://acme-v02.api.letsencrypt.org/directory email=${email};
ACME
echo "wrote $acme_conf (email=${email})"
else
echo "_existing $acme_conf (shared default client)"
fi
target=/etc/angie/targets/${domain}.conf
sed -e "s|<DOMAIN>|${domain}|g" /etc/angie/targets/example-https.conf.example > "$target"
echo "wrote $target"
ln -sfn "../targets/${domain}.conf" /etc/angie/_on/${domain}.conf
echo "enabled: $domain"
sudo angie -t
sudo systemctl reload angie && echo "angie reloaded — certificate is obtained automatically."
EOF
sudo_ chmod +x /usr/local/bin/angie-issue
ok "angie ACME ready: example host at $d/targets/example-https.conf.example"
echo " issue a cert: angie-issue <domain> you@email"
echo " then reload picks it up; Angie renews automatically before expiry."
}
+2 -2
View File
@@ -15,7 +15,7 @@ run_sanity() {
echo " memory : $(free -h | awk '/^Mem/ {print $2}') total" echo " memory : $(free -h | awk '/^Mem/ {print $2}') total"
echo " swap : $(swapon --show 2>/dev/null | awk 'NR>1{print $3; found=1} END{if(!found) print "none"}')" echo " swap : $(swapon --show 2>/dev/null | awk 'NR>1{print $3; found=1} END{if(!found) print "none"}')"
echo " timezone : $(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo '?')" echo " timezone : $(timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo '?')"
echo " hostname : $(hostname)" echo " hostname : $(uname -n 2>/dev/null || hostnamectl --static 2>/dev/null || cat /etc/hostname 2>/dev/null || echo '?')"
echo " locale : $(locale 2>/dev/null | awk -F= '/^LANG/ {print $2}')" echo " locale : $(locale 2>/dev/null | awk -F= '/^LANG/ {print $2}')"
echo " ufw : $(sudo_ ufw status 2>/dev/null | head -1 || echo 'not installed')" echo " ufw : $(sudo_ ufw status 2>/dev/null | head -1 || echo 'not installed')"
echo " fail2ban : $(sudo_ fail2ban-client status 2>/dev/null | head -2 || echo 'not running')" echo " fail2ban : $(sudo_ fail2ban-client status 2>/dev/null | head -2 || echo 'not running')"
@@ -32,7 +32,7 @@ run_sanity() {
- Add starship config if you want a custom prompt: starship preset list - Add starship config if you want a custom prompt: starship preset list
(defaults ship with the bootstrap). (defaults ship with the bootstrap).
- Consider 'newgrp docker' or re-login to activate the docker group. - Consider 'newgrp docker' or re-login to activate the docker group.
- Install domain + TLS for angie separately (certbot/acme.sh), not handled here. - Issue TLS certs for angie hosts with: angie-issue <domain> you@email.com
EOF EOF
ok "bootstrap sanity report done." ok "bootstrap sanity report done."
} }
+17 -3
View File
@@ -6,11 +6,18 @@
# Flags: # Flags:
# --yes run all steps without prompting # --yes run all steps without prompting
# --only NAME run only the step matching NAME # --only NAME run only the step matching NAME
# --skip NAME run all steps except NAME (repeatable in future) # --skip NAME run all steps except NAME
# --create-user force creating a sudo user (otherwise Arch auto-creates, Debian asks)
# --username NAME override the sudo user name (default: mk)
# -h|--help usage # -h|--help usage
set -euo pipefail set -euo pipefail
# Default config target = whoever is running this. The create-user step
# (05-create-user) may override these to point at a freshly-created sudoer.
export TARGET_USER="$USER"
export TARGET_HOME="$HOME"
BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
@@ -18,18 +25,25 @@ source "$BOOT_DIR/lib/detect.sh"
source "$BOOT_DIR/lib/log.sh" source "$BOOT_DIR/lib/log.sh"
source "$BOOT_DIR/lib/prompt.sh" source "$BOOT_DIR/lib/prompt.sh"
ALL=0; ONLY=""; SKIP="" # helper: resolve the target user's real home even when we're root operating
# on a created user (whose home isn't $HOME).
target_home() { getent passwd "${TARGET_USER:-$USER}" | cut -d: -f6; }
ALL=0; ONLY=""; SKIP=""; CREATE_USER=0; USERNAME_ARG=""
while (( $# )); do while (( $# )); do
case "$1" in case "$1" in
--yes|-y) ALL=1; shift ;; --yes|-y) ALL=1; shift ;;
--only) ONLY="${2:-}"; shift 2 ;; --only) ONLY="${2:-}"; shift 2 ;;
--skip) SKIP="${2:-}"; shift 2 ;; --skip) SKIP="${2:-}"; shift 2 ;;
--create-user) CREATE_USER=1; shift ;;
--username) USERNAME_ARG="${2:-}"; shift 2 ;;
-h|--help) -h|--help)
echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]" grep -E '^# --' "$0" | sed 's/^# //'
exit 0 ;; exit 0 ;;
*) echo "unknown arg: $1" >&2; shift ;; *) echo "unknown arg: $1" >&2; shift ;;
esac esac
done done
export CREATE_USER USERNAME_ARG
# ---------------------------------------------------------------- collect steps # ---------------------------------------------------------------- collect steps
NAMES=(); DESCS=(); RUNS=() NAMES=(); DESCS=(); RUNS=()
+1
View File
@@ -0,0 +1 @@
- [ ] pm2