Initial commit: VaultMesh Skills collection
Collection of operational skills for VaultMesh infrastructure including: - backup-sovereign: Backup and recovery operations - btc-anchor: Bitcoin anchoring - cloudflare-tunnel-manager: Cloudflare tunnel management - container-registry: Container registry operations - disaster-recovery: Disaster recovery procedures - dns-sovereign: DNS management - eth-anchor: Ethereum anchoring - gitea-bootstrap: Gitea setup and configuration - hetzner-bootstrap: Hetzner server provisioning - merkle-forest: Merkle tree operations - node-hardening: Node security hardening - operator-bootstrap: Operator initialization - proof-verifier: Cryptographic proof verification - rfc3161-anchor: RFC3161 timestamping - secrets-vault: Secrets management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
7
hetzner-bootstrap/.gitignore
vendored
Normal file
7
hetzner-bootstrap/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
*.zip
|
||||
outputs/
|
||||
*.key
|
||||
*.pem
|
||||
*.env
|
||||
*secret*
|
||||
87
hetzner-bootstrap/SKILL.md
Normal file
87
hetzner-bootstrap/SKILL.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: hetzner-bootstrap
|
||||
description: >
|
||||
Bootstrap a Hetzner-hosted Ubuntu/Debian node for sovereign operations:
|
||||
base packages, sovereign user, hostname, UFW, SSH hardening (reload-safe),
|
||||
cloudflared install, and WireGuard scaffold. Plan/apply/rollback with DRY_RUN.
|
||||
Triggers: 'bootstrap hetzner', 'server prep', 'hetzner node a', 'wireguard setup',
|
||||
'install cloudflared', 'ufw + ssh hardening'.
|
||||
version: 1.1.0
|
||||
---
|
||||
|
||||
# Hetzner Bootstrap (Node A)
|
||||
|
||||
This skill turns a fresh Hetzner server into a VaultMesh-ready node using the
|
||||
exact safe sequence you specified:
|
||||
|
||||
- Update + install dependencies
|
||||
- Install **cloudflared** (Cloudflare repo)
|
||||
- Create `sovereign` user + SSH authorized key
|
||||
- Set hostname
|
||||
- Configure UFW (WireGuard port opened **before** enable)
|
||||
- Harden SSH (disable root + passwords) using **reload** (not restart)
|
||||
- Scaffold WireGuard keys + `wg0.conf`
|
||||
|
||||
## Safety model
|
||||
- **DRY_RUN=1** by default; apply scripts refuse unless `DRY_RUN=0`.
|
||||
- **CONFIRM_PHRASE** required for apply steps.
|
||||
- SSH changes use `sshd -t` validation and `systemctl reload` to avoid session loss.
|
||||
- WireGuard private key is root-owned and `0600`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run as **root** on the server:
|
||||
|
||||
```bash
|
||||
cd ~/.codex/skills/hetzner-bootstrap # or ~/.codex/skills/hetzner-bootstrap # or ~/.claude/skills/hetzner-bootstrap
|
||||
|
||||
export SERVER_IP="46.224.119.129"
|
||||
export NODE_NAME="vm-de-op"
|
||||
export SOVEREIGN_USER="sovereign"
|
||||
export SSH_PUBLIC_KEY="ssh-ed25519 AAAA... hetzner-sovereign-YYYYMMDD"
|
||||
|
||||
# Optional tuning
|
||||
export WG_PORT="51820"
|
||||
export WG_CIDR="10.200.0.1/24"
|
||||
|
||||
./scripts/00_preflight.sh
|
||||
./scripts/10_plan.sh
|
||||
|
||||
export DRY_RUN=0
|
||||
./scripts/11_apply.sh
|
||||
|
||||
# Optional: scaffold WireGuard (root)
|
||||
./scripts/20_wireguard_plan.sh
|
||||
export DRY_RUN=0
|
||||
./scripts/21_wireguard_apply.sh
|
||||
|
||||
./scripts/90_verify.sh
|
||||
./scripts/99_report.sh
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---|---:|---|---|
|
||||
| NODE_NAME | Yes | (none) | Hostname to set (e.g. vm-de-op) |
|
||||
| SOVEREIGN_USER | No | sovereign | User to create |
|
||||
| SSH_PUBLIC_KEY | Yes | (none) | Public key to authorize for sovereign |
|
||||
| SSH_PORT | No | 22 | SSH port to allow in UFW (auto-detected if unset) |
|
||||
| ALLOW_SSH_FALLBACK_22 | No | true | Safety: keep 22/tcp open if SSH_PORT != 22 |
|
||||
| WG_PORT | No | 51820 | WireGuard listen port |
|
||||
| WG_CIDR | No | 10.200.0.1/24 | WireGuard interface address |
|
||||
| INSTALL_CLOUDFLARED | No | true | Install cloudflared from Cloudflare apt repo |
|
||||
| INSTALL_WIREGUARD | No | true | Install wireguard package |
|
||||
| DRY_RUN | No | 1 | Apply refuses unless DRY_RUN=0 |
|
||||
| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase |
|
||||
| CONFIRM_PHRASE | No | I UNDERSTAND THIS CAN AFFECT REMOTE ACCESS | Safety phrase |
|
||||
|
||||
## Outputs
|
||||
|
||||
- `outputs/status_matrix.json`
|
||||
- `outputs/audit_report.md`
|
||||
- `outputs/backups/*` (sshd_config, ufw before, etc.)
|
||||
|
||||
## Notes
|
||||
- After Phase 11 apply, **open a second SSH session** as the sovereign user.
|
||||
- Only after confirming sovereign access should you close the root session.
|
||||
54
hetzner-bootstrap/config.json
Normal file
54
hetzner-bootstrap/config.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "hetzner-bootstrap",
|
||||
"version": "1.1.0",
|
||||
"defaults": {
|
||||
"SOVEREIGN_USER": "sovereign",
|
||||
"WG_PORT": "51820",
|
||||
"WG_CIDR": "10.200.0.1/24",
|
||||
"INSTALL_CLOUDFLARED": "true",
|
||||
"INSTALL_WIREGUARD": "true",
|
||||
"DRY_RUN": "1",
|
||||
"REQUIRE_CONFIRM": "1",
|
||||
"CONFIRM_PHRASE": "I UNDERSTAND THIS CAN AFFECT REMOTE ACCESS",
|
||||
"SSH_PORT": "22",
|
||||
"ALLOW_SSH_FALLBACK_22": "true"
|
||||
},
|
||||
"phases": {
|
||||
"preflight": [
|
||||
"00_preflight.sh"
|
||||
],
|
||||
"bootstrap": {
|
||||
"plan": [
|
||||
"10_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"11_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/emergency_restore_ssh_ufw.sh"
|
||||
]
|
||||
},
|
||||
"wireguard": {
|
||||
"plan": [
|
||||
"20_wireguard_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"21_wireguard_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_wireguard.sh"
|
||||
]
|
||||
},
|
||||
"verify": [
|
||||
"90_verify.sh"
|
||||
],
|
||||
"report": [
|
||||
"99_report.sh"
|
||||
]
|
||||
},
|
||||
"eu_compliance": {
|
||||
"data_residency": "EU",
|
||||
"jurisdiction": "Germany/Finland (Hetzner DC)",
|
||||
"gdpr_applicable": true
|
||||
}
|
||||
}
|
||||
9
hetzner-bootstrap/references/notes.md
Normal file
9
hetzner-bootstrap/references/notes.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Notes
|
||||
|
||||
## Operational guidance
|
||||
- Keep a root session open while applying SSH/UFW changes.
|
||||
- Verify `ssh sovereign@<ip>` in a second terminal before closing root.
|
||||
- Prefer exposing services via Cloudflare Tunnel; keep inbound ports minimal.
|
||||
|
||||
## Extending
|
||||
v1 is apt-based and designed for Ubuntu/Debian. For other distros, fork the install steps.
|
||||
64
hetzner-bootstrap/scripts/00_preflight.sh
Executable file
64
hetzner-bootstrap/scripts/00_preflight.sh
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${NODE_NAME:=}"
|
||||
: "${SOVEREIGN_USER:=sovereign}"
|
||||
: "${SSH_PUBLIC_KEY:=}"
|
||||
: "${SSH_PORT:=}" # optional; auto-detect in apply if unset
|
||||
: "${INSTALL_CLOUDFLARED:=true}"
|
||||
: "${INSTALL_WIREGUARD:=true}"
|
||||
: "${WG_PORT:=51820}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
|
||||
[[ -n "$NODE_NAME" ]] || die "NODE_NAME is required"
|
||||
[[ -n "$SSH_PUBLIC_KEY" ]] || die "SSH_PUBLIC_KEY is required"
|
||||
|
||||
# Required baseline tools for a typical Hetzner Debian/Ubuntu image.
|
||||
need apt
|
||||
need systemctl
|
||||
need hostnamectl
|
||||
need sed
|
||||
need grep
|
||||
need getent
|
||||
need id
|
||||
need chmod
|
||||
need chown
|
||||
|
||||
# Soft checks: these may be installed during apply.
|
||||
if ! command -v ufw >/dev/null 2>&1; then
|
||||
log_warn "ufw not found (will be installed during apply)."
|
||||
fi
|
||||
if ! command -v sshd >/dev/null 2>&1; then
|
||||
log_warn "sshd not found (openssh-server may be installed during apply)."
|
||||
fi
|
||||
if [[ "$INSTALL_WIREGUARD" == "true" ]] && ! command -v wg >/dev/null 2>&1; then
|
||||
log_warn "wg not found (wireguard will be installed during apply)."
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_CLOUDFLARED" == "true" ]]; then
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
log_warn "curl not found (will be installed during apply)."
|
||||
fi
|
||||
if ! command -v gpg >/dev/null 2>&1; then
|
||||
log_warn "gpg not found (will be installed during apply via gnupg)."
|
||||
fi
|
||||
code="$(os_codename)"
|
||||
if [[ -z "$code" ]]; then
|
||||
log_warn "Could not determine OS codename in preflight; apply will attempt again."
|
||||
else
|
||||
log_info "OS codename detected: $code"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$SSH_PORT" ]]; then
|
||||
log_info "SSH_PORT override set: $SSH_PORT"
|
||||
fi
|
||||
|
||||
log_info "Preflight OK."
|
||||
log_info "Reminder: keep an open root session until sovereign access is verified."
|
||||
}
|
||||
main "$@"
|
||||
41
hetzner-bootstrap/scripts/10_plan.sh
Executable file
41
hetzner-bootstrap/scripts/10_plan.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${SERVER_IP:=}"
|
||||
: "${NODE_NAME:=}"
|
||||
: "${SOVEREIGN_USER:=sovereign}"
|
||||
: "${SSH_PUBLIC_KEY:=}"
|
||||
: "${SSH_PORT:=}" # optional: if unset, apply auto-detects current sshd port (fallback 22)
|
||||
: "${ALLOW_SSH_FALLBACK_22:=true}"# safety: keep 22 open if SSH_PORT != 22
|
||||
: "${WG_PORT:=51820}"
|
||||
: "${INSTALL_CLOUDFLARED:=true}"
|
||||
: "${INSTALL_WIREGUARD:=true}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
[[ -n "$NODE_NAME" ]] || die "NODE_NAME required"
|
||||
[[ -n "$SSH_PUBLIC_KEY" ]] || die "SSH_PUBLIC_KEY required"
|
||||
|
||||
echo "[PLAN] $(date -Iseconds) hetzner-bootstrap"
|
||||
echo "[PLAN] Server IP: ${SERVER_IP:-<unknown>}"
|
||||
echo "[PLAN] Hostname: $NODE_NAME"
|
||||
echo "[PLAN] Sovereign user: $SOVEREIGN_USER"
|
||||
echo "[PLAN] SSH port allowance:"
|
||||
if [[ -n "$SSH_PORT" ]]; then
|
||||
echo " - Will allow SSH_PORT=${SSH_PORT}/tcp"
|
||||
if [[ "$ALLOW_SSH_FALLBACK_22" == "true" && "$SSH_PORT" != "22" ]]; then
|
||||
echo " - Safety fallback: also allow 22/tcp"
|
||||
fi
|
||||
else
|
||||
echo " - SSH_PORT not set: apply will auto-detect current sshd port (fallback 22) and allow it"
|
||||
fi
|
||||
echo "[PLAN] UFW: allow SSH port(s) + allow ${WG_PORT}/udp BEFORE enable; default deny incoming"
|
||||
echo "[PLAN] SSH hardening: PermitRootLogin no, PasswordAuthentication no, validate with sshd -t, reload ssh service"
|
||||
echo "[PLAN] cloudflared install: $INSTALL_CLOUDFLARED"
|
||||
echo "[PLAN] wireguard package install: $INSTALL_WIREGUARD"
|
||||
echo
|
||||
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/11_apply.sh"
|
||||
}
|
||||
main "$@"
|
||||
196
hetzner-bootstrap/scripts/11_apply.sh
Executable file
196
hetzner-bootstrap/scripts/11_apply.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${NODE_NAME:=}"
|
||||
: "${SOVEREIGN_USER:=sovereign}"
|
||||
: "${SSH_PUBLIC_KEY:=}"
|
||||
: "${SSH_PORT:=}" # optional; auto-detect if unset
|
||||
: "${ALLOW_SSH_FALLBACK_22:=true}" # if SSH_PORT != 22, keep 22 open as safety net
|
||||
: "${WG_PORT:=51820}"
|
||||
: "${INSTALL_CLOUDFLARED:=true}"
|
||||
: "${INSTALL_WIREGUARD:=true}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${BACKUP_DIR:=$OUTPUT_DIR/backups}"
|
||||
|
||||
install_cloudflared() {
|
||||
log_info "Installing cloudflared apt repo + package..."
|
||||
need curl
|
||||
need gpg
|
||||
|
||||
mkdir -p /usr/share/keyrings
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | gpg --dearmor -o /usr/share/keyrings/cloudflare-main.gpg
|
||||
|
||||
local code
|
||||
code="$(os_codename)"
|
||||
[[ -n "$code" ]] || die "Could not determine OS codename for cloudflared apt repo."
|
||||
|
||||
cat > /etc/apt/sources.list.d/cloudflared.list <<EOF
|
||||
deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared ${code} main
|
||||
EOF
|
||||
apt update
|
||||
apt install -y cloudflared
|
||||
}
|
||||
|
||||
detect_sshd_port() {
|
||||
# Prefer sshd -T output if available; otherwise fall back to 22.
|
||||
local p=""
|
||||
if command -v sshd >/dev/null 2>&1; then
|
||||
p="$(sshd -T 2>/dev/null | awk '/^port /{print $2; exit}' || true)"
|
||||
fi
|
||||
echo "${p:-22}"
|
||||
}
|
||||
|
||||
ensure_authorized_key() {
|
||||
local user="$1"
|
||||
local key_line="$2"
|
||||
local homedir="$3"
|
||||
local ssh_dir="$homedir/.ssh"
|
||||
local auth_file="$ssh_dir/authorized_keys"
|
||||
|
||||
install -d -m 700 -o "$user" -g "$user" "$ssh_dir"
|
||||
touch "$auth_file"
|
||||
chown "$user:$user" "$auth_file"
|
||||
chmod 600 "$auth_file"
|
||||
|
||||
# Normalize comparison to "type base64" (ignore comment).
|
||||
local key_norm
|
||||
key_norm="$(echo "$key_line" | awk '{print $1" "$2}')"
|
||||
if [[ -z "$key_norm" ]]; then
|
||||
die "SSH_PUBLIC_KEY appears invalid (expected: <type> <base64> [comment])"
|
||||
fi
|
||||
|
||||
if awk '{print $1" "$2}' "$auth_file" | grep -qxF "$key_norm"; then
|
||||
log_info "authorized_keys already contains this key (idempotent)."
|
||||
else
|
||||
echo "$key_line" >> "$auth_file"
|
||||
log_info "Appended key to $auth_file"
|
||||
fi
|
||||
}
|
||||
|
||||
harden_sshd_config_safely() {
|
||||
local cfg="/etc/ssh/sshd_config"
|
||||
local before="$BACKUP_DIR/sshd_config.before"
|
||||
|
||||
[[ -f "$cfg" ]] || die "Missing $cfg"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
if [[ ! -f "$before" ]]; then
|
||||
cp -p "$cfg" "$before"
|
||||
fi
|
||||
backup_file "$cfg" "$BACKUP_DIR"
|
||||
|
||||
local restore_on_exit="1"
|
||||
trap 'if [[ "$restore_on_exit" == "1" ]]; then log_warn "Restoring sshd_config from backup due to failure."; cp -p "$before" "$cfg" || true; reload_ssh_service; fi' EXIT
|
||||
|
||||
# Conservative replacements: add if missing, otherwise modify.
|
||||
if grep -qE '^\s*#?\s*PermitRootLogin' "$cfg"; then
|
||||
sed -i 's/^\s*#\?\s*PermitRootLogin.*/PermitRootLogin no/' "$cfg"
|
||||
else
|
||||
echo "PermitRootLogin no" >> "$cfg"
|
||||
fi
|
||||
|
||||
if grep -qE '^\s*#?\s*PasswordAuthentication' "$cfg"; then
|
||||
sed -i 's/^\s*#\?\s*PasswordAuthentication.*/PasswordAuthentication no/' "$cfg"
|
||||
else
|
||||
echo "PasswordAuthentication no" >> "$cfg"
|
||||
fi
|
||||
|
||||
# Validate before reload.
|
||||
sshd -t || die "sshd config validation failed (restoring)."
|
||||
|
||||
restore_on_exit="0"
|
||||
trap - EXIT
|
||||
|
||||
reload_ssh_service
|
||||
|
||||
if ! is_ssh_active; then
|
||||
die "SSH service is not active after reload. Use rollback/emergency_restore_ssh_ufw.sh from console."
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
confirm_gate
|
||||
[[ -n "$NODE_NAME" ]] || die "NODE_NAME required"
|
||||
[[ -n "$SSH_PUBLIC_KEY" ]] || die "SSH_PUBLIC_KEY required"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR" "$BACKUP_DIR"
|
||||
|
||||
log_info "1) Base packages"
|
||||
apt update
|
||||
apt install -y gnupg pass git curl jq htop tmux vim ufw fail2ban
|
||||
|
||||
# Ensure OpenSSH server exists (some minimal images may not).
|
||||
if ! command -v sshd >/dev/null 2>&1; then
|
||||
log_warn "openssh-server not found; installing..."
|
||||
apt install -y openssh-server
|
||||
fi
|
||||
|
||||
# Ensure host keys exist (sshd -t / sshd -T can fail without them)
|
||||
if command -v ssh-keygen >/dev/null 2>&1; then
|
||||
ssh-keygen -A >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Ensure SSH service is enabled/running
|
||||
restart_ssh_service
|
||||
|
||||
if [[ "$INSTALL_WIREGUARD" == "true" ]]; then
|
||||
apt install -y wireguard
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_CLOUDFLARED" == "true" ]]; then
|
||||
install_cloudflared
|
||||
fi
|
||||
|
||||
log_info "2) Create sovereign user + authorized_keys (idempotent)"
|
||||
if id "$SOVEREIGN_USER" >/dev/null 2>&1; then
|
||||
log_warn "User exists: $SOVEREIGN_USER"
|
||||
else
|
||||
useradd -m -s /bin/bash -G sudo "$SOVEREIGN_USER"
|
||||
fi
|
||||
|
||||
homedir="$(getent passwd "$SOVEREIGN_USER" | cut -d: -f6)"
|
||||
[[ -n "$homedir" ]] || die "Could not resolve homedir for $SOVEREIGN_USER"
|
||||
ensure_authorized_key "$SOVEREIGN_USER" "$SSH_PUBLIC_KEY" "$homedir"
|
||||
|
||||
log_info "3) Set hostname"
|
||||
hostnamectl set-hostname "$NODE_NAME"
|
||||
if ! grep -q "127.0.1.1 $NODE_NAME" /etc/hosts 2>/dev/null; then
|
||||
echo "127.0.1.1 $NODE_NAME" >> /etc/hosts
|
||||
fi
|
||||
|
||||
log_info "4) Configure UFW (SSH + WG ports BEFORE enable)"
|
||||
backup_file "/etc/ufw/user.rules" "$BACKUP_DIR"
|
||||
|
||||
# Auto-detect current sshd port if SSH_PORT unset.
|
||||
current_port="$(detect_sshd_port)"
|
||||
if [[ -z "$SSH_PORT" ]]; then
|
||||
SSH_PORT="$current_port"
|
||||
fi
|
||||
|
||||
# Always allow the chosen SSH_PORT. If you are migrating ports, keep 22 open as a safety net.
|
||||
ufw allow "${SSH_PORT}/tcp"
|
||||
if [[ "$ALLOW_SSH_FALLBACK_22" == "true" && "$SSH_PORT" != "22" ]]; then
|
||||
ufw allow "22/tcp"
|
||||
fi
|
||||
# If auto-detected differs (rare), keep it open too.
|
||||
if [[ "$current_port" != "$SSH_PORT" ]]; then
|
||||
ufw allow "${current_port}/tcp"
|
||||
fi
|
||||
|
||||
ufw allow "${WG_PORT}/udp"
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
ufw --force enable
|
||||
ufw status verbose > "$OUTPUT_DIR/ufw_status_after.txt" || true
|
||||
|
||||
log_info "5) Harden SSH (reload-safe + restore-on-failure)"
|
||||
harden_sshd_config_safely
|
||||
|
||||
log_info "Bootstrap apply complete."
|
||||
log_info "NEXT: open a second session as the sovereign user before closing root."
|
||||
}
|
||||
main "$@"
|
||||
21
hetzner-bootstrap/scripts/20_wireguard_plan.sh
Executable file
21
hetzner-bootstrap/scripts/20_wireguard_plan.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${WG_PORT:=51820}"
|
||||
: "${WG_CIDR:=10.200.0.1/24}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
echo "[PLAN] $(date -Iseconds) WireGuard scaffold"
|
||||
echo "[PLAN] Will generate keys in /etc/wireguard/{privatekey,publickey} (private is 0600 root:root)"
|
||||
echo "[PLAN] Will write /etc/wireguard/wg0.conf with:"
|
||||
echo " Address=$WG_CIDR"
|
||||
echo " ListenPort=$WG_PORT"
|
||||
echo " PrivateKey=(read from /etc/wireguard/privatekey)"
|
||||
echo "[PLAN] Will enable + start wg-quick@wg0"
|
||||
echo
|
||||
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/21_wireguard_apply.sh"
|
||||
}
|
||||
main "$@"
|
||||
51
hetzner-bootstrap/scripts/21_wireguard_apply.sh
Executable file
51
hetzner-bootstrap/scripts/21_wireguard_apply.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${WG_PORT:=51820}"
|
||||
: "${WG_CIDR:=10.200.0.1/24}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${BACKUP_DIR:=$OUTPUT_DIR/backups}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
confirm_gate
|
||||
need wg
|
||||
mkdir -p "$OUTPUT_DIR" "$BACKUP_DIR"
|
||||
mkdir -p /etc/wireguard
|
||||
|
||||
backup_file "/etc/wireguard/wg0.conf" "$BACKUP_DIR"
|
||||
|
||||
if [[ ! -f /etc/wireguard/privatekey ]]; then
|
||||
log_info "Generating WireGuard keys..."
|
||||
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
|
||||
chown root:root /etc/wireguard/privatekey /etc/wireguard/publickey
|
||||
chmod 600 /etc/wireguard/privatekey
|
||||
chmod 644 /etc/wireguard/publickey || true
|
||||
else
|
||||
log_warn "WireGuard privatekey exists; not overwriting."
|
||||
fi
|
||||
|
||||
privkey="$(cat /etc/wireguard/privatekey)"
|
||||
cat > /etc/wireguard/wg0.conf <<EOF
|
||||
[Interface]
|
||||
Address = $WG_CIDR
|
||||
ListenPort = $WG_PORT
|
||||
PrivateKey = $privkey
|
||||
|
||||
# Add peers below. Never commit private keys to git.
|
||||
# [Peer]
|
||||
# PublicKey = <peer-public-key>
|
||||
# AllowedIPs = 10.200.0.2/32
|
||||
EOF
|
||||
chmod 600 /etc/wireguard/wg0.conf
|
||||
|
||||
systemctl enable wg-quick@wg0
|
||||
systemctl start wg-quick@wg0
|
||||
|
||||
log_info "WireGuard started. Public key:"
|
||||
cat /etc/wireguard/publickey | tee "$OUTPUT_DIR/wireguard_publickey.txt"
|
||||
}
|
||||
main "$@"
|
||||
51
hetzner-bootstrap/scripts/90_verify.sh
Executable file
51
hetzner-bootstrap/scripts/90_verify.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${NODE_NAME:=}"
|
||||
: "${SOVEREIGN_USER:=sovereign}"
|
||||
: "${WG_PORT:=51820}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
status="$OUTPUT_DIR/status_matrix.json"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
ok_user=false; ok_hostname=false; ok_ufw=false; ok_ssh=false; ok_wg=false
|
||||
|
||||
id "$SOVEREIGN_USER" >/dev/null 2>&1 && ok_user=true
|
||||
[[ -n "$NODE_NAME" ]] && hostname | grep -q "$NODE_NAME" && ok_hostname=true || true
|
||||
ufw status 2>/dev/null | grep -qi "Status: active" && ok_ufw=true || true
|
||||
sshd -t >/dev/null 2>&1 && ok_ssh=true || true
|
||||
systemctl is-active wg-quick@wg0 >/dev/null 2>&1 && ok_wg=true || true
|
||||
|
||||
blockers="[]"
|
||||
if [[ "$ok_user" != "true" ]]; then blockers='["missing_sovereign_user"]'
|
||||
elif [[ "$ok_ufw" != "true" ]]; then blockers='["ufw_not_active"]'
|
||||
elif [[ "$ok_ssh" != "true" ]]; then blockers='["sshd_config_invalid"]'
|
||||
fi
|
||||
|
||||
cat > "$status" <<EOF
|
||||
{
|
||||
"skill":"hetzner-bootstrap",
|
||||
"timestamp":"$(date -Iseconds)",
|
||||
"checks":[
|
||||
{"name":"sovereign_user_present","ok": $ok_user, "user":"$(json_escape "$SOVEREIGN_USER")"},
|
||||
{"name":"hostname_set","ok": $ok_hostname, "hostname":"$(json_escape "$(hostname)")"},
|
||||
{"name":"ufw_active","ok": $ok_ufw, "wg_port":"$(json_escape "$WG_PORT")"},
|
||||
{"name":"sshd_config_valid","ok": $ok_ssh},
|
||||
{"name":"wireguard_active_optional","ok": $ok_wg}
|
||||
],
|
||||
"blockers": $blockers,
|
||||
"warnings": [],
|
||||
"next_steps": ["verify sovereign SSH login", "operator-bootstrap", "secrets-vault", "cloudflare-tunnel-manager"]
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote $status"
|
||||
cat "$status"
|
||||
}
|
||||
main "$@"
|
||||
46
hetzner-bootstrap/scripts/99_report.sh
Executable file
46
hetzner-bootstrap/scripts/99_report.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
main() {
|
||||
report="$OUTPUT_DIR/audit_report.md"
|
||||
status="$OUTPUT_DIR/status_matrix.json"
|
||||
ufw_out="$OUTPUT_DIR/ufw_status_after.txt"
|
||||
wg_pub="$OUTPUT_DIR/wireguard_publickey.txt"
|
||||
|
||||
cat > "$report" <<EOF
|
||||
# Hetzner Bootstrap Audit Report
|
||||
|
||||
**Generated:** $(date -Iseconds)
|
||||
**Skill Version:** 1.0.0
|
||||
|
||||
## Status Matrix
|
||||
|
||||
$(if [[ -f "$status" ]]; then echo '```json'; cat "$status"; echo '```'; else echo "_Missing status_matrix.json_"; fi)
|
||||
|
||||
## UFW Snapshot (after)
|
||||
|
||||
$(if [[ -f "$ufw_out" ]]; then echo '```'; cat "$ufw_out"; echo '```'; else echo "_No ufw snapshot captured._"; fi)
|
||||
|
||||
## WireGuard Public Key (if generated)
|
||||
|
||||
$(if [[ -f "$wg_pub" ]]; then echo '```'; cat "$wg_pub"; echo '```'; else echo "_No wg public key captured._"; fi)
|
||||
|
||||
## Rollback
|
||||
|
||||
- SSH/UFW emergency restore: \`./scripts/rollback/emergency_restore_ssh_ufw.sh\`
|
||||
- WireGuard undo: \`./scripts/rollback/undo_wireguard.sh\`
|
||||
|
||||
## EU Compliance
|
||||
|
||||
Hetzner EU DC (Germany/Finland). Local-first node operations; public services should be via Cloudflare Tunnel.
|
||||
EOF
|
||||
|
||||
log_info "Wrote $report"
|
||||
cat "$report"
|
||||
}
|
||||
main "$@"
|
||||
79
hetzner-bootstrap/scripts/_common.sh
Executable file
79
hetzner-bootstrap/scripts/_common.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log_info(){ echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn(){ echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
log_error(){ echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
die(){ log_error "$*"; exit 1; }
|
||||
need(){ command -v "$1" >/dev/null 2>&1 || die "Missing required tool: $1"; }
|
||||
|
||||
confirm_gate() {
|
||||
: "${DRY_RUN:=1}"
|
||||
: "${REQUIRE_CONFIRM:=1}"
|
||||
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS CAN AFFECT REMOTE ACCESS}"
|
||||
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0)."
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo "Type to confirm:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
echo
|
||||
read -r -p "> " typed
|
||||
[[ "$typed" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase did not match."
|
||||
fi
|
||||
}
|
||||
|
||||
require_root() {
|
||||
[[ "$(id -u)" == "0" ]] || die "Run as root."
|
||||
}
|
||||
|
||||
backup_file() {
|
||||
local src="$1" backup_dir="$2"
|
||||
mkdir -p "$backup_dir"
|
||||
if [[ -f "$src" ]]; then
|
||||
local ts; ts="$(date -Iseconds | tr ':' '-')"
|
||||
cp -p "$src" "$backup_dir/$(basename "$src").${ts}.bak"
|
||||
fi
|
||||
}
|
||||
|
||||
os_codename() {
|
||||
# Prefer /etc/os-release (present on Debian/Ubuntu).
|
||||
local code=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
code="${VERSION_CODENAME:-}"
|
||||
fi
|
||||
if [[ -z "$code" ]] && command -v lsb_release >/dev/null 2>&1; then
|
||||
code="$(lsb_release -cs 2>/dev/null || true)"
|
||||
fi
|
||||
echo "$code"
|
||||
}
|
||||
|
||||
ssh_service_name() {
|
||||
# Debian/Ubuntu commonly use "ssh.service". Some distros use "sshd.service".
|
||||
if systemctl status ssh >/dev/null 2>&1; then
|
||||
echo "ssh"
|
||||
return 0
|
||||
fi
|
||||
if systemctl status sshd >/dev/null 2>&1; then
|
||||
echo "sshd"
|
||||
return 0
|
||||
fi
|
||||
# Fallback
|
||||
echo "ssh"
|
||||
}
|
||||
|
||||
reload_ssh_service() {
|
||||
local svc; svc="$(ssh_service_name)"
|
||||
systemctl reload "$svc" >/dev/null 2>&1 || systemctl restart "$svc" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
restart_ssh_service() {
|
||||
local svc; svc="$(ssh_service_name)"
|
||||
systemctl restart "$svc" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
is_ssh_active() {
|
||||
local svc; svc="$(ssh_service_name)"
|
||||
systemctl is-active "$svc" >/dev/null 2>&1
|
||||
}
|
||||
34
hetzner-bootstrap/scripts/rollback/emergency_restore_ssh_ufw.sh
Executable file
34
hetzner-bootstrap/scripts/rollback/emergency_restore_ssh_ufw.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${BACKUP_DIR:=$OUTPUT_DIR/backups}"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
confirm_gate
|
||||
log_warn "Emergency restore: attempting to relax UFW + restore sshd_config from backup."
|
||||
ufw --force disable >/dev/null 2>&1 || true
|
||||
|
||||
# Prefer the stable "before" snapshot if present.
|
||||
if [[ -f "$BACKUP_DIR/sshd_config.before" ]]; then
|
||||
latest="$BACKUP_DIR/sshd_config.before"
|
||||
else
|
||||
latest="$(ls -1t "$BACKUP_DIR/sshd_config."*.bak 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
|
||||
if [[ -n "${latest:-}" && -f "${latest:-}" ]]; then
|
||||
cp -p "$latest" /etc/ssh/sshd_config
|
||||
sshd -t || die "Restored sshd_config still invalid."
|
||||
restart_ssh_service
|
||||
log_info "Restored sshd_config from $latest"
|
||||
else
|
||||
log_warn "No sshd_config backup found in $BACKUP_DIR"
|
||||
fi
|
||||
|
||||
log_info "Emergency restore complete. Confirm access via console + SSH."
|
||||
}
|
||||
main "$@"
|
||||
14
hetzner-bootstrap/scripts/rollback/undo_wireguard.sh
Executable file
14
hetzner-bootstrap/scripts/rollback/undo_wireguard.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
main() {
|
||||
require_root
|
||||
confirm_gate
|
||||
log_warn "Stopping and disabling WireGuard wg0"
|
||||
systemctl disable --now wg-quick@wg0 >/dev/null 2>&1 || true
|
||||
log_info "WireGuard disabled. Config remains at /etc/wireguard/wg0.conf (remove manually if desired)."
|
||||
}
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user