#!/bin/bash # lab/host-setup/mini-baseline.sh # # Baseline for an SB on-perimeter Mac (mini or Studio). After Setup Assistant, in Terminal on the Mac: # # curl -fsSL https://mac.sblabs.ca | bash # # It asks for the Mac's name, then: SB admin SSH keys, Remote Login + Screen Sharing, never sleep, # restart after power loss, firewall + stealth, time zone, Homebrew + Tailscale (shows a QR code to join # the tailnet). Password SSH stays on. Key-only SSH is a second pass, run once a key login works from # another machine: # # curl -fsSL https://mac.sblabs.ca | bash -s -- --lockdown-ssh --confirmed-key-login # # Implements lab/projects/sb-on-perimeter/RUNBOOK-MINIS-DAY1.md section 2 (items 4-7, 9) and the # Tailscale install from section 4. FileVault is NOT touched: turn it on in Setup Assistant and save # the recovery key first. # # Options: # --hostname NAME ComputerName / HostName / LocalHostName (asked for on a terminal if omitted) # --key 'PUBKEY' authorize an extra public key (repeatable); the SB admin keys are always added # --key-file FILE extra public keys, one per line # --tz ZONE default America/Whitehorse (BC is permanently UTC-7 since 2026-11-01) # --skip-tailscale skip Homebrew + Tailscale # --lockdown-ssh --confirmed-key-login pass 2: key-only SSH # --dry-run print the changes instead of making them # # Remote runs with no terminal: set SUDO_ASKPASS to a helper that prints the password. # Idempotent. Paste the summary block into the Mac's SETUP-LOG.md. # The SB admin keys (public keys; the laptop key is also published at github.com/troymcginnis.keys). SB_KEYS=( 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMzPYd3xLkOhRjI7qwbnZRL7TDmxeBOHF6GytDYerkGT troymcginnis@sbtm01' 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCtZopUziPy/PmK9Zs+Xz3wfHEHHcYYP99KRCGZ9cSvfapYdHjT9PcBOHq059TPSdJzgJTTNSBVtxcemDtUzP64jLuAOfGk1I6qwcRQMIOqmZoBykvhlyOvBrWkEVUWMZPhC6JEaBx9T6v/YYBQpjhKXDIGQ9Q6IHr8rGhYmbIRUsJmLz/okoBWV5klILV+8nycBEOVdZuNGK2vNqktIBa/TpPTMHgLPEQlTvU23PKZtD2SEFj08NkgM1bJGwW8Sw5WAf1hroEmEge06qEwDMxkI6rq1qAJFhj6sNmDzHlSIVVYpsGXy1isrYc6hlf/AScq7RZApjQmmatwk/dhLp8MsZogOvcbOJ4WMSt7h6sVjUchucznFjV6CZj2MO2LiJJRXikMYlcal+wCH9+PtNN4bwLlHXXQT3XUKisvLlt0E0XbpanA4DthIpJrvsNSOWjZ77u+7pcIdVWh+uZBIaKtBGcn7rNbTlG4Waime04CFi0sSn7lgMbb6NZkn7duaFc= troymcginnis@Troys-Laptop.local' ) # Everything runs inside main(), so a half-downloaded `curl | bash` never runs a partial script. main() { set -uo pipefail local DRY=0 SKIP_TS=0 LOCKDOWN=0 CONFIRMED=0 local TZ_NAME="America/Whitehorse" HOST_NAME="" local KEYS=("${SB_KEYS[@]}") FAILED=() while [ $# -gt 0 ]; do case "$1" in --dry-run) DRY=1 ;; --key) shift; KEYS+=("${1:?--key needs a value}") ;; --key-file) shift; local f="${1:?--key-file needs a path}" line [ -f "$f" ] || { echo "No such file: $f" >&2; return 2; } while IFS= read -r line; do case "$line" in ssh-*|ecdsa-*|sk-*) KEYS+=("$line") ;; esac done < "$f" ;; --hostname) shift; HOST_NAME="${1:?--hostname needs a value}" ;; --tz) shift; TZ_NAME="${1:?--tz needs a value}" ;; --skip-tailscale) SKIP_TS=1 ;; --lockdown-ssh) LOCKDOWN=1 ;; --confirmed-key-login) CONFIRMED=1 ;; -h|--help) echo "See the comments at the top of lab/host-setup/mini-baseline.sh (or https://mac.sblabs.ca)."; return 0 ;; *) echo "Unknown option: $1" >&2; return 2 ;; esac shift done [ "$(uname -s)" = "Darwin" ] || { echo "macOS only." >&2; return 2; } [ "$(id -u)" -ne 0 ] || { echo "Run as the admin user (it calls sudo itself), not as root." >&2; return 2; } if [ "$LOCKDOWN" = 1 ] && [ "$CONFIRMED" != 1 ]; then echo "--lockdown-ssh needs --confirmed-key-login. Log in from another machine with your key first." >&2 return 2 fi if [ "$LOCKDOWN" = 0 ] && [ -z "$HOST_NAME" ] && [ -t 0 ]; then read -r -p "Name for this Mac (e.g. sb003): " HOST_NAME fi if [ -n "$HOST_NAME" ] && ! printf '%s' "$HOST_NAME" | grep -Eq '^[a-z0-9][a-z0-9-]{0,62}$'; then echo "Use lowercase letters, digits and dashes for the name (e.g. sb003)." >&2; return 2 fi [ "$DRY" = 1 ] || sudo -v || { echo "sudo failed." >&2; return 2; } # Keep sudo fresh on a terminal: the Homebrew install can outlast sudo's 5-minute cache. if [ "$DRY" = 0 ] && [ -z "${SUDO_ASKPASS:-}" ]; then ( while sleep 50; do kill -0 "$$" 2>/dev/null || exit; command sudo -n true 2>/dev/null; done ) & trap 'kill $! 2>/dev/null' EXIT fi if [ "$LOCKDOWN" = 1 ]; then lockdown_ssh "$DRY"; return $?; fi say "Computer name" if [ -n "$HOST_NAME" ]; then local k for k in ComputerName HostName LocalHostName; do run sudo scutil --set "$k" "$HOST_NAME"; done else note "skipped (no name given)" fi say "SSH keys for $(id -un)" run mkdir -p "$HOME/.ssh"; run chmod 700 "$HOME/.ssh" [ "$DRY" = 1 ] || touch "$HOME/.ssh/authorized_keys" local key for key in "${KEYS[@]}"; do if [ "$DRY" = 1 ]; then note "[dry-run] would authorize: ${key:0:30}... ${key##* }" elif grep -qxF "$key" "$HOME/.ssh/authorized_keys"; then note "already there: ${key##* }" else printf '%s\n' "$key" >> "$HOME/.ssh/authorized_keys"; note "added: ${key##* }" fi done run chmod 600 "$HOME/.ssh/authorized_keys" say "Remote Login (SSH) and Screen Sharing" if [ "$DRY" = 1 ]; then note "[dry-run] would turn on Remote Login and Screen Sharing if they're off" else if ! listening 22; then sudo systemsetup -setremotelogin on >/dev/null 2>&1 \ || { sudo launchctl enable system/com.openssh.sshd; sudo launchctl bootstrap system /System/Library/LaunchDaemons/ssh.plist; } >/dev/null 2>&1 fi if listening 22; then note "Remote Login: on" else FAILED+=("Remote Login"); note "Remote Login is OFF: System Settings > General > Sharing > Remote Login"; fi if ! listening 5900; then { sudo launchctl enable system/com.apple.screensharing; sudo launchctl bootstrap system /System/Library/LaunchDaemons/com.apple.screensharing.plist; } >/dev/null 2>&1 fi if listening 5900; then note "Screen Sharing: on" else FAILED+=("Screen Sharing"); note "Screen Sharing is OFF: System Settings > General > Sharing > Screen Sharing"; fi fi say "Power and sleep (headless server)" run sudo pmset -a sleep 0 displaysleep 0 disksleep 0 powernap 0 run sudo pmset -a autorestart 1 say "Firewall on, stealth on, block-all off" local FW=/usr/libexec/ApplicationFirewall/socketfilterfw run sudo "$FW" --setglobalstate on run sudo "$FW" --setstealthmode on run sudo "$FW" --setblockall off say "Auto-login off" if [ "$DRY" = 1 ]; then note "[dry-run] would delete autoLoginUser if set" elif sudo defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser >/dev/null 2>&1; then run sudo defaults delete /Library/Preferences/com.apple.loginwindow autoLoginUser else note "already off" fi say "Time zone $TZ_NAME + network time" if [ "$DRY" = 1 ]; then note "[dry-run] sudo systemsetup -settimezone $TZ_NAME; -setusingnetworktime on" else sudo systemsetup -settimezone "$TZ_NAME" >/dev/null 2>&1 \ || note "systemsetup refused the time zone (needs Full Disk Access). Set it in System Settings > General > Date & Time." sudo systemsetup -setusingnetworktime on >/dev/null 2>&1 || true fi if [ "$SKIP_TS" = 1 ]; then say "Tailscale"; note "skipped (--skip-tailscale)" else say "Homebrew + Tailscale system daemon" local BREW=/opt/homebrew/bin/brew TS=/opt/homebrew/bin/tailscale if [ -x "$BREW" ]; then note "Homebrew present" elif [ "$DRY" = 1 ]; then note "[dry-run] would install Homebrew (official installer, NONINTERACTIVE=1)" else NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" < /dev/null \ || FAILED+=("homebrew install") fi if [ "$DRY" = 1 ]; then note "[dry-run] brew install tailscale; sudo brew services start tailscale" elif [ -x "$BREW" ]; then grep -qs 'brew shellenv' "$HOME/.zprofile" || echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile" "$BREW" list tailscale >/dev/null 2>&1 || "$BREW" install tailscale < /dev/null || FAILED+=("brew install tailscale") sudo "$BREW" services start tailscale >/dev/null || FAILED+=("tailscale service start") fi if [ "$DRY" = 0 ] && [ -x "$TS" ] && [ -t 0 ] && ! "$TS" ip -4 >/dev/null 2>&1; then local i; for i in 1 2 3 4 5 6 7 8 9 10; do [ -S /var/run/tailscaled.socket ] && break; sleep 1; done note "Join the tailnet: scan this QR code with your phone and sign in to SB's Tailscale." sudo "$TS" up --hostname="${HOST_NAME:-$(scutil --get LocalHostName)}" --qr --timeout=10m || FAILED+=("tailscale up") elif [ "$DRY" = 0 ] && [ -x "$TS" ] && "$TS" ip -4 >/dev/null 2>&1; then note "already on the tailnet: $("$TS" ip -4 2>/dev/null)" else note "NEXT: sudo /opt/homebrew/bin/tailscale up --hostname=${HOST_NAME:-} --qr" fi note "In the Tailscale admin console: disable key expiry for this machine. No serve/Funnel." fi say "Verify (paste into SETUP-LOG.md)" if [ "$DRY" = 1 ]; then note "[dry-run] would print FileVault, power, firewall, sshd, sharing and Tailscale state" else summary fi if [ "${#FAILED[@]}" -gt 0 ]; then printf '\nFAILED steps:\n'; printf ' - %s\n' "${FAILED[@]}"; return 1 fi echo echo "Done. Next, from another machine: ssh $(id -un)@ with your key." echo "Then key-only SSH: curl -fsSL https://mac.sblabs.ca | bash -s -- --lockdown-ssh --confirmed-key-login" } say() { printf '\n== %s\n' "$*"; } note() { printf ' %s\n' "$*"; } listening() { nc -z -G 2 127.0.0.1 "$1" >/dev/null 2>&1; } # run: print in dry-run, execute otherwise; record failures but keep going. run() { if [ "$DRY" = 1 ]; then printf ' [dry-run] %s\n' "$*"; return 0; fi if ! "$@"; then FAILED+=("$*"); printf ' FAILED: %s\n' "$*" >&2; return 1; fi } # With SUDO_ASKPASS set (a remote run with no terminal), every sudo asks that helper for the password. # Homebrew's installer honours SUDO_ASKPASS on its own. sudo() { if [ -n "${SUDO_ASKPASS:-}" ]; then command sudo -A "$@"; else command sudo "$@"; fi; } # Pass 2: key-only sshd. macOS starts a fresh sshd per connection, so new logins use it at once; # nothing is restarted and the current session stays up. lockdown_ssh() { local DRY="$1" DROPIN=/etc/ssh/sshd_config.d/10-sb.conf say "Key-only SSH (pass 2)" if ! grep -q '^Include /etc/ssh/sshd_config.d' /etc/ssh/sshd_config 2>/dev/null; then echo "/etc/ssh/sshd_config has no sshd_config.d Include; not touching sshd." >&2; return 1 fi if [ "$DRY" = 1 ]; then note "[dry-run] would write $DROPIN (PasswordAuthentication no, KbdInteractiveAuthentication no, PermitRootLogin no) and check it with sshd -t" return 0 fi sudo mkdir -p /etc/ssh/sshd_config.d printf '%s\n' 'PasswordAuthentication no' 'KbdInteractiveAuthentication no' 'PermitRootLogin no' \ | sudo tee "$DROPIN" >/dev/null if ! sudo sshd -t; then sudo rm -f "$DROPIN" echo "sshd -t rejected the drop-in; removed it. Password SSH unchanged." >&2; return 1 fi sudo sshd -T 2>/dev/null | grep -iE '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin) ' note "Keep this session open; confirm a NEW ssh login with your key works before closing it." } summary() { local FW=/usr/libexec/ApplicationFirewall/socketfilterfw TS=/opt/homebrew/bin/tailscale echo '```' echo "date: $(date '+%Y-%m-%d %H:%M %Z')" echo "host: $(scutil --get LocalHostName 2>/dev/null) | $(sw_vers -productName) $(sw_vers -productVersion) ($(sw_vers -buildVersion))" echo "hardware: $(system_profiler SPHardwareDataType 2>/dev/null | grep -E 'Model Name|Chip|Memory' | sed 's/^ *//' | tr '\n' ' ')" echo "filevault: $(fdesetup status 2>&1 | head -1)" echo "firewall: $(sudo "$FW" --getglobalstate 2>&1 | head -1) | $(sudo "$FW" --getstealthmode 2>&1 | head -1)" echo "pmset: $(pmset -g | grep -E '^ *(sleep|displaysleep|disksleep|powernap|autorestart) ' | sed 's/^ *//;s/ */ /g' | tr '\n' ';')" echo "autologin: $(sudo defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null || echo off)" echo "timezone: $(sudo systemsetup -gettimezone 2>/dev/null | sed 's/Time Zone: //')" echo "sharing: ssh $(listening 22 && echo on || echo OFF) | screen sharing $(listening 5900 && echo on || echo OFF)" echo "sshd: $(sudo sshd -T 2>/dev/null | grep -iE '^(passwordauthentication|permitrootlogin) ' | tr '\n' ';')" echo "tailscale: $(pgrep -x tailscaled >/dev/null && echo 'daemon running' || echo 'daemon NOT running') | $([ -x "$TS" ] && "$TS" ip -4 2>/dev/null || echo 'not joined')" echo "lan-ip: $(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo unknown) (FileVault SSH unlock is LAN-only)" echo '```' } # Read prompts from the terminal even under `curl | bash`; without one (a remote run), keep stdin as is. if (exec /dev/null; then main "$@"