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:
82
operator-bootstrap/scripts/00_preflight.sh
Executable file
82
operator-bootstrap/scripts/00_preflight.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${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; }
|
||||
|
||||
check_dependency() {
|
||||
if command -v "$1" &>/dev/null; then
|
||||
log_info "Found: $1 ($(command -v "$1"))"
|
||||
return 0
|
||||
else
|
||||
log_warn "Missing: $1"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
preflight() {
|
||||
log_info "Running preflight checks..."
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
local missing=0
|
||||
|
||||
# Required dependencies
|
||||
log_info "=== Required Dependencies ==="
|
||||
check_dependency gpg || ((missing++))
|
||||
check_dependency ssh-keygen || ((missing++))
|
||||
check_dependency pass || ((missing++))
|
||||
check_dependency cloudflared || ((missing++))
|
||||
check_dependency git || ((missing++))
|
||||
|
||||
# Optional dependencies
|
||||
log_info "=== Optional Dependencies ==="
|
||||
check_dependency kate || log_info "Kate not found - editor setup will be skipped"
|
||||
check_dependency jq || log_info "jq not found - JSON output will be basic"
|
||||
check_dependency curl || log_info "curl not found - network checks skipped"
|
||||
|
||||
# Network checks
|
||||
log_info "=== Network Connectivity ==="
|
||||
if command -v curl &>/dev/null; then
|
||||
if curl -s --connect-timeout 5 https://api.cloudflare.com/client/v4/ &>/dev/null; then
|
||||
log_info "Cloudflare API reachable"
|
||||
else
|
||||
log_warn "Cloudflare API unreachable - tunnel setup may fail"
|
||||
fi
|
||||
fi
|
||||
|
||||
# EU residency check (informational)
|
||||
log_info "=== EU Data Residency ==="
|
||||
log_info "This node will be configured for EU data sovereignty"
|
||||
log_info "Jurisdiction: Ireland (Dublin)"
|
||||
|
||||
# Check for required parameters
|
||||
log_info "=== Required Parameters ==="
|
||||
[[ -n "${OPERATOR_NAME:-}" ]] && log_info "OPERATOR_NAME: set" || log_warn "OPERATOR_NAME: not set"
|
||||
[[ -n "${OPERATOR_EMAIL:-}" ]] && log_info "OPERATOR_EMAIL: set" || log_warn "OPERATOR_EMAIL: not set"
|
||||
[[ -n "${DOMAIN:-}" ]] && log_info "DOMAIN: set" || log_warn "DOMAIN: not set"
|
||||
[[ -n "${CF_ACCOUNT_ID:-}" ]] && log_info "CF_ACCOUNT_ID: set" || log_warn "CF_ACCOUNT_ID: not set"
|
||||
|
||||
if [[ $missing -gt 0 ]]; then
|
||||
die "Missing $missing required dependencies. Install them before proceeding."
|
||||
fi
|
||||
|
||||
log_info "Completed $SCRIPT_NAME - all preflight checks passed"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
72
operator-bootstrap/scripts/01_identity_plan.sh
Executable file
72
operator-bootstrap/scripts/01_identity_plan.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${OPERATOR_NAME:?OPERATOR_NAME required}"
|
||||
: "${OPERATOR_EMAIL:?OPERATOR_EMAIL required}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${SSH_KEY_COMMENT:=$NODE_NAME-operator}"
|
||||
: "${GPG_KEY_SIZE:=4096}"
|
||||
: "${GPG_KEY_EXPIRE:=2y}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME (PLAN ONLY - no changes made)..."
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " IDENTITY SETUP PLAN"
|
||||
echo " Node: $NODE_NAME"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
echo "=== GPG Key Configuration ==="
|
||||
echo " Name: $OPERATOR_NAME"
|
||||
echo " Email: $OPERATOR_EMAIL"
|
||||
echo " Key Size: $GPG_KEY_SIZE bits (RSA)"
|
||||
echo " Expiry: $GPG_KEY_EXPIRE"
|
||||
echo " Location: ~/.gnupg/"
|
||||
echo ""
|
||||
|
||||
echo "=== SSH Key Configuration ==="
|
||||
echo " Primary: Ed25519 (~/.ssh/id_ed25519_${NODE_NAME})"
|
||||
echo " Fallback: RSA-4096 (~/.ssh/id_rsa_${NODE_NAME})"
|
||||
echo " Comment: $SSH_KEY_COMMENT"
|
||||
echo ""
|
||||
|
||||
echo "=== SSH Config Changes ==="
|
||||
echo " File: ~/.ssh/config"
|
||||
echo " Backup: ~/.ssh/config.bak.$(date +%Y%m%d)"
|
||||
echo " Addition: Host alias for $NODE_NAME"
|
||||
echo ""
|
||||
|
||||
# Check for existing keys
|
||||
if [[ -f "$HOME/.ssh/id_ed25519_${NODE_NAME}" ]]; then
|
||||
log_warn "SSH key already exists at ~/.ssh/id_ed25519_${NODE_NAME}"
|
||||
log_warn "Apply will skip SSH key creation (idempotent)"
|
||||
fi
|
||||
|
||||
if gpg --list-keys "$OPERATOR_EMAIL" &>/dev/null 2>&1; then
|
||||
log_warn "GPG key for $OPERATOR_EMAIL already exists"
|
||||
log_warn "Apply will skip GPG key creation (idempotent)"
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " To apply: ./scripts/02_identity_apply.sh"
|
||||
echo " To abort: Do nothing"
|
||||
echo " To rollback: ./scripts/rollback/undo_identity.sh"
|
||||
echo "============================================"
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
155
operator-bootstrap/scripts/02_identity_apply.sh
Executable file
155
operator-bootstrap/scripts/02_identity_apply.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${OPERATOR_NAME:?OPERATOR_NAME required}"
|
||||
: "${OPERATOR_EMAIL:?OPERATOR_EMAIL required}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${SSH_KEY_COMMENT:=$NODE_NAME-operator}"
|
||||
: "${GPG_KEY_SIZE:=4096}"
|
||||
: "${GPG_KEY_EXPIRE:=2y}"
|
||||
: "${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/.ssh" ]] || { mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"; }
|
||||
}
|
||||
|
||||
create_gpg_key() {
|
||||
if gpg --list-keys "$OPERATOR_EMAIL" &>/dev/null 2>&1; then
|
||||
log_info "GPG key for $OPERATOR_EMAIL already exists - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Generating GPG key (you will be prompted for passphrase)..."
|
||||
|
||||
# Create key generation parameters
|
||||
local params_file
|
||||
params_file=$(mktemp)
|
||||
cat > "$params_file" <<EOF
|
||||
%echo Generating GPG key for $OPERATOR_NAME
|
||||
Key-Type: RSA
|
||||
Key-Length: $GPG_KEY_SIZE
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: $GPG_KEY_SIZE
|
||||
Name-Real: $OPERATOR_NAME
|
||||
Name-Email: $OPERATOR_EMAIL
|
||||
Expire-Date: $GPG_KEY_EXPIRE
|
||||
%ask-passphrase
|
||||
%commit
|
||||
%echo Done
|
||||
EOF
|
||||
|
||||
gpg --batch --gen-key "$params_file"
|
||||
rm -f "$params_file"
|
||||
|
||||
log_info "GPG key created for $OPERATOR_EMAIL"
|
||||
}
|
||||
|
||||
create_ssh_keys() {
|
||||
local ed_key="$HOME/.ssh/id_ed25519_${NODE_NAME}"
|
||||
local rsa_key="$HOME/.ssh/id_rsa_${NODE_NAME}"
|
||||
|
||||
# Ed25519 key
|
||||
if [[ -f "$ed_key" ]]; then
|
||||
log_info "Ed25519 key already exists at $ed_key - skipping"
|
||||
else
|
||||
log_info "Generating Ed25519 SSH key..."
|
||||
ssh-keygen -t ed25519 -f "$ed_key" -C "$SSH_KEY_COMMENT" -N ""
|
||||
log_info "Ed25519 key created: $ed_key"
|
||||
fi
|
||||
|
||||
# RSA fallback key
|
||||
if [[ -f "$rsa_key" ]]; then
|
||||
log_info "RSA key already exists at $rsa_key - skipping"
|
||||
else
|
||||
log_info "Generating RSA fallback SSH key..."
|
||||
ssh-keygen -t rsa -b 4096 -f "$rsa_key" -C "$SSH_KEY_COMMENT-rsa" -N ""
|
||||
log_info "RSA key created: $rsa_key"
|
||||
fi
|
||||
}
|
||||
|
||||
update_ssh_config() {
|
||||
local config="$HOME/.ssh/config"
|
||||
local backup="$HOME/.ssh/config.bak.$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
# Create config if it doesn't exist
|
||||
if [[ ! -f "$config" ]]; then
|
||||
touch "$config"
|
||||
chmod 600 "$config"
|
||||
fi
|
||||
|
||||
# Backup existing config
|
||||
cp "$config" "$backup"
|
||||
log_info "SSH config backed up to $backup"
|
||||
|
||||
# Check if entry already exists
|
||||
if grep -q "Host $NODE_NAME\$" "$config" 2>/dev/null; then
|
||||
log_info "SSH config entry for $NODE_NAME already exists - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Append new entry
|
||||
cat >> "$config" <<EOF
|
||||
|
||||
# Added by operator-bootstrap $(date -Iseconds)
|
||||
Host $NODE_NAME
|
||||
IdentityFile ~/.ssh/id_ed25519_${NODE_NAME}
|
||||
IdentitiesOnly yes
|
||||
EOF
|
||||
|
||||
chmod 600 "$config"
|
||||
log_info "SSH config updated with $NODE_NAME entry"
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local manifest="$OUTPUT_DIR/identity_manifest.json"
|
||||
local gpg_fingerprint
|
||||
gpg_fingerprint=$(gpg --list-keys --with-colons "$OPERATOR_EMAIL" 2>/dev/null | grep fpr | head -1 | cut -d: -f10 || echo "unknown")
|
||||
|
||||
cat > "$manifest" <<EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"node": "$NODE_NAME",
|
||||
"operator": {
|
||||
"name": "$OPERATOR_NAME",
|
||||
"email": "$OPERATOR_EMAIL"
|
||||
},
|
||||
"gpg": {
|
||||
"fingerprint": "$gpg_fingerprint",
|
||||
"key_size": $GPG_KEY_SIZE,
|
||||
"expires": "$GPG_KEY_EXPIRE"
|
||||
},
|
||||
"ssh": {
|
||||
"ed25519": "$HOME/.ssh/id_ed25519_${NODE_NAME}",
|
||||
"rsa": "$HOME/.ssh/id_rsa_${NODE_NAME}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
log_info "Identity manifest written to $manifest"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
create_gpg_key
|
||||
create_ssh_keys
|
||||
update_ssh_config
|
||||
generate_manifest
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
165
operator-bootstrap/scripts/10_secrets_guide.sh
Executable file
165
operator-bootstrap/scripts/10_secrets_guide.sh
Executable file
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${OPERATOR_EMAIL:?OPERATOR_EMAIL required}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${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; }
|
||||
|
||||
preflight() {
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
}
|
||||
|
||||
guide_pass_init() {
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " SECRETS ENROLLMENT - GUIDED SETUP"
|
||||
echo " This is an INTERACTIVE process"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# Check if pass is already initialized
|
||||
if [[ -d "$HOME/.password-store" ]] && [[ -f "$HOME/.password-store/.gpg-id" ]]; then
|
||||
log_info "Pass store already initialized"
|
||||
local existing_gpg
|
||||
existing_gpg=$(cat "$HOME/.password-store/.gpg-id")
|
||||
echo " Current GPG ID: $existing_gpg"
|
||||
echo ""
|
||||
read -p "Re-initialize with $OPERATOR_EMAIL? (y/N): " reinit
|
||||
if [[ "$reinit" != "y" && "$reinit" != "Y" ]]; then
|
||||
log_info "Keeping existing pass store"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== Step 1: Initialize Pass Store ==="
|
||||
echo ""
|
||||
echo "Run the following command to initialize pass with your GPG key:"
|
||||
echo ""
|
||||
echo " pass init \"$OPERATOR_EMAIL\""
|
||||
echo ""
|
||||
read -p "Press Enter when you have completed this step..."
|
||||
|
||||
# Verify
|
||||
if [[ ! -d "$HOME/.password-store" ]]; then
|
||||
log_warn "Pass store not detected at ~/.password-store"
|
||||
log_warn "Please verify initialization before continuing"
|
||||
else
|
||||
log_info "Pass store detected at $HOME/.password-store"
|
||||
fi
|
||||
}
|
||||
|
||||
guide_structure_creation() {
|
||||
echo ""
|
||||
echo "=== Step 2: Create Password Structure ==="
|
||||
echo ""
|
||||
echo "Create the following password hierarchy for infrastructure secrets:"
|
||||
echo ""
|
||||
echo " infrastructure/"
|
||||
echo " +-- cloudflare/"
|
||||
echo " | +-- api-token"
|
||||
echo " | +-- tunnel-secret"
|
||||
echo " +-- ssh/"
|
||||
echo " | +-- passphrase"
|
||||
echo " +-- services/"
|
||||
echo " +-- (future services)"
|
||||
echo ""
|
||||
echo "Commands to run (replace with your actual secrets):"
|
||||
echo ""
|
||||
echo " pass insert infrastructure/cloudflare/api-token"
|
||||
echo " pass insert infrastructure/cloudflare/tunnel-secret"
|
||||
echo ""
|
||||
echo "Note: You'll be prompted to enter each secret value."
|
||||
echo ""
|
||||
read -p "Press Enter when you have stored the critical secrets..."
|
||||
}
|
||||
|
||||
guide_verify() {
|
||||
echo ""
|
||||
echo "=== Step 3: Verify Encryption/Decryption ==="
|
||||
echo ""
|
||||
echo "Test that you can retrieve a secret:"
|
||||
echo ""
|
||||
echo " pass infrastructure/cloudflare/api-token"
|
||||
echo ""
|
||||
echo "This should display your API token (confirm it matches what you entered)."
|
||||
echo ""
|
||||
read -p "Press Enter when you have verified decryption works..."
|
||||
|
||||
# Attempt to list pass contents
|
||||
if command -v pass &>/dev/null; then
|
||||
echo ""
|
||||
echo "=== Current Pass Store Contents ==="
|
||||
pass ls 2>/dev/null || log_warn "Could not list pass store"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
guide_git_init() {
|
||||
echo ""
|
||||
echo "=== Step 4: Initialize Git for Pass Store (Optional) ==="
|
||||
echo ""
|
||||
echo "For version control of your encrypted passwords:"
|
||||
echo ""
|
||||
echo " pass git init"
|
||||
echo ""
|
||||
echo "This enables automatic commits when passwords change."
|
||||
echo ""
|
||||
read -p "Press Enter to continue (skip if you prefer not to use git)..."
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local manifest="$OUTPUT_DIR/secrets_manifest.json"
|
||||
|
||||
cat > "$manifest" <<EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"node": "$NODE_NAME",
|
||||
"pass_store": "$HOME/.password-store",
|
||||
"gpg_id": "$OPERATOR_EMAIL",
|
||||
"guided_enrollment": true,
|
||||
"structure": [
|
||||
"infrastructure/cloudflare/api-token",
|
||||
"infrastructure/cloudflare/tunnel-secret",
|
||||
"infrastructure/ssh/passphrase",
|
||||
"infrastructure/services/"
|
||||
],
|
||||
"note": "Secrets are encrypted with GPG. Never store this manifest with actual secret values."
|
||||
}
|
||||
EOF
|
||||
log_info "Secrets manifest written to $manifest"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME (INTERACTIVE GUIDED SETUP)..."
|
||||
|
||||
guide_pass_init
|
||||
guide_structure_creation
|
||||
guide_verify
|
||||
guide_git_init
|
||||
generate_manifest
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " SECRETS ENROLLMENT COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Your pass store is now configured."
|
||||
echo "Secrets are encrypted with your GPG key: $OPERATOR_EMAIL"
|
||||
echo ""
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
81
operator-bootstrap/scripts/20_tunnel_plan.sh
Executable file
81
operator-bootstrap/scripts/20_tunnel_plan.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/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; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME (PLAN ONLY - no changes made)..."
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " CLOUDFLARE TUNNEL PLAN"
|
||||
echo " Node: $NODE_NAME"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
echo "=== Tunnel Configuration ==="
|
||||
echo " Tunnel Name: $TUNNEL_NAME"
|
||||
echo " Account ID: $CF_ACCOUNT_ID"
|
||||
echo " Credentials: ~/.cloudflared/$TUNNEL_NAME.json"
|
||||
echo " Config: ~/.cloudflared/config-$TUNNEL_NAME.yml"
|
||||
echo ""
|
||||
|
||||
echo "=== Proposed Ingress Rules ==="
|
||||
echo " 1. ssh.$DOMAIN -> ssh://localhost:22"
|
||||
echo " 2. *.$DOMAIN -> http://localhost:8080 (catch-all)"
|
||||
echo " 3. (fallback) -> http_status:404"
|
||||
echo ""
|
||||
|
||||
echo "=== DNS Records to Create ==="
|
||||
echo " ssh.$DOMAIN CNAME -> $TUNNEL_NAME.cfargotunnel.com"
|
||||
echo ""
|
||||
|
||||
echo "=== systemd Service ==="
|
||||
echo " Unit: cloudflared@$TUNNEL_NAME.service (or user service)"
|
||||
echo " Status: Will be enabled and started"
|
||||
echo ""
|
||||
|
||||
echo "=== Security Notes ==="
|
||||
echo " - Tunnel credentials will be stored locally"
|
||||
echo " - Tunnel ID will be stored in pass (if available)"
|
||||
echo " - No API token stored in config files"
|
||||
echo ""
|
||||
|
||||
# Check for existing tunnel
|
||||
if [[ -f "$HOME/.cloudflared/$TUNNEL_NAME.json" ]]; then
|
||||
log_warn "Tunnel credentials already exist at ~/.cloudflared/$TUNNEL_NAME.json"
|
||||
log_warn "Apply will reuse existing tunnel (idempotent)"
|
||||
fi
|
||||
|
||||
# Check cloudflared login status
|
||||
if [[ -f "$HOME/.cloudflared/cert.pem" ]]; then
|
||||
log_info "Cloudflared certificate found - already authenticated"
|
||||
else
|
||||
log_warn "No cloudflared certificate found"
|
||||
log_warn "You may need to run: cloudflared tunnel login"
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " To apply: ./scripts/21_tunnel_apply.sh"
|
||||
echo " To abort: Do nothing"
|
||||
echo " To rollback: ./scripts/rollback/undo_tunnel.sh"
|
||||
echo "============================================"
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
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 "$@"
|
||||
73
operator-bootstrap/scripts/30_gitops_plan.sh
Executable file
73
operator-bootstrap/scripts/30_gitops_plan.sh
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME (PLAN ONLY - no changes made)..."
|
||||
|
||||
# Expand ~ in GITOPS_ROOT
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " GITOPS STRUCTURE PLAN"
|
||||
echo " Node: $NODE_NAME"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
echo "=== Directory Structure ==="
|
||||
echo " $GITOPS_ROOT/"
|
||||
echo " +-- config.git/ (bare repo: infrastructure config)"
|
||||
echo " +-- secrets.git/ (bare repo: encrypted secrets)"
|
||||
echo " +-- manifests.git/ (bare repo: k8s/deployment manifests)"
|
||||
echo ""
|
||||
|
||||
echo "=== Branch Structure (each repo) ==="
|
||||
echo " main - production state"
|
||||
echo " staging - pre-production testing"
|
||||
echo " dev - development changes"
|
||||
echo ""
|
||||
|
||||
echo "=== Post-Receive Hooks ==="
|
||||
echo " config.git: Validate YAML on push"
|
||||
echo " secrets.git: Verify GPG encryption"
|
||||
echo " manifests.git: Validate manifest syntax"
|
||||
echo ""
|
||||
|
||||
echo "=== Working Directories ==="
|
||||
echo " After setup, clone repos to working directories:"
|
||||
echo " git clone $GITOPS_ROOT/config.git ~/config"
|
||||
echo " git clone $GITOPS_ROOT/secrets.git ~/secrets"
|
||||
echo " git clone $GITOPS_ROOT/manifests.git ~/manifests"
|
||||
echo ""
|
||||
|
||||
# Check for existing repos
|
||||
for repo in config secrets manifests; do
|
||||
if [[ -d "$GITOPS_ROOT/${repo}.git" ]]; then
|
||||
log_warn "$repo.git already exists - apply will skip creation"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "============================================"
|
||||
echo " To apply: ./scripts/31_gitops_apply.sh"
|
||||
echo " To abort: Do nothing"
|
||||
echo " To rollback: ./scripts/rollback/undo_gitops.sh"
|
||||
echo "============================================"
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
193
operator-bootstrap/scripts/31_gitops_apply.sh
Executable file
193
operator-bootstrap/scripts/31_gitops_apply.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
: "${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() {
|
||||
# Expand ~ in GITOPS_ROOT
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
[[ -d "$GITOPS_ROOT" ]] || mkdir -p "$GITOPS_ROOT"
|
||||
}
|
||||
|
||||
create_bare_repo() {
|
||||
local name="$1"
|
||||
local description="$2"
|
||||
local repo_path="$GITOPS_ROOT/${name}.git"
|
||||
|
||||
if [[ -d "$repo_path" ]]; then
|
||||
log_info "Repository $name.git already exists - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Creating bare repository: $name.git"
|
||||
git init --bare "$repo_path"
|
||||
|
||||
# Set description
|
||||
echo "$description" > "$repo_path/description"
|
||||
|
||||
# Create initial branch structure by setting HEAD
|
||||
git -C "$repo_path" symbolic-ref HEAD refs/heads/main
|
||||
|
||||
log_info "Created $repo_path"
|
||||
}
|
||||
|
||||
create_hook() {
|
||||
local repo="$1"
|
||||
local hook_name="$2"
|
||||
local hook_content="$3"
|
||||
local hook_path="$GITOPS_ROOT/${repo}.git/hooks/$hook_name"
|
||||
|
||||
if [[ -f "$hook_path" ]] && [[ -x "$hook_path" ]]; then
|
||||
log_info "Hook $hook_name for $repo already exists - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "$hook_content" > "$hook_path"
|
||||
chmod +x "$hook_path"
|
||||
log_info "Created hook: $repo.git/hooks/$hook_name"
|
||||
}
|
||||
|
||||
setup_hooks() {
|
||||
# Config repo hook - validate YAML
|
||||
create_hook "config" "post-receive" '#!/usr/bin/env bash
|
||||
# Post-receive hook for config repository
|
||||
# Validates YAML files on push
|
||||
|
||||
echo "[config-hook] Validating pushed files..."
|
||||
|
||||
while read oldrev newrev refname; do
|
||||
if [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
|
||||
continue # Branch deleted
|
||||
fi
|
||||
|
||||
# List changed files
|
||||
files=$(git diff-tree --no-commit-id --name-only -r "$newrev" 2>/dev/null || echo "")
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == *.yml ]] || [[ "$file" == *.yaml ]]; then
|
||||
echo "[config-hook] YAML file changed: $file"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "[config-hook] Validation complete"
|
||||
'
|
||||
|
||||
# Secrets repo hook - verify GPG
|
||||
create_hook "secrets" "post-receive" '#!/usr/bin/env bash
|
||||
# Post-receive hook for secrets repository
|
||||
# Verifies that pushed files are GPG encrypted
|
||||
|
||||
echo "[secrets-hook] Verifying encryption..."
|
||||
|
||||
while read oldrev newrev refname; do
|
||||
if [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
files=$(git diff-tree --no-commit-id --name-only -r "$newrev" 2>/dev/null || echo "")
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == *.gpg ]]; then
|
||||
echo "[secrets-hook] Encrypted file: $file"
|
||||
elif [[ "$file" != ".gitignore" ]] && [[ "$file" != "README"* ]]; then
|
||||
echo "[secrets-hook] WARNING: Unencrypted file detected: $file"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "[secrets-hook] Verification complete"
|
||||
'
|
||||
|
||||
# Manifests repo hook - validate manifests
|
||||
create_hook "manifests" "post-receive" '#!/usr/bin/env bash
|
||||
# Post-receive hook for manifests repository
|
||||
# Validates Kubernetes/deployment manifests on push
|
||||
|
||||
echo "[manifests-hook] Validating manifests..."
|
||||
|
||||
while read oldrev newrev refname; do
|
||||
if [[ "$newrev" == "0000000000000000000000000000000000000000" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
files=$(git diff-tree --no-commit-id --name-only -r "$newrev" 2>/dev/null || echo "")
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == *.yml ]] || [[ "$file" == *.yaml ]]; then
|
||||
echo "[manifests-hook] Manifest changed: $file"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "[manifests-hook] Validation complete"
|
||||
'
|
||||
}
|
||||
|
||||
generate_manifest() {
|
||||
local manifest="$OUTPUT_DIR/gitops_manifest.json"
|
||||
|
||||
cat > "$manifest" <<EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"node": "$NODE_NAME",
|
||||
"gitops_root": "$GITOPS_ROOT",
|
||||
"repositories": {
|
||||
"config": "$GITOPS_ROOT/config.git",
|
||||
"secrets": "$GITOPS_ROOT/secrets.git",
|
||||
"manifests": "$GITOPS_ROOT/manifests.git"
|
||||
},
|
||||
"branches": ["main", "staging", "dev"],
|
||||
"hooks_installed": true,
|
||||
"clone_commands": [
|
||||
"git clone $GITOPS_ROOT/config.git ~/config",
|
||||
"git clone $GITOPS_ROOT/secrets.git ~/secrets",
|
||||
"git clone $GITOPS_ROOT/manifests.git ~/manifests"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
log_info "GitOps manifest written to $manifest"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
create_bare_repo "config" "Infrastructure configuration (YAML, TOML)"
|
||||
create_bare_repo "secrets" "Encrypted secrets (GPG)"
|
||||
create_bare_repo "manifests" "Kubernetes and deployment manifests"
|
||||
setup_hooks
|
||||
generate_manifest
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " GITOPS SETUP COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " Root: $GITOPS_ROOT"
|
||||
echo ""
|
||||
echo " To clone and start working:"
|
||||
echo " git clone $GITOPS_ROOT/config.git ~/config"
|
||||
echo " git clone $GITOPS_ROOT/secrets.git ~/secrets"
|
||||
echo " git clone $GITOPS_ROOT/manifests.git ~/manifests"
|
||||
echo ""
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
102
operator-bootstrap/scripts/40_editor_setup.sh
Executable file
102
operator-bootstrap/scripts/40_editor_setup.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
: "${ENABLE_KATE:=true}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
preflight() {
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
}
|
||||
|
||||
setup_kate() {
|
||||
if [[ "$ENABLE_KATE" != "true" ]]; then
|
||||
log_info "Kate setup disabled (ENABLE_KATE=$ENABLE_KATE)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v kate &>/dev/null; then
|
||||
log_warn "Kate not found - skipping editor setup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Setting up Kate project..."
|
||||
|
||||
local project_dir="$GITOPS_ROOT"
|
||||
local project_file="$project_dir/.kateproject"
|
||||
|
||||
[[ -d "$project_dir" ]] || mkdir -p "$project_dir"
|
||||
|
||||
if [[ -f "$project_file" ]]; then
|
||||
log_info "Kate project already exists at $project_file - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cat > "$project_file" <<EOF
|
||||
{
|
||||
"name": "$NODE_NAME Infrastructure",
|
||||
"files": [
|
||||
{
|
||||
"directory": ".",
|
||||
"filters": ["*.yml", "*.yaml", "*.toml", "*.json", "*.sh", "*.md"],
|
||||
"recursive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Kate project file created: $project_file"
|
||||
}
|
||||
|
||||
setup_vim() {
|
||||
# Create basic .vimrc additions for infrastructure work if vim exists
|
||||
if ! command -v vim &>/dev/null && ! command -v nvim &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local vimrc="$HOME/.vimrc"
|
||||
local marker="\" Added by operator-bootstrap"
|
||||
|
||||
if [[ -f "$vimrc" ]] && grep -q "$marker" "$vimrc" 2>/dev/null; then
|
||||
log_info "Vim config already updated - skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cat >> "$vimrc" <<EOF
|
||||
|
||||
$marker
|
||||
" YAML settings for infrastructure files
|
||||
autocmd FileType yaml setlocal ts=2 sts=2 sw=2 expandtab
|
||||
autocmd FileType json setlocal ts=2 sts=2 sw=2 expandtab
|
||||
|
||||
" Highlight trailing whitespace
|
||||
highlight ExtraWhitespace ctermbg=red guibg=red
|
||||
match ExtraWhitespace /\s\+$/
|
||||
EOF
|
||||
|
||||
log_info "Vim config updated with YAML/JSON settings"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
setup_kate
|
||||
setup_vim
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
181
operator-bootstrap/scripts/90_verify.sh
Executable file
181
operator-bootstrap/scripts/90_verify.sh
Executable file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
CHECKS_DIR="$SKILL_ROOT/checks"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${OPERATOR_EMAIL:=}"
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
: "${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; }
|
||||
|
||||
preflight() {
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
}
|
||||
|
||||
check_gpg() {
|
||||
if [[ -n "$OPERATOR_EMAIL" ]] && gpg --list-keys "$OPERATOR_EMAIL" &>/dev/null 2>&1; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
check_ssh() {
|
||||
if [[ -f "$HOME/.ssh/id_ed25519_${NODE_NAME}" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
check_pass() {
|
||||
if [[ -d "$HOME/.password-store" ]] && [[ -f "$HOME/.password-store/.gpg-id" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
check_tunnel() {
|
||||
if [[ -f "$HOME/.cloudflared/${TUNNEL_NAME}.json" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
check_gitops() {
|
||||
if [[ -d "$GITOPS_ROOT/config.git" ]] && \
|
||||
[[ -d "$GITOPS_ROOT/secrets.git" ]] && \
|
||||
[[ -d "$GITOPS_ROOT/manifests.git" ]]; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
run_external_check() {
|
||||
local check_script="$1"
|
||||
if [[ -x "$CHECKS_DIR/$check_script" ]]; then
|
||||
if "$CHECKS_DIR/$check_script" &>/dev/null; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
else
|
||||
echo "skip"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_status_matrix() {
|
||||
local status_file="$OUTPUT_DIR/status_matrix.json"
|
||||
|
||||
local gpg_ok=$(check_gpg)
|
||||
local ssh_ok=$(check_ssh)
|
||||
local pass_ok=$(check_pass)
|
||||
local tunnel_ok=$(check_tunnel)
|
||||
local gitops_ok=$(check_gitops)
|
||||
|
||||
# Build arrays for JSON
|
||||
local blockers=""
|
||||
local warnings=""
|
||||
local next_steps=""
|
||||
|
||||
if [[ "$gpg_ok" == "false" ]]; then
|
||||
blockers="${blockers}\"GPG key not found for $OPERATOR_EMAIL\","
|
||||
fi
|
||||
if [[ "$ssh_ok" == "false" ]]; then
|
||||
blockers="${blockers}\"SSH keys not found at ~/.ssh/id_ed25519_${NODE_NAME}\","
|
||||
fi
|
||||
if [[ "$pass_ok" == "false" ]]; then
|
||||
warnings="${warnings}\"Pass store not initialized\","
|
||||
fi
|
||||
if [[ "$tunnel_ok" == "false" ]]; then
|
||||
warnings="${warnings}\"Cloudflare tunnel not configured\","
|
||||
fi
|
||||
if [[ "$gitops_ok" == "false" ]]; then
|
||||
warnings="${warnings}\"GitOps repositories not created\","
|
||||
fi
|
||||
|
||||
if [[ "$tunnel_ok" == "true" ]]; then
|
||||
next_steps="${next_steps}\"Test tunnel: cloudflared tunnel --config ~/.cloudflared/config-${TUNNEL_NAME}.yml run\","
|
||||
fi
|
||||
if [[ "$gitops_ok" == "true" ]]; then
|
||||
next_steps="${next_steps}\"Clone repos: git clone $GITOPS_ROOT/config.git ~/config\","
|
||||
fi
|
||||
if [[ "$gpg_ok" == "true" ]] && [[ "$ssh_ok" == "true" ]]; then
|
||||
next_steps="${next_steps}\"Proceed to node-hardening skill\","
|
||||
fi
|
||||
|
||||
# Remove trailing commas and wrap in arrays
|
||||
blockers="[${blockers%,}]"
|
||||
warnings="[${warnings%,}]"
|
||||
next_steps="[${next_steps%,}]"
|
||||
|
||||
cat > "$status_file" <<EOF
|
||||
{
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"skill": "operator-bootstrap",
|
||||
"node": "$NODE_NAME",
|
||||
"checks": {
|
||||
"gpg_key": $gpg_ok,
|
||||
"ssh_keys": $ssh_ok,
|
||||
"pass_store": $pass_ok,
|
||||
"tunnel": $tunnel_ok,
|
||||
"gitops_repos": $gitops_ok
|
||||
},
|
||||
"blockers": $blockers,
|
||||
"warnings": $warnings,
|
||||
"next_steps": $next_steps
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Status matrix written to $status_file"
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " VERIFICATION SUMMARY"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " GPG Key: $gpg_ok"
|
||||
echo " SSH Keys: $ssh_ok"
|
||||
echo " Pass Store: $pass_ok"
|
||||
echo " Tunnel: $tunnel_ok"
|
||||
echo " GitOps: $gitops_ok"
|
||||
echo ""
|
||||
|
||||
# Return success only if no blockers
|
||||
if [[ "$gpg_ok" == "true" ]] && [[ "$ssh_ok" == "true" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
if generate_status_matrix; then
|
||||
log_info "All critical checks passed"
|
||||
else
|
||||
log_warn "Some checks failed - review status matrix"
|
||||
fi
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
197
operator-bootstrap/scripts/99_report.sh
Executable file
197
operator-bootstrap/scripts/99_report.sh
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${OPERATOR_NAME:=Unknown Operator}"
|
||||
: "${OPERATOR_EMAIL:=unknown@example.com}"
|
||||
: "${DOMAIN:=example.com}"
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
: "${TUNNEL_NAME:=$NODE_NAME-tunnel}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
|
||||
preflight() {
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
[[ -d "$OUTPUT_DIR" ]] || mkdir -p "$OUTPUT_DIR"
|
||||
}
|
||||
|
||||
get_gpg_fingerprint() {
|
||||
gpg --list-keys --with-colons "$OPERATOR_EMAIL" 2>/dev/null | \
|
||||
grep fpr | head -1 | cut -d: -f10 || echo "Not configured"
|
||||
}
|
||||
|
||||
get_ssh_key_count() {
|
||||
ls -1 "$HOME/.ssh/id_"*"_${NODE_NAME}" 2>/dev/null | wc -l || echo "0"
|
||||
}
|
||||
|
||||
get_pass_status() {
|
||||
if [[ -d "$HOME/.password-store" ]]; then
|
||||
echo "Initialized ($(cat "$HOME/.password-store/.gpg-id" 2>/dev/null || echo "unknown GPG ID"))"
|
||||
else
|
||||
echo "Not initialized"
|
||||
fi
|
||||
}
|
||||
|
||||
get_tunnel_status() {
|
||||
if [[ -f "$HOME/.cloudflared/config-${TUNNEL_NAME}.yml" ]]; then
|
||||
echo "Configured"
|
||||
else
|
||||
echo "Not configured"
|
||||
fi
|
||||
}
|
||||
|
||||
get_gitops_status() {
|
||||
local count=0
|
||||
[[ -d "$GITOPS_ROOT/config.git" ]] && ((count++))
|
||||
[[ -d "$GITOPS_ROOT/secrets.git" ]] && ((count++))
|
||||
[[ -d "$GITOPS_ROOT/manifests.git" ]] && ((count++))
|
||||
echo "$count/3 repositories"
|
||||
}
|
||||
|
||||
main() {
|
||||
preflight
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
|
||||
local report="$OUTPUT_DIR/audit_report.md"
|
||||
local status_file="$OUTPUT_DIR/status_matrix.json"
|
||||
|
||||
cat > "$report" <<EOF
|
||||
# Operator Bootstrap Audit Report
|
||||
|
||||
**Generated:** $(date -Iseconds)
|
||||
**Node:** $NODE_NAME
|
||||
**Operator:** $OPERATOR_NAME <$OPERATOR_EMAIL>
|
||||
**Skill Version:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report documents the bootstrap operations performed on **$NODE_NAME**
|
||||
for sovereign EU infrastructure deployment.
|
||||
|
||||
---
|
||||
|
||||
## Components Status
|
||||
|
||||
### 1. Identity
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| GPG Key | $(get_gpg_fingerprint) |
|
||||
| SSH Keys | $(get_ssh_key_count) key pair(s) |
|
||||
| SSH Config | $(grep -q "Host $NODE_NAME" "$HOME/.ssh/config" 2>/dev/null && echo "Updated" || echo "Not updated") |
|
||||
|
||||
### 2. Secrets Management
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Pass Store | $(get_pass_status) |
|
||||
| Location | ~/.password-store |
|
||||
|
||||
### 3. Cloudflare Tunnel
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Tunnel Name | $TUNNEL_NAME |
|
||||
| Configuration | $(get_tunnel_status) |
|
||||
| SSH Endpoint | ssh.$DOMAIN |
|
||||
|
||||
### 4. GitOps Repositories
|
||||
|
||||
| Component | Status |
|
||||
|-----------|--------|
|
||||
| Repository Count | $(get_gitops_status) |
|
||||
| Root Directory | $GITOPS_ROOT |
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
$(if [[ -f "$status_file" ]]; then
|
||||
echo '```json'
|
||||
cat "$status_file"
|
||||
echo '```'
|
||||
else
|
||||
echo "Status matrix not found. Run 90_verify.sh first."
|
||||
fi)
|
||||
|
||||
---
|
||||
|
||||
## EU Compliance Declaration
|
||||
|
||||
| Aspect | Value |
|
||||
|--------|-------|
|
||||
| Data Residency | EU (Ireland - Dublin) |
|
||||
| GDPR Applicable | Yes |
|
||||
| Jurisdiction | Irish Law |
|
||||
| Encryption | GPG (local keys only) |
|
||||
|
||||
---
|
||||
|
||||
## Rollback Procedures
|
||||
|
||||
If any component needs to be reverted, use these scripts in order:
|
||||
|
||||
1. **Tunnel:** \`./scripts/rollback/undo_tunnel.sh\`
|
||||
2. **GitOps:** \`./scripts/rollback/undo_gitops.sh\`
|
||||
3. **SSH Config:** \`./scripts/rollback/undo_ssh_config.sh\`
|
||||
4. **Identity:** \`./scripts/rollback/undo_identity.sh\`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test tunnel connectivity:
|
||||
\`\`\`bash
|
||||
cloudflared tunnel --config ~/.cloudflared/config-${TUNNEL_NAME}.yml run
|
||||
\`\`\`
|
||||
|
||||
2. Clone GitOps repositories:
|
||||
\`\`\`bash
|
||||
git clone $GITOPS_ROOT/config.git ~/config
|
||||
git clone $GITOPS_ROOT/secrets.git ~/secrets
|
||||
git clone $GITOPS_ROOT/manifests.git ~/manifests
|
||||
\`\`\`
|
||||
|
||||
3. Proceed to **node-hardening** skill for security configuration
|
||||
|
||||
4. Document this bootstrap in LAWCHAIN (if applicable)
|
||||
|
||||
---
|
||||
|
||||
## Artifact Locations
|
||||
|
||||
| Artifact | Path |
|
||||
|----------|------|
|
||||
| Identity Manifest | $OUTPUT_DIR/identity_manifest.json |
|
||||
| Secrets Manifest | $OUTPUT_DIR/secrets_manifest.json |
|
||||
| Tunnel Config | $OUTPUT_DIR/tunnel_config.json |
|
||||
| GitOps Manifest | $OUTPUT_DIR/gitops_manifest.json |
|
||||
| Status Matrix | $OUTPUT_DIR/status_matrix.json |
|
||||
| This Report | $OUTPUT_DIR/audit_report.md |
|
||||
|
||||
---
|
||||
|
||||
*Report generated by operator-bootstrap skill v1.0.0*
|
||||
*$(date -Iseconds)*
|
||||
EOF
|
||||
|
||||
log_info "Audit report written to $report"
|
||||
|
||||
# Display the report
|
||||
echo ""
|
||||
cat "$report"
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
82
operator-bootstrap/scripts/rollback/undo_gitops.sh
Executable file
82
operator-bootstrap/scripts/rollback/undo_gitops.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${GITOPS_ROOT:=$HOME/infrastructure}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME - ROLLBACK GitOps..."
|
||||
|
||||
# Expand ~
|
||||
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " GITOPS ROLLBACK"
|
||||
echo " Root: $GITOPS_ROOT"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# Check what exists
|
||||
local repos_found=0
|
||||
for repo in config secrets manifests; do
|
||||
if [[ -d "$GITOPS_ROOT/${repo}.git" ]]; then
|
||||
echo " Found: ${repo}.git"
|
||||
((repos_found++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $repos_found -eq 0 ]]; then
|
||||
log_info "No GitOps repositories found - nothing to rollback"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "This will ARCHIVE (not delete) the repositories to:"
|
||||
echo " $GITOPS_ROOT/archived-$(date +%Y%m%d%H%M%S)/"
|
||||
echo ""
|
||||
|
||||
read -p "Type 'CONFIRM' to proceed: " confirm
|
||||
|
||||
if [[ "$confirm" != "CONFIRM" ]]; then
|
||||
log_info "Aborted - no changes made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create archive directory
|
||||
local archive_dir="$GITOPS_ROOT/archived-$(date +%Y%m%d%H%M%S)"
|
||||
mkdir -p "$archive_dir"
|
||||
|
||||
# Move repositories to archive
|
||||
for repo in config secrets manifests; do
|
||||
if [[ -d "$GITOPS_ROOT/${repo}.git" ]]; then
|
||||
mv "$GITOPS_ROOT/${repo}.git" "$archive_dir/"
|
||||
log_info "Archived: ${repo}.git -> $archive_dir/"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " GITOPS ROLLBACK COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Repositories archived to: $archive_dir"
|
||||
echo ""
|
||||
echo "To permanently delete:"
|
||||
echo " rm -rf $archive_dir"
|
||||
echo ""
|
||||
echo "To restore:"
|
||||
echo " mv $archive_dir/*.git $GITOPS_ROOT/"
|
||||
echo ""
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
76
operator-bootstrap/scripts/rollback/undo_identity.sh
Executable file
76
operator-bootstrap/scripts/rollback/undo_identity.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${OPERATOR_EMAIL:=}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME - ROLLBACK identity..."
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " IDENTITY ROLLBACK"
|
||||
echo " WARNING: This will remove GPG and SSH keys!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "This will:"
|
||||
echo " - Remove SSH keys: ~/.ssh/id_*_${NODE_NAME}*"
|
||||
echo " - Restore SSH config from backup"
|
||||
if [[ -n "$OPERATOR_EMAIL" ]]; then
|
||||
echo " - Prompt for GPG key removal (manual step)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
read -p "Type 'CONFIRM' to proceed: " confirm
|
||||
|
||||
if [[ "$confirm" != "CONFIRM" ]]; then
|
||||
log_info "Aborted - no changes made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remove SSH keys
|
||||
log_info "Removing SSH keys..."
|
||||
rm -f "$HOME/.ssh/id_ed25519_${NODE_NAME}"
|
||||
rm -f "$HOME/.ssh/id_ed25519_${NODE_NAME}.pub"
|
||||
rm -f "$HOME/.ssh/id_rsa_${NODE_NAME}"
|
||||
rm -f "$HOME/.ssh/id_rsa_${NODE_NAME}.pub"
|
||||
log_info "SSH keys removed"
|
||||
|
||||
# Restore SSH config backup
|
||||
local latest_backup
|
||||
latest_backup=$(ls -t "$HOME/.ssh/config.bak."* 2>/dev/null | head -1 || true)
|
||||
if [[ -n "$latest_backup" ]]; then
|
||||
cp "$latest_backup" "$HOME/.ssh/config"
|
||||
log_info "SSH config restored from $latest_backup"
|
||||
else
|
||||
log_warn "No SSH config backup found"
|
||||
fi
|
||||
|
||||
# GPG key removal guidance
|
||||
if [[ -n "$OPERATOR_EMAIL" ]]; then
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " GPG KEY REMOVAL (MANUAL STEP)"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "To remove the GPG key, run these commands:"
|
||||
echo ""
|
||||
echo " gpg --delete-secret-keys $OPERATOR_EMAIL"
|
||||
echo " gpg --delete-keys $OPERATOR_EMAIL"
|
||||
echo ""
|
||||
log_warn "GPG key requires manual removal for safety"
|
||||
fi
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
63
operator-bootstrap/scripts/rollback/undo_ssh_config.sh
Executable file
63
operator-bootstrap/scripts/rollback/undo_ssh_config.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME - ROLLBACK SSH config..."
|
||||
|
||||
local config="$HOME/.ssh/config"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " SSH CONFIG ROLLBACK"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# List available backups
|
||||
echo "Available backups:"
|
||||
ls -la "$HOME/.ssh/config.bak."* 2>/dev/null || echo " (none found)"
|
||||
echo ""
|
||||
|
||||
local latest_backup
|
||||
latest_backup=$(ls -t "$HOME/.ssh/config.bak."* 2>/dev/null | head -1 || true)
|
||||
|
||||
if [[ -z "$latest_backup" ]]; then
|
||||
log_warn "No backup files found"
|
||||
echo ""
|
||||
echo "Alternative: Manually remove the $NODE_NAME entry from ~/.ssh/config"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Latest backup: $latest_backup"
|
||||
echo ""
|
||||
read -p "Restore from this backup? (y/N): " confirm
|
||||
|
||||
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
||||
log_info "Aborted - no changes made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create a backup of current config before restoring
|
||||
if [[ -f "$config" ]]; then
|
||||
cp "$config" "$config.pre-rollback.$(date +%Y%m%d%H%M%S)"
|
||||
log_info "Current config backed up"
|
||||
fi
|
||||
|
||||
# Restore from backup
|
||||
cp "$latest_backup" "$config"
|
||||
chmod 600 "$config"
|
||||
log_info "SSH config restored from $latest_backup"
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
81
operator-bootstrap/scripts/rollback/undo_tunnel.sh
Executable file
81
operator-bootstrap/scripts/rollback/undo_tunnel.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
|
||||
# === CONFIGURATION ===
|
||||
: "${NODE_NAME:=node-a}"
|
||||
: "${TUNNEL_NAME:=$NODE_NAME-tunnel}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn() { echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME - ROLLBACK tunnel..."
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " TUNNEL ROLLBACK"
|
||||
echo " Tunnel: $TUNNEL_NAME"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "This will:"
|
||||
echo " - Stop and disable the systemd service"
|
||||
echo " - Delete the tunnel from Cloudflare"
|
||||
echo " - Remove local credential and config files"
|
||||
echo " - Remove tunnel ID from pass (if stored)"
|
||||
echo ""
|
||||
|
||||
read -p "Type 'CONFIRM' to proceed: " confirm
|
||||
|
||||
if [[ "$confirm" != "CONFIRM" ]]; then
|
||||
log_info "Aborted - no changes made"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stop and disable service
|
||||
log_info "Stopping systemd service..."
|
||||
systemctl --user stop "cloudflared-$TUNNEL_NAME" 2>/dev/null || true
|
||||
systemctl --user disable "cloudflared-$TUNNEL_NAME" 2>/dev/null || true
|
||||
rm -f "$HOME/.config/systemd/user/cloudflared-$TUNNEL_NAME.service" 2>/dev/null || true
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
log_info "Service stopped and disabled"
|
||||
|
||||
# Delete tunnel from Cloudflare
|
||||
log_info "Deleting tunnel from Cloudflare..."
|
||||
if cloudflared tunnel delete "$TUNNEL_NAME" 2>/dev/null; then
|
||||
log_info "Tunnel deleted from Cloudflare"
|
||||
else
|
||||
log_warn "Could not delete tunnel - may need manual cleanup"
|
||||
log_warn "Check: https://dash.cloudflare.com -> Zero Trust -> Tunnels"
|
||||
fi
|
||||
|
||||
# Remove local files
|
||||
log_info "Removing local files..."
|
||||
rm -f "$HOME/.cloudflared/$TUNNEL_NAME.json"
|
||||
rm -f "$HOME/.cloudflared/config-$TUNNEL_NAME.yml"
|
||||
log_info "Local files removed"
|
||||
|
||||
# Remove from pass
|
||||
if command -v pass &>/dev/null && [[ -d "$HOME/.password-store" ]]; then
|
||||
log_info "Removing tunnel ID from pass..."
|
||||
pass rm -f "infrastructure/cloudflare/tunnel-id" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " TUNNEL ROLLBACK COMPLETE"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Note: DNS records may still exist in Cloudflare."
|
||||
echo "Remove them manually if needed:"
|
||||
echo " https://dash.cloudflare.com -> DNS"
|
||||
echo ""
|
||||
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
Reference in New Issue
Block a user