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:
216
operator-bootstrap/scripts/21_tunnel_apply.sh
Executable file
216
operator-bootstrap/scripts/21_tunnel_apply.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${DOMAIN:?DOMAIN required}"
|
||||
: "${CF_ACCOUNT_ID:?CF_ACCOUNT_ID required}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${TUNNEL_NAME:=$NODE_NAME-tunnel}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
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; }
|
||||
|
||||
preflight() {
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
[[ -d "$HOME/.cloudflared" ]] || mkdir -p "$HOME/.cloudflared"
|
||||
}
|
||||
|
||||
ensure_authenticated() {
|
||||
if [[ ! -f "$HOME/.cloudflared/cert.pem" ]]; then
|
||||
log_info "No cloudflared certificate found. Starting login..."
|
||||
log_info "A browser window will open for authentication."
|
||||
cloudflared tunnel login
|
||||
else
|
||||
log_info "Cloudflared already authenticated"
|
||||
fi
|
||||
}
|
||||
|
||||
create_tunnel() {
|
||||
local creds="$HOME/.cloudflared/$TUNNEL_NAME.json"
|
||||
|
||||
if [[ -f "$creds" ]]; then
|
||||
log_info "Tunnel credentials already exist - reusing existing tunnel"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating Cloudflare tunnel: $TUNNEL_NAME"
|
||||
|
||||
cloudflared tunnel create "$TUNNEL_NAME"
|
||||
|
||||
if [[ ! -f "$creds" ]]; then
|
||||
die "Tunnel creation failed - credentials not found at $creds"
|
||||
fi
|
||||
|
||||
log_info "Tunnel created successfully"
|
||||
}
|
||||
|
||||
create_config() {
|
||||
local config="$HOME/.cloudflared/config-$TUNNEL_NAME.yml"
|
||||
|
||||
if [[ -f "$config" ]]; then
|
||||
log_info "Tunnel config already exists at $config - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cat > "$config" <<EOF
|
||||
# Cloudflare Tunnel Configuration
|
||||
# Generated by operator-bootstrap $(date -Iseconds)
|
||||
# Node: $NODE_NAME
|
||||
|
||||
tunnel: $TUNNEL_NAME
|
||||
credentials-file: $HOME/.cloudflared/$TUNNEL_NAME.json
|
||||
|
||||
ingress:
|
||||
# SSH access via Cloudflare Access
|
||||
- hostname: ssh.$DOMAIN
|
||||
service: ssh://localhost:22
|
||||
|
||||
# Default web service (placeholder)
|
||||
- hostname: "*.$DOMAIN"
|
||||
service: http://localhost:8080
|
||||
|
||||
# Catch-all 404
|
||||
- service: http_status:404
|
||||
EOF
|
||||
|
||||
log_info "Tunnel config created: $config"
|
||||
}
|
||||
|
||||
setup_dns() {
|
||||
log_info "Creating DNS route for ssh.$DOMAIN..."
|
||||
|
||||
# This may fail if DNS already exists - that's OK
|
||||
if cloudflared tunnel route dns "$TUNNEL_NAME" "ssh.$DOMAIN" 2>/dev/null; then
|
||||
log_info "DNS route created for ssh.$DOMAIN"
|
||||
else
|
||||
log_warn "DNS route creation returned error - may already exist"
|
||||
log_info "Verify DNS at: https://dash.cloudflare.com"
|
||||
fi
|
||||
}
|
||||
|
||||
store_credentials_in_pass() {
|
||||
local creds="$HOME/.cloudflared/$TUNNEL_NAME.json"
|
||||
|
||||
if command -v pass &>/dev/null && [[ -d "$HOME/.password-store" ]]; then
|
||||
log_info "Storing tunnel ID in pass..."
|
||||
|
||||
local tunnel_id
|
||||
if command -v jq &>/dev/null; then
|
||||
tunnel_id=$(jq -r '.TunnelID' "$creds" 2>/dev/null || echo "unknown")
|
||||
else
|
||||
tunnel_id=$(grep -o '"TunnelID":"[^"]*"' "$creds" | cut -d'"' -f4 || echo "unknown")
|
||||
fi
|
||||
|
||||
echo "$tunnel_id" | pass insert -f "infrastructure/cloudflare/tunnel-id" 2>/dev/null || \
|
||||
log_warn "Could not store tunnel ID in pass"
|
||||
|
||||
log_info "Tunnel ID stored in pass: infrastructure/cloudflare/tunnel-id"
|
||||
else
|
||||
log_warn "Pass not available - tunnel ID stored only in $creds"
|
||||
fi
|
||||
}
|
||||
|
||||
install_service() {
|
||||
local config="$HOME/.cloudflared/config-$TUNNEL_NAME.yml"
|
||||
|
||||
log_info "Attempting to install systemd service..."
|
||||
|
||||
# Try user service first (doesn't require root)
|
||||
if systemctl --user daemon-reload 2>/dev/null; then
|
||||
# Create user service file
|
||||
local service_dir="$HOME/.config/systemd/user"
|
||||
mkdir -p "$service_dir"
|
||||
|
||||
cat > "$service_dir/cloudflared-$TUNNEL_NAME.service" <<EOF
|
||||
[Unit]
|
||||
Description=Cloudflare Tunnel $TUNNEL_NAME
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$(command -v cloudflared) tunnel --config $config run
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable "cloudflared-$TUNNEL_NAME.service"
|
||||
log_info "User service installed: cloudflared-$TUNNEL_NAME.service"
|
||||
log_info "Start with: systemctl --user start cloudflared-$TUNNEL_NAME"
|
||||
else
|
||||
log_warn "Could not set up user systemd service"
|
||||
log_info "To run tunnel manually: cloudflared tunnel --config $config run"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local manifest="$OUTPUT_DIR/tunnel_config.json"
|
||||
local creds="$HOME/.cloudflared/$TUNNEL_NAME.json"
|
||||
|
||||
local tunnel_id="unknown"
|
||||
if command -v jq &>/dev/null && [[ -f "$creds" ]]; then
|
||||
tunnel_id=$(jq -r '.TunnelID' "$creds" 2>/dev/null || echo "unknown")
|
||||
elif [[ -f "$creds" ]]; then
|
||||
tunnel_id=$(grep -o '"TunnelID":"[^"]*"' "$creds" | cut -d'"' -f4 || echo "unknown")
|
||||
fi
|
||||
|
||||
cat > "$manifest" <<EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"node": "$NODE_NAME",
|
||||
"tunnel": {
|
||||
"name": "$TUNNEL_NAME",
|
||||
"id": "$tunnel_id",
|
||||
"credentials": "$HOME/.cloudflared/$TUNNEL_NAME.json",
|
||||
"config": "$HOME/.cloudflared/config-$TUNNEL_NAME.yml"
|
||||
},
|
||||
"dns": {
|
||||
"ssh": "ssh.$DOMAIN"
|
||||
},
|
||||
"service": "cloudflared-$TUNNEL_NAME"
|
||||
}
|
||||
EOF
|
||||
log_info "Tunnel manifest written to $manifest"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
ensure_authenticated
|
||||
create_tunnel
|
||||
create_config
|
||||
setup_dns
|
||||
store_credentials_in_pass
|
||||
install_service
|
||||
generate_manifest
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " TUNNEL SETUP COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " Tunnel: $TUNNEL_NAME"
|
||||
echo " SSH access: ssh.$DOMAIN"
|
||||
echo ""
|
||||
echo " To start: systemctl --user start cloudflared-$TUNNEL_NAME"
|
||||
echo " To test: cloudflared tunnel --config ~/.cloudflared/config-$TUNNEL_NAME.yml run"
|
||||
echo ""
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
Reference in New Issue
Block a user