Compare commits

...

10 Commits

16 changed files with 921 additions and 55 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.bak.*

178
README.md Normal file
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.

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
config/zsh/aliases.zsh Normal file
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
config/zsh/functions.zsh Normal file
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
config/zsh/path.zsh Normal file
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
config/zsh/plugins.zsh Normal file
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
config/zsh/zshrc Normal file
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

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
modules/05-create-user.sh Executable file
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"
}

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
} }

View File

@ -5,18 +5,31 @@ 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="${ZSH:-$TARGET_HOME/.oh-my-zsh}"
ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}" ZSH_CUSTOM="${ZSH_CUSTOM:-$ZSH/custom}"
ZDOTDIR_TARGET="${ZDOTDIR_TARGET:-$HOME/.config/zsh}" ZDOTDIR_TARGET="${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() {
@ -55,7 +68,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 +79,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 +89,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 +103,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
} }

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
} }

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
} }

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,266 @@ 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 ──
# 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."
}

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=()