device-code onboarding: bootstrap auto-installs deps + netbird; Authentik device flow wired
Two changes that together collapse new-workstation onboarding from ~7 steps
to ~2 commands:
* bootstrap.sh:
- Dep-check now AUTO-INSTALLS missing tools (apt/dnf/brew) instead of
printing instructions and exiting. Re-verifies after install.
- New pre-mesh-check block: if netbird isn't installed, installs it
from the netbird apt/dnf repo (or `brew install --cask netbird` on
Darwin), then if mgmt isn't connected, runs `netbird up
--management-url=https://netbird.hubris.network --ssh-jwt-cache-ttl 86400`.
Operator clicks the device-code URL inline. Waits up to ~30s for
Management: Connected before continuing. Skipped on --no-secrets +
--dry-run.
* containers/124-authentik.md: replaces the "KNOWN MISSING — Device Code
Stage" subsection with a working recipe — Authentik 2026.2 routes
/device via a BRAND-level "Device code flow" field, not a provider
field. Documented stage bindings for a `default-device-code-flow`
flow (identification → password → user-login → consent) and the
brand-level binding step.
* operations/agent-enrollment.md: Path B (interactive `netbird up`) is
now the default; Path A (setup-key) demoted to "headless/scripted"
alternative. "Install dependencies" section collapsed into a note
that bootstrap handles it, with the manual recipes kept in a
collapsible <details> block for air-gapped use.
The flow uniquely available to lab owners (single Authentik user today)
still relies on the first-time-owner sqlite promotion documented in
124-authentik.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
bootstrap.sh
126
bootstrap.sh
@@ -114,32 +114,110 @@ if [ "$NO_SECRETS" -eq 0 ]; then
|
||||
for cmd in age sops; do command -v "$cmd" >/dev/null || missing+=("$cmd"); done
|
||||
fi
|
||||
if [ "${#missing[@]}" -gt 0 ]; then
|
||||
echo "missing required tools: ${missing[*]}" >&2
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
brew_list=()
|
||||
for m in "${missing[@]}"; do
|
||||
case "$m" in
|
||||
python3-yaml) echo " pip3 install pyyaml (or brew install pyyaml)" >&2 ;;
|
||||
*) brew_list+=("$m") ;;
|
||||
esac
|
||||
done
|
||||
[ "${#brew_list[@]}" -gt 0 ] && echo " brew install ${brew_list[*]}" >&2
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
# Fedora/RHEL/Nobara: python yaml package is python3-pyyaml.
|
||||
dnf_list=()
|
||||
for m in "${missing[@]}"; do
|
||||
case "$m" in
|
||||
python3-yaml) dnf_list+=("python3-pyyaml") ;;
|
||||
*) dnf_list+=("$m") ;;
|
||||
esac
|
||||
done
|
||||
echo " sudo dnf install -y ${dnf_list[*]}" >&2
|
||||
elif command -v apt >/dev/null 2>&1; then
|
||||
echo " sudo apt install -y ${missing[*]}" >&2
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "+ would install missing tools: ${missing[*]}"
|
||||
else
|
||||
echo " install with your package manager: ${missing[*]}" >&2
|
||||
echo "[bootstrap] installing missing tools: ${missing[*]}"
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
brew_list=()
|
||||
for m in "${missing[@]}"; do
|
||||
case "$m" in
|
||||
python3-yaml) python3 -m pip install --break-system-packages pyyaml >/dev/null 2>&1 \
|
||||
|| python3 -m pip install pyyaml ;;
|
||||
*) brew_list+=("$m") ;;
|
||||
esac
|
||||
done
|
||||
[ "${#brew_list[@]}" -gt 0 ] && brew install "${brew_list[@]}"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf_list=()
|
||||
for m in "${missing[@]}"; do
|
||||
case "$m" in
|
||||
python3-yaml) dnf_list+=("python3-pyyaml") ;;
|
||||
*) dnf_list+=("$m") ;;
|
||||
esac
|
||||
done
|
||||
dnf install -y "${dnf_list[@]}"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y "${missing[@]}"
|
||||
else
|
||||
echo "[bootstrap] no supported package manager for: ${missing[*]}" >&2
|
||||
echo "[bootstrap] install with your package manager + re-run" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Re-verify (especially python yaml — the rename is the most common gotcha).
|
||||
for cmd in git python3; do
|
||||
command -v "$cmd" >/dev/null || { echo "[bootstrap] post-install $cmd still missing" >&2; exit 1; }
|
||||
done
|
||||
python3 -c "import yaml" 2>/dev/null \
|
||||
|| { echo "[bootstrap] post-install python3-yaml/pyyaml still missing" >&2; exit 1; }
|
||||
if [ "$NO_SECRETS" -eq 0 ]; then
|
||||
for cmd in age sops; do
|
||||
command -v "$cmd" >/dev/null || { echo "[bootstrap] post-install $cmd still missing" >&2; exit 1; }
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# -------- ensure netbird is installed + connected (workstation/VM hosts) --------
|
||||
# Skipped on --no-secrets (LXCs that route via the LAN already) and --dry-run.
|
||||
# Installs netbird if missing, then drives `netbird up` against the homelab
|
||||
# management server. The operator clicks the printed device-code URL once.
|
||||
if [ "$NO_SECRETS" -eq 0 ] && [ "$DRY_RUN" -eq 0 ]; then
|
||||
if ! command -v netbird >/dev/null 2>&1 && ! command -v tailscale >/dev/null 2>&1; then
|
||||
echo "[bootstrap] no mesh CLI found; installing netbird..."
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
brew install --cask netbird || { echo "[bootstrap] brew install netbird failed" >&2; exit 1; }
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
cat > /etc/yum.repos.d/netbird.repo <<'NBREPO'
|
||||
[netbird]
|
||||
name=NetBird stable
|
||||
baseurl=https://pkgs.netbird.io/yum/$basearch
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
NBREPO
|
||||
dnf install -y netbird netbird-ui || { echo "[bootstrap] dnf install netbird failed" >&2; exit 1; }
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
install -d -m 0755 /usr/share/keyrings
|
||||
curl -fsSL https://pkgs.netbird.io/debian/public.key \
|
||||
| gpg --dearmor -o /usr/share/keyrings/netbird-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main" \
|
||||
> /etc/apt/sources.list.d/netbird.list
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y netbird \
|
||||
|| { echo "[bootstrap] apt install netbird failed" >&2; exit 1; }
|
||||
else
|
||||
echo "[bootstrap] can't auto-install netbird on this OS; install manually + re-run" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Bring netbird up if not already connected.
|
||||
if command -v netbird >/dev/null && ! netbird status 2>/dev/null | grep -q "Management: Connected"; then
|
||||
cat <<MSG
|
||||
|
||||
[bootstrap] netbird is not connected to https://netbird.hubris.network.
|
||||
[bootstrap] running 'netbird up' — a device-code URL will print below.
|
||||
[bootstrap] OPEN THAT URL in a browser and approve the device when prompted.
|
||||
[bootstrap] You may need to log in to https://auth.hubris.network first.
|
||||
|
||||
MSG
|
||||
# --ssh-jwt-cache-ttl=86400 keeps the SSO valid for 24h of subsequent ssh
|
||||
# ops into mesh peers; saves repeated browser clicks during this bootstrap.
|
||||
netbird up --management-url https://netbird.hubris.network --ssh-jwt-cache-ttl 86400 \
|
||||
|| { echo "[bootstrap] 'netbird up' failed (see error above)" >&2; exit 1; }
|
||||
# `netbird up` returns once the device-code SSO completes; give the
|
||||
# mgmt connection ~30s to settle before continuing.
|
||||
for _ in $(seq 1 10); do
|
||||
netbird status 2>/dev/null | grep -q "Management: Connected" && break
|
||||
sleep 3
|
||||
done
|
||||
if ! netbird status 2>/dev/null | grep -q "Management: Connected"; then
|
||||
echo "[bootstrap] netbird daemon not reporting Management: Connected after 30s" >&2
|
||||
echo "[bootstrap] try: 'netbird status -d' and 'sudo journalctl -u netbird -n 30'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[bootstrap] netbird connected."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mesh check — accept Netbird, Tailscale, or LAN reachability of the issuance
|
||||
|
||||
Reference in New Issue
Block a user