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:
Vault Sovereign
2025-12-27 00:25:00 +00:00
commit eac77ef7b4
213 changed files with 11724 additions and 0 deletions

154
operator-bootstrap/SKILL.md Normal file
View File

@@ -0,0 +1,154 @@
---
name: operator-bootstrap
description: >
Bootstrap Node A for sovereign EU infrastructure. Initializes operator identity
(GPG/SSH keys), configures secrets management (pass), establishes Cloudflare
tunnels for remote access, and creates GitOps repository structure. Use when
setting up the foundational node of self-hosted infrastructure. Triggers:
'bootstrap node A', 'initialize sovereign infrastructure', 'set up operator
identity', 'configure cloudflare tunnel', 'initialize gitops', 'first node
setup', 'foundation infrastructure setup'.
version: 1.0.0
---
# Operator Bootstrap
Foundation skill for establishing Node A in a sovereign EU infrastructure. All other infrastructure components depend on this skill completing successfully.
## Quick Start
```bash
# Set required parameters
export OPERATOR_NAME="Your Name"
export OPERATOR_EMAIL="you@domain.com"
export DOMAIN="yourdomain.com"
export CF_ACCOUNT_ID="your-cloudflare-account-id"
# Run in sequence
./scripts/00_preflight.sh
./scripts/01_identity_plan.sh
./scripts/02_identity_apply.sh
./scripts/10_secrets_guide.sh # Interactive
./scripts/20_tunnel_plan.sh
./scripts/21_tunnel_apply.sh
./scripts/30_gitops_plan.sh
./scripts/31_gitops_apply.sh
./scripts/40_editor_setup.sh
./scripts/90_verify.sh
./scripts/99_report.sh
```
## Workflow
### Phase 1: Preflight (00)
Check dependencies: gpg, ssh-keygen, pass, cloudflared, git.
Verify network connectivity and EU data residency requirements.
### Phase 2: Identity (01-02)
**Two-phase operation with rollback support.**
Plan phase shows:
- GPG key parameters (4096-bit RSA, operator identity)
- SSH key types (Ed25519 primary, RSA fallback)
- Proposed file locations
Apply phase executes:
- GPG master key generation (prompted passphrase)
- SSH keypair generation
- SSH config updates
Rollback: `./scripts/rollback/undo_identity.sh`
### Phase 3: Secrets (10)
**Guided interactive setup - never automated.**
Operator is guided through:
1. Initialize pass with GPG key
2. Create initial password structure
3. Store critical secrets (tunnel token, etc.)
4. Verify encryption/decryption
### Phase 4: Tunnel (20-21)
**Two-phase operation with rollback support.**
Plan phase shows:
- Proposed tunnel name and ingress rules
- DNS entries to be created
- Service mappings
Apply phase executes:
- Cloudflare tunnel creation
- Credential storage in pass
- systemd service installation
Rollback: `./scripts/rollback/undo_tunnel.sh`
### Phase 5: GitOps (30-31)
**Two-phase operation with rollback support.**
Plan phase shows:
- Bare repository locations
- Branch structure
- Hook scripts
Apply phase executes:
- Create bare repos for config, secrets-encrypted, manifests
- Initialize with sensible defaults
- Configure receive hooks
Rollback: `./scripts/rollback/undo_gitops.sh`
### Phase 6: Editor (40)
Configure Kate (if available) with:
- Project file for infrastructure
- Syntax highlighting for YAML/TOML
- Git integration
### Phase 7: Verification (90-99)
Generate JSON status matrix and human-readable audit report.
## Inputs
| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| OPERATOR_NAME | Yes | - | Full name for GPG key |
| OPERATOR_EMAIL | Yes | - | Email for GPG key |
| DOMAIN | Yes | - | Primary domain |
| CF_ACCOUNT_ID | Yes | - | Cloudflare account ID |
| NODE_NAME | No | node-a | Hostname for this node |
| GITOPS_ROOT | No | ~/infrastructure | Root for GitOps repos |
| SSH_KEY_COMMENT | No | node-a-operator | SSH key comment |
| GPG_KEY_SIZE | No | 4096 | GPG key size in bits |
| GPG_KEY_EXPIRE | No | 2y | GPG key expiration |
| TUNNEL_NAME | No | node-a-tunnel | Cloudflare tunnel name |
| ENABLE_KATE | No | true | Enable Kate editor setup |
## Outputs
| File | Description |
|------|-------------|
| `outputs/identity_manifest.json` | Record of created keys |
| `outputs/secrets_manifest.json` | Secrets structure record |
| `outputs/tunnel_config.json` | Tunnel configuration |
| `outputs/gitops_manifest.json` | Repository locations |
| `outputs/status_matrix.json` | Verification results |
| `outputs/audit_report.md` | Human-readable audit trail |
## Safety Guarantees
1. **All risky operations are two-phase** (plan/apply)
2. **Secrets are never automated** - guided enrollment only
3. **Rollback scripts provided** for identity, tunnel, SSH config, GitOps
4. **All scripts are idempotent** - safe to run multiple times
5. **Audit trail generated** for compliance
## EU Compliance
- Data Residency: EU (Ireland - Dublin)
- GDPR Applicable: Yes
- Jurisdiction: Irish law
## References
- [EU Data Sovereignty](references/eu_data_sovereignty.md)
- [Cloudflare Tunnel Setup](references/cloudflare_tunnel_setup.md)

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Check: GitOps repositories exist
# Returns 0 if all repos exist, 1 otherwise
set -euo pipefail
: "${GITOPS_ROOT:=$HOME/infrastructure}"
# Expand ~
GITOPS_ROOT="${GITOPS_ROOT/#\~/$HOME}"
[[ -d "$GITOPS_ROOT/config.git" ]] && \
[[ -d "$GITOPS_ROOT/secrets.git" ]] && \
[[ -d "$GITOPS_ROOT/manifests.git" ]]

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Check: GPG key exists for operator
# Returns 0 if GPG key found, 1 otherwise
set -euo pipefail
: "${OPERATOR_EMAIL:=}"
if [[ -z "$OPERATOR_EMAIL" ]]; then
exit 1
fi
gpg --list-keys "$OPERATOR_EMAIL" &>/dev/null

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Check: Pass store initialized
# Returns 0 if pass store exists, 1 otherwise
set -euo pipefail
[[ -d "$HOME/.password-store" ]] && [[ -f "$HOME/.password-store/.gpg-id" ]]

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Check: SSH keys exist for node
# Returns 0 if SSH keys found, 1 otherwise
set -euo pipefail
: "${NODE_NAME:=node-a}"
[[ -f "$HOME/.ssh/id_ed25519_${NODE_NAME}" ]]

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Check: Cloudflare tunnel configured
# Returns 0 if tunnel credentials exist, 1 otherwise
set -euo pipefail
: "${NODE_NAME:=node-a}"
: "${TUNNEL_NAME:=$NODE_NAME-tunnel}"
[[ -f "$HOME/.cloudflared/${TUNNEL_NAME}.json" ]]

