Compare commits
No commits in common. "146c79a57e216287ed6e823144370b8673e60f55" and "5e0e96e8e2cba3293fa20ab603b6060eb48577e9" have entirely different histories.
146c79a57e
...
5e0e96e8e2
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
||||
*.bak.*
|
||||
178
README.md
178
README.md
@ -1,178 +0,0 @@
|
||||
# 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.
|
||||
@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@ -1,76 +0,0 @@
|
||||
# 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
|
||||
@ -1,28 +0,0 @@
|
||||
# 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 "$@"; }
|
||||
@ -1,35 +0,0 @@
|
||||
# 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
|
||||
@ -1,23 +0,0 @@
|
||||
# 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
|
||||
@ -1,59 +0,0 @@
|
||||
# ~/.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
|
||||
@ -2,15 +2,9 @@
|
||||
# prompt.sh — plain y/N prompts.
|
||||
|
||||
# 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() {
|
||||
local p="$1" d="${2:-y}" hint
|
||||
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
|
||||
[[ -z "$REPLY" ]] && REPLY="$d"
|
||||
}
|
||||
|
||||
@ -1,157 +0,0 @@
|
||||
#!/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"
|
||||
}
|
||||
@ -24,13 +24,13 @@ run_docker() {
|
||||
warn "systemctl unavailable — start docker manually if needed"
|
||||
fi
|
||||
|
||||
# add the config target user to the docker group (created-user or self)
|
||||
# add current user to docker group
|
||||
if getent group docker >/dev/null 2>&1; then
|
||||
if id -nG "${TARGET_USER:-$USER}" | grep -qw docker; then
|
||||
ok "${TARGET_USER:-$USER} already in docker group"
|
||||
if id -nG "$USER" | grep -qw docker; then
|
||||
ok "$USER already in docker group"
|
||||
else
|
||||
sudo_ usermod -aG docker "${TARGET_USER:-$USER}"
|
||||
ok "${TARGET_USER:-$USER} added to docker group (log out/in or 'newgrp docker' to use it)"
|
||||
sudo_ usermod -aG docker "$USER"
|
||||
ok "$USER added to docker group (log out/in or 'newgrp docker' to use it)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@ -5,31 +5,18 @@ step_name="shell-zsh"
|
||||
step_desc="zsh + oh-my-zsh + plugins, set default shell, ship config"
|
||||
step_run="run_zsh"
|
||||
|
||||
# All paths resolve against the config TARGET (set by 05-create-user).
|
||||
ZSH="${ZSH:-$TARGET_HOME/.oh-my-zsh}"
|
||||
ZSH="${ZSH:-$HOME/.oh-my-zsh}"
|
||||
ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
|
||||
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$TARGET_HOME/.config/zsh}"
|
||||
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$HOME/.config/zsh}"
|
||||
|
||||
run_zsh() {
|
||||
log "installing zsh + oh-my-zsh"
|
||||
_install_zsh
|
||||
_install_omz
|
||||
_install_plugins
|
||||
_chown_target_tree
|
||||
_ship_config
|
||||
_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"
|
||||
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
|
||||
ok "zsh ready — start it with: exec zsh"
|
||||
}
|
||||
|
||||
_install_zsh() {
|
||||
@ -68,6 +55,7 @@ _ship_config() {
|
||||
log "shipping zsh config to $ZDOTDIR_TARGET"
|
||||
mkdir -p "$ZDOTDIR_TARGET"
|
||||
local f
|
||||
local f
|
||||
for f in zshrc aliases.zsh functions.zsh path.zsh plugins.zsh; do
|
||||
local src="$BOOT_DIR/config/zsh/$f"
|
||||
[[ -f "$src" ]] || continue
|
||||
@ -79,8 +67,8 @@ _ship_config() {
|
||||
if [[ ! -f "$ZDOTDIR_TARGET/aliases-extras.zsh" ]]; then
|
||||
cp "$BOOT_DIR/config/zsh/aliases-extras.zsh" "$ZDOTDIR_TARGET/aliases-extras.zsh"
|
||||
fi
|
||||
# ~/.zshrc shim that loads our real config via ZDOTDIR
|
||||
local shim="$TARGET_HOME/.zshrc"
|
||||
# ~/.zshrc shim that loads our real config via ZDOTDIR (so omz $ZSH + plugins resolve)
|
||||
local shim="$HOME/.zshrc"
|
||||
if ! grep -q 'BOOTSTRAP ZDOTDIR' "$shim" 2>/dev/null; then
|
||||
cat >> "$shim" <<'EOF'
|
||||
|
||||
@ -89,11 +77,7 @@ export ZDOTDIR="${ZDOTDIR:-$HOME/.config/zsh}"
|
||||
[[ -f "$ZDOTDIR/zshrc" ]] && source "$ZDOTDIR/zshrc"
|
||||
EOF
|
||||
fi
|
||||
# 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"
|
||||
ok "zsh config installed ($ZDOTDIR_TARGET) + ~/.zshrc shim"
|
||||
}
|
||||
|
||||
_set_default_shell() {
|
||||
@ -103,13 +87,13 @@ _set_default_shell() {
|
||||
if ! grep -qx "$zsh_bin" /etc/shells 2>/dev/null; then
|
||||
echo "$zsh_bin" | sudo_ tee -a /etc/shells >/dev/null
|
||||
fi
|
||||
if [[ "$(getent passwd "$TARGET_USER" | cut -d: -f7)" != "$zsh_bin" ]]; then
|
||||
if yn "Set zsh as default login shell for $TARGET_USER?" y; then
|
||||
sudo_ chsh -s "$zsh_bin" "$TARGET_USER" && ok "default shell set to zsh for $TARGET_USER"
|
||||
if [[ "$SHELL" != "$zsh_bin" ]] && [[ "$(getent passwd "$USER" | cut -d: -f7)" != "$zsh_bin" ]]; then
|
||||
if yn "Set zsh as default login shell for $USER?" y; then
|
||||
sudo_ chsh -s "$zsh_bin" "$USER" && ok "default shell set to zsh"
|
||||
else
|
||||
warn "zsh not set as default — run 'chsh -s $zsh_bin' for $TARGET_USER' later"
|
||||
warn "zsh not set as default — run 'chsh -s $zsh_bin' later"
|
||||
fi
|
||||
else
|
||||
ok "zsh already the default shell for $TARGET_USER"
|
||||
ok "zsh already your default shell"
|
||||
fi
|
||||
}
|
||||
@ -2,25 +2,30 @@
|
||||
# 35-nvm — Node Version Manager + Node LTS.
|
||||
|
||||
step_name="nvm"
|
||||
step_desc="nvm into ~$TARGET_USER/.nvm, install Node LTS"
|
||||
step_desc="nvm into ~/.nvm, install Node LTS"
|
||||
step_run="run_nvm"
|
||||
|
||||
run_nvm() {
|
||||
log "installing nvm for $TARGET_USER"
|
||||
local nvm_home="$TARGET_HOME/.nvm"
|
||||
export NVM_DIR="$nvm_home"
|
||||
|
||||
log "installing nvm"
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||
ok "nvm already present at $NVM_DIR"
|
||||
else
|
||||
# fetch the install script at a pinned NVM version (update here to upgrade)
|
||||
as_user env NVM_DIR="$nvm_home" bash -c 'curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash' \
|
||||
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \
|
||||
|| { warn "nvm install failed"; return; }
|
||||
ok "nvm installed to $NVM_DIR"
|
||||
fi
|
||||
|
||||
# run node/lts install as the target user so binary ownership + default are theirs
|
||||
as_user bash -lc 'export NVM_DIR='"$NVM_DIR"'; source "$NVM_DIR/nvm.sh"; nvm install --lts && nvm use --lts && nvm alias default lts/*' \
|
||||
|| warn "node lts install had issues (continue; run 'nvm install --lts' as $TARGET_USER)"
|
||||
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
|
||||
# shellcheck disable=SC1091
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
if command -v nvm >/dev/null 2>&1; then
|
||||
log "installing Node LTS via nvm"
|
||||
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
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# 40-nvim — clone your nvim-minimal config from gitea to ~$TARGET_USER/.config/nvim.
|
||||
# 40-nvim — clone your nvim-minimal config from gitea to ~/.config/nvim.
|
||||
|
||||
step_name="nvim"
|
||||
step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~$TARGET_USER/.config/nvim"
|
||||
step_desc="clone g.mk.fo/bootstrap/nvim-minimal -> ~/.config/nvim"
|
||||
step_run="run_nvim"
|
||||
|
||||
NVIM_REPO_URL="${NVIM_REPO_URL:-https://g.mk.fo/bootstrap/nvim-minimal.git}"
|
||||
@ -11,22 +11,24 @@ run_nvim() {
|
||||
log "installing nvim config"
|
||||
command -v nvim >/dev/null 2>&1 || warn "nvim binary not found — install the packages step first"
|
||||
|
||||
local target="$TARGET_HOME/.config/nvim"
|
||||
local target="${XDG_CONFIG_HOME:-$HOME/.config}/nvim"
|
||||
if [[ -d "$target/.git" ]]; then
|
||||
log "existing nvim config is a git repo — pulling"
|
||||
as_user git -C "$target" pull --ff-only || warn "could not pull existing nvim config"
|
||||
git -C "$target" pull --ff-only || warn "could not pull existing nvim config"
|
||||
ok "nvim config updated at $target"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -e "$target" ]]; then
|
||||
local bak="${target}.bak.$(date +%Y%m%d-%H%M%S)"
|
||||
as_user mv "$target" "$bak" 2>/dev/null || sudo_ mv "$target" "$bak"
|
||||
mv "$target" "$bak"
|
||||
warn "backed up existing $target -> $bak"
|
||||
fi
|
||||
|
||||
as_user git clone "$NVIM_REPO_URL" "$target" && ok "nvim config cloned to $target" \
|
||||
|| { err "failed to clone $NVIM_REPO_URL"; sudo_ git clone "$NVIM_REPO_URL" "$target" \
|
||||
&& chown -R "$TARGET_USER:$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")" "$target" \
|
||||
&& ok "nvim config cloned to $target (as root, then chowned)"; }
|
||||
if git clone "$NVIM_REPO_URL" "$target"; then
|
||||
ok "nvim config cloned to $target"
|
||||
else
|
||||
err "failed to clone $NVIM_REPO_URL"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
# 50-hardening — system defaults. Each sub-step y/N, all idempotent.
|
||||
|
||||
step_name="hardening"
|
||||
step_desc="ssh, ufw, updates, unattended, tz, hostname, swap, locale, fail2ban, angie config + built-in ACME"
|
||||
step_desc="ssh(ufw-safe), ufw, updates, unattended, tz, hostname, swap, locale, fail2ban"
|
||||
step_run="run_hardening"
|
||||
|
||||
run_hardening() {
|
||||
@ -14,8 +14,6 @@ run_hardening() {
|
||||
_h_swap
|
||||
_h_locale
|
||||
_h_fail2ban
|
||||
_h_angie_config
|
||||
_h_angie_acme
|
||||
ok "hardening pass complete"
|
||||
}
|
||||
|
||||
@ -105,12 +103,8 @@ _h_tz_hostname() {
|
||||
if command -v timedatectl >/dev/null 2>&1; then
|
||||
if yn "Set timezone (default UTC)?" y; then
|
||||
local tz="${REPLY_TZ:-}"
|
||||
if (( ${ALL:-0} )); then
|
||||
tz="UTC"; ok "--yes: timezone defaulting to UTC"
|
||||
else
|
||||
read -rp " timezone [UTC]: " tz
|
||||
[[ -z "$tz" ]] && tz="UTC"
|
||||
fi
|
||||
read -rp " timezone [UTC]: " tz
|
||||
[[ -z "$tz" ]] && tz="UTC"
|
||||
sudo_ timedatectl set-timezone "$tz" && ok "timezone set to $tz"
|
||||
fi
|
||||
else
|
||||
@ -191,267 +185,4 @@ EOF
|
||||
sudo_ systemctl enable --now fail2ban 2>/dev/null || warn "could not enable fail2ban"
|
||||
fi
|
||||
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 ──
|
||||
# Angie packages set user/pid/error_log differently per distro; preserve them.
|
||||
local oldcfg="$d/angie.conf" angie_user angie_pid angie_err angie_modules=""
|
||||
if [[ -f "$oldcfg" ]]; then
|
||||
angie_user=$(grep -E '^\s*user\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
||||
angie_pid=$(grep -E '^\s*pid\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
||||
angie_err=$(grep -E '^\s*error_log\s+' "$oldcfg" | head -1 | awk '{print $2}' | tr -d ';')
|
||||
# collect any load_module lines (dynamic modules)
|
||||
angie_modules=$(grep -E '^\s*load_module\s+' "$oldcfg" || 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
|
||||
|
||||
# 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 /etc/angie/mime.types;
|
||||
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
|
||||
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 default_server;
|
||||
listen [::]:443 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ── angie-enable / angie-disable helpers ──
|
||||
_angie_helpers
|
||||
|
||||
# validate + (best-effort) reload
|
||||
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_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."
|
||||
}
|
||||
32
setup.sh
32
setup.sh
@ -4,20 +4,13 @@
|
||||
# Detects distro/arch, lists modular steps, asks y/N per step, runs them.
|
||||
#
|
||||
# Flags:
|
||||
# --yes run all steps without prompting
|
||||
# --only NAME run only the step matching NAME
|
||||
# --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
|
||||
# --yes run all steps without prompting
|
||||
# --only NAME run only the step matching NAME
|
||||
# --skip NAME run all steps except NAME (repeatable in future)
|
||||
# -h|--help usage
|
||||
|
||||
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)"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
@ -25,25 +18,18 @@ source "$BOOT_DIR/lib/detect.sh"
|
||||
source "$BOOT_DIR/lib/log.sh"
|
||||
source "$BOOT_DIR/lib/prompt.sh"
|
||||
|
||||
# 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=""
|
||||
ALL=0; ONLY=""; SKIP=""
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--yes|-y) ALL=1; shift ;;
|
||||
--only) ONLY="${2:-}"; shift 2 ;;
|
||||
--skip) SKIP="${2:-}"; shift 2 ;;
|
||||
--create-user) CREATE_USER=1; shift ;;
|
||||
--username) USERNAME_ARG="${2:-}"; shift 2 ;;
|
||||
--yes|-y) ALL=1; shift ;;
|
||||
--only) ONLY="${2:-}"; shift 2 ;;
|
||||
--skip) SKIP="${2:-}"; shift 2 ;;
|
||||
-h|--help)
|
||||
grep -E '^# --' "$0" | sed 's/^# //'
|
||||
echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]"
|
||||
exit 0 ;;
|
||||
*) echo "unknown arg: $1" >&2; shift ;;
|
||||
esac
|
||||
done
|
||||
export CREATE_USER USERNAME_ARG
|
||||
|
||||
# ---------------------------------------------------------------- collect steps
|
||||
NAMES=(); DESCS=(); RUNS=()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user