View File

@@ -0,0 +1,64 @@
{
"version": "1.0.0",
"skill": "operator-bootstrap",
"description": "Node A foundation for sovereign EU infrastructure",
"parameters": {
"required": [
"OPERATOR_NAME",
"OPERATOR_EMAIL",
"DOMAIN",
"CF_ACCOUNT_ID"
],
"optional": {
"NODE_NAME": "node-a",
"GITOPS_ROOT": "~/infrastructure",
"SSH_KEY_COMMENT": "node-a-operator",
"GPG_KEY_SIZE": 4096,
"GPG_KEY_EXPIRE": "2y",
"TUNNEL_NAME": "node-a-tunnel",
"ENABLE_KATE": true
}
},
"phases": {
"preflight": ["00_preflight.sh"],
"identity": {
"plan": ["01_identity_plan.sh"],
"apply": ["02_identity_apply.sh"],
"rollback": ["rollback/undo_identity.sh"]
},
"secrets": {
"guide": ["10_secrets_guide.sh"],
"interactive": true
},
"tunnel": {
"plan": ["20_tunnel_plan.sh"],
"apply": ["21_tunnel_apply.sh"],
"rollback": ["rollback/undo_tunnel.sh"]
},
"gitops": {
"plan": ["30_gitops_plan.sh"],
"apply": ["31_gitops_apply.sh"],
"rollback": ["rollback/undo_gitops.sh"]
},
"editor": ["40_editor_setup.sh"],
"verify": ["90_verify.sh"],
"report": ["99_report.sh"]
},
"checks": {
"identity": ["check_gpg.sh", "check_ssh.sh"],
"secrets": ["check_pass.sh"],
"tunnel": ["check_tunnel.sh"],
"gitops": ["check_gitops.sh"]
},
"rollback_order": [
"undo_tunnel.sh",
"undo_gitops.sh",
"undo_ssh_config.sh",
"undo_identity.sh"
],
"eu_compliance": {
"data_residency": "EU",
"jurisdiction": "Ireland",
"gdpr_applicable": true
}
}

View File

@@ -0,0 +1,192 @@
# Cloudflare Tunnel Setup Guide
## Overview
Cloudflare Tunnels (formerly Argo Tunnels) provide secure, outbound-only connections from your infrastructure to Cloudflare's edge, eliminating the need for public IP addresses or open firewall ports.
## Prerequisites
### Required
- Cloudflare account (free tier works)
- Domain added to Cloudflare DNS
- `cloudflared` CLI installed
### Installation (Linux)
```bash
# Debian/Ubuntu
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
# Or via package manager (if available)
sudo apt install cloudflared
```
### Installation (Termux/Android)
```bash
pkg install cloudflared
```
## Authentication
Before creating tunnels, authenticate with Cloudflare:
```bash
cloudflared tunnel login
```
This opens a browser for authentication and stores a certificate at `~/.cloudflared/cert.pem`.
## Tunnel Lifecycle
### Create Tunnel
```bash
cloudflared tunnel create my-tunnel
```
Creates credentials at `~/.cloudflared/<tunnel-id>.json`.
### Configure Tunnel
Create `~/.cloudflared/config.yml`:
```yaml
tunnel: my-tunnel
credentials-file: /path/to/credentials.json
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
- hostname: web.example.com
service: http://localhost:8080
- service: http_status:404
```
### Route DNS
```bash
cloudflared tunnel route dns my-tunnel ssh.example.com
```
### Run Tunnel
```bash
cloudflared tunnel run my-tunnel
```
Or with explicit config:
```bash
cloudflared tunnel --config ~/.cloudflared/config.yml run
```
## SSH Access via Tunnel
### Server Side
Tunnel config includes SSH service:
```yaml
ingress:
- hostname: ssh.example.com
service: ssh://localhost:22
```
### Client Side
Option 1: Using ProxyCommand:
```
Host my-server
HostName ssh.example.com
ProxyCommand cloudflared access ssh --hostname %h
```
Option 2: Using `cloudflared access`:
```bash
cloudflared access ssh --hostname ssh.example.com
```
## Cloudflare Access (Optional)
For additional authentication:
1. Go to Cloudflare Zero Trust dashboard
2. Create an Access Application
3. Define authentication policies (email, SSO, etc.)
4. Apply to SSH hostname
## systemd Service
### User Service
```ini
[Unit]
Description=Cloudflare Tunnel
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/cloudflared tunnel --config /path/to/config.yml run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
Enable:
```bash
systemctl --user enable cloudflared-tunnel
systemctl --user start cloudflared-tunnel
```
### System Service
```bash
sudo cloudflared service install
```
## Troubleshooting
### Check Tunnel Status
```bash
cloudflared tunnel info my-tunnel
```
### View Logs
```bash
journalctl --user -u cloudflared-tunnel -f
```
### Test Connectivity
```bash
curl -v https://ssh.example.com
```
### Common Issues
1. **Certificate expired**: Re-run `cloudflared tunnel login`
2. **DNS not resolving**: Check Cloudflare DNS for CNAME record
3. **Connection refused**: Verify local service is running
## Security Considerations
- Tunnel credentials (`*.json`) are sensitive - protect like SSH keys
- Use Cloudflare Access for authentication on sensitive services
- Regularly rotate tunnel credentials
- Monitor tunnel connections in Cloudflare dashboard
## References
- [Cloudflare Tunnel Docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/)
- [cloudflared GitHub](https://github.com/cloudflare/cloudflared)
- [Zero Trust Dashboard](https://one.dash.cloudflare.com/)

View File

@@ -0,0 +1,69 @@
# EU Data Sovereignty Requirements
## Overview
This skill operates under EU data sovereignty principles, ensuring all data remains within EU jurisdiction and complies with applicable regulations.
## Regulatory Framework
### GDPR (General Data Protection Regulation)
Key requirements for infrastructure operators:
1. **Data Residency** - Personal data of EU residents must be processed in compliance with GDPR, regardless of where processing occurs
2. **Legal Basis** - All data processing must have a valid legal basis (consent, contract, legal obligation, vital interests, public task, or legitimate interests)
3. **Data Subject Rights** - Infrastructure must support right to access, rectification, erasure, portability, and objection
4. **Security** - Appropriate technical and organizational measures required
### Schrems II Implications
Following the Court of Justice ruling (C-311/18):
- Standard Contractual Clauses alone may not be sufficient for US transfers
- Supplementary measures may be required
- Self-hosted EU infrastructure avoids many transfer concerns
## Implementation in This Skill
### Encryption
- **GPG Keys**: Generated and stored locally on EU infrastructure
- **No Cloud KMS**: Keys never leave the operator's control
- **Pass Store**: Encrypted at rest with local GPG keys
### Network Access
- **Cloudflare Tunnels**: Traffic routed through EU Cloudflare PoPs when possible
- **No Direct US Routing**: Configure Cloudflare region preferences
- **SSH Keys**: Ed25519 primary (modern, efficient)
### Data Storage
- **GitOps Repositories**: Stored on local EU infrastructure
- **Secrets**: Encrypted before storage, never in plaintext
- **Audit Logs**: Retained locally, not exported to non-EU services
## Jurisdiction
This skill is designed for operators in **Ireland (Dublin)** and assumes:
- Irish law applies as the primary jurisdiction
- Data Protection Commission (DPC) is the supervisory authority
- Irish implementation of GDPR applies
## Compliance Checklist
Before deploying infrastructure bootstrapped with this skill:
- [ ] Identify lawful basis for any personal data processing
- [ ] Document data flows and storage locations
- [ ] Implement appropriate access controls
- [ ] Establish incident response procedures
- [ ] Configure data retention policies
- [ ] Prepare for data subject requests
## References
- [GDPR Official Text](https://eur-lex.europa.eu/eli/reg/2016/679/oj)
- [DPC Guidance](https://www.dataprotection.ie/en/organisations)
- [EDPB Guidelines](https://edpb.europa.eu/our-work-tools/general-guidance_en)

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View 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 "$@"

View File

@@ -0,0 +1,53 @@
# Bootstrap Audit Record Template
# Variables: {{TIMESTAMP}}, {{NODE_NAME}}, {{OPERATOR_NAME}}, {{DOMAIN}}
## Metadata
| Field | Value |
|-------|-------|
| Timestamp | {{TIMESTAMP}} |
| Node | {{NODE_NAME}} |
| Operator | {{OPERATOR_NAME}} |
| Domain | {{DOMAIN}} |
| Skill Version | 1.0.0 |
## Actions Performed
{{ACTIONS}}
## Artifacts Created
{{ARTIFACTS}}
## Verification Status
{{STATUS}}
## Evidence Hashes
| Artifact | BLAKE3 Hash |
|----------|-------------|
{{EVIDENCE_HASHES}}
## Signature Block
This record should be:
1. Signed with the operator's GPG key
2. Committed to the secrets.git repository
3. Anchored to LAWCHAIN (if applicable)
### GPG Signature Command
```bash
gpg --armor --detach-sign audit_record.md
```
### Verification Command
```bash
gpg --verify audit_record.md.asc audit_record.md
```
---
*Record generated by operator-bootstrap skill*

View File

@@ -0,0 +1,30 @@
# Cloudflare Tunnel Configuration Template
# Generated by operator-bootstrap
# Variables: {{TUNNEL_NAME}}, {{DOMAIN}}, {{HOME}}
tunnel: {{TUNNEL_NAME}}
credentials-file: {{HOME}}/.cloudflared/{{TUNNEL_NAME}}.json
# Ingress rules define how traffic is routed
ingress:
# SSH access via Cloudflare Access
# Requires Cloudflare Access policy for authentication
- hostname: ssh.{{DOMAIN}}
service: ssh://localhost:22
# Web services (uncomment and modify as needed)
# - hostname: api.{{DOMAIN}}
# service: http://localhost:8080
#
# - hostname: dashboard.{{DOMAIN}}
# service: http://localhost:3000
# Catch-all for undefined hostnames
- service: http_status:404
# Optional: Metrics endpoint
# metrics: localhost:2000
# Optional: Logging
# loglevel: info
# logfile: /var/log/cloudflared.log

View File

@@ -0,0 +1,29 @@
# SSH Configuration Template
# Generated by operator-bootstrap
# Variables: {{NODE_NAME}}, {{NODE_IP}}, {{DOMAIN}}, {{OPERATOR_USER}}
# Direct SSH to node (when on same network)
Host {{NODE_NAME}}
HostName {{NODE_IP}}
User {{OPERATOR_USER}}
IdentityFile ~/.ssh/id_ed25519_{{NODE_NAME}}
IdentitiesOnly yes
ForwardAgent no
AddKeysToAgent yes
# SSH via Cloudflare Tunnel (remote access)
Host {{NODE_NAME}}-tunnel
HostName ssh.{{DOMAIN}}
User {{OPERATOR_USER}}
IdentityFile ~/.ssh/id_ed25519_{{NODE_NAME}}
IdentitiesOnly yes
ProxyCommand cloudflared access ssh --hostname %h
# Fallback with RSA key (for legacy systems)
Host {{NODE_NAME}}-rsa
HostName {{NODE_IP}}
User {{OPERATOR_USER}}
IdentityFile ~/.ssh/id_rsa_{{NODE_NAME}}
IdentitiesOnly yes
PubkeyAcceptedAlgorithms +ssh-rsa
HostkeyAlgorithms +ssh-rsa