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:
105
cloudflare-tunnel-manager/SKILL.md
Normal file
105
cloudflare-tunnel-manager/SKILL.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: cloudflare-tunnel-manager
|
||||
description: >
|
||||
Plan/apply/rollback for Cloudflare Tunnel lifecycle (create, configure,
|
||||
route DNS, run as service). Includes DRY_RUN safety gates, status matrix,
|
||||
and audit report. Triggers: 'cloudflare tunnel', 'create tunnel', 'tunnel plan',
|
||||
'tunnel rollback', 'cloudflared config', 'dns route'.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Cloudflare Tunnel Manager
|
||||
|
||||
Tier 1 skill for managing **Cloudflare Tunnels** safely:
|
||||
- **Plan → Apply** workflow (two-phase)
|
||||
- **Rollback** scripts for DNS route, service, and tunnel delete
|
||||
- Verification + audit report
|
||||
|
||||
Designed for sovereign Node A style setups where you terminate TLS at Cloudflare
|
||||
and route traffic to a local service over a tunnel.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/cloudflare-tunnel-manager
|
||||
|
||||
# Required
|
||||
export CF_API_TOKEN="..." # Cloudflare API token
|
||||
export CF_ACCOUNT_ID="..." # Cloudflare account ID
|
||||
|
||||
# Tunnel identity
|
||||
export TUNNEL_NAME="node-a-tunnel"
|
||||
export ZONE_NAME="example.com" # domain in Cloudflare
|
||||
export HOSTNAME="node-a.example.com"
|
||||
|
||||
# Local origin (what tunnel forwards to)
|
||||
export LOCAL_SERVICE="http://127.0.0.1:9110"
|
||||
|
||||
# Safety
|
||||
export DRY_RUN=1
|
||||
export REQUIRE_CONFIRM=1
|
||||
export CONFIRM_PHRASE="I UNDERSTAND THIS CAN CHANGE DNS AND TUNNEL ROUTES"
|
||||
|
||||
./scripts/00_preflight.sh
|
||||
./scripts/10_tunnel_plan.sh
|
||||
./scripts/20_dns_plan.sh
|
||||
./scripts/30_service_plan.sh
|
||||
|
||||
export DRY_RUN=0
|
||||
./scripts/11_tunnel_apply.sh
|
||||
./scripts/21_dns_apply.sh
|
||||
./scripts/31_service_apply.sh
|
||||
|
||||
./scripts/90_verify.sh
|
||||
./scripts/99_report.sh
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---|---:|---|---|
|
||||
| CF_API_TOKEN | Yes | (none) | Cloudflare API token with Tunnel + DNS permissions |
|
||||
| CF_ACCOUNT_ID | Yes | (none) | Cloudflare account ID |
|
||||
| TUNNEL_NAME | Yes | (none) | Tunnel name |
|
||||
| ZONE_NAME | Yes | (none) | Zone/domain in Cloudflare (e.g., example.com) |
|
||||
| HOSTNAME | Yes | (none) | DNS hostname to route (e.g., node-a.example.com) |
|
||||
| LOCAL_SERVICE | Yes | (none) | Local origin URL (e.g., http://127.0.0.1:9110) |
|
||||
| CONFIG_DIR | No | outputs/config | Where generated config lives |
|
||||
| SERVICE_NAME | No | cloudflared-tunnel | systemd unit name |
|
||||
| DRY_RUN | No | 1 | Apply scripts refuse unless DRY_RUN=0 |
|
||||
| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase |
|
||||
| CONFIRM_PHRASE | No | I UNDERSTAND THIS CAN CHANGE DNS AND TUNNEL ROUTES | Safety phrase |
|
||||
|
||||
## Outputs
|
||||
|
||||
- `outputs/config/config.yml` (generated cloudflared config)
|
||||
- `outputs/config/tunnel.json` (tunnel metadata snapshot)
|
||||
- `outputs/status_matrix.json`
|
||||
- `outputs/audit_report.md`
|
||||
|
||||
## Safety Guarantees
|
||||
|
||||
1. Default **DRY_RUN=1**
|
||||
2. Confirmation phrase required for apply and rollback
|
||||
3. Plan scripts print exact commands and expected changes
|
||||
4. Rollbacks available:
|
||||
- DNS route removal
|
||||
- systemd service stop/disable
|
||||
- tunnel delete (optional)
|
||||
|
||||
## Notes
|
||||
|
||||
- This skill uses `cloudflared` CLI.
|
||||
- You can run the tunnel without systemd (manual) if desired.
|
||||
|
||||
## EU Compliance
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Data Residency | EU (Ireland - Dublin) |
|
||||
| Jurisdiction | Irish Law |
|
||||
| Transport | Encrypted tunnel (Cloudflare) |
|
||||
| Logs | Local status + reports only |
|
||||
|
||||
## References
|
||||
- [Cloudflare Tunnel Notes](references/cloudflare_tunnel_notes.md)
|
||||
17
cloudflare-tunnel-manager/checks/check_service.sh
Normal file
17
cloudflare-tunnel-manager/checks/check_service.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
|
||||
main() {
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl is-active "$SERVICE_NAME" >/dev/null 2>&1 || die "Service not active: $SERVICE_NAME"
|
||||
log_info "Service active: $SERVICE_NAME"
|
||||
else
|
||||
die "systemctl not available"
|
||||
fi
|
||||
}
|
||||
main "$@"
|
||||
13
cloudflare-tunnel-manager/checks/check_tools.sh
Normal file
13
cloudflare-tunnel-manager/checks/check_tools.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
main() {
|
||||
need cloudflared
|
||||
need curl
|
||||
need jq
|
||||
log_info "Tools OK."
|
||||
}
|
||||
main "$@"
|
||||
61
cloudflare-tunnel-manager/config.json
Normal file
61
cloudflare-tunnel-manager/config.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "cloudflare-tunnel-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "Two-phase plan/apply/rollback management for Cloudflare Tunnel lifecycle.",
|
||||
"defaults": {
|
||||
"CONFIG_DIR": "outputs/config",
|
||||
"SERVICE_NAME": "cloudflared-tunnel",
|
||||
"DRY_RUN": "1",
|
||||
"REQUIRE_CONFIRM": "1",
|
||||
"CONFIRM_PHRASE": "I UNDERSTAND THIS CAN CHANGE DNS AND TUNNEL ROUTES"
|
||||
},
|
||||
"phases": {
|
||||
"preflight": [
|
||||
"00_preflight.sh"
|
||||
],
|
||||
"tunnel": {
|
||||
"plan": [
|
||||
"10_tunnel_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"11_tunnel_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_tunnel.sh"
|
||||
]
|
||||
},
|
||||
"dns": {
|
||||
"plan": [
|
||||
"20_dns_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"21_dns_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_dns.sh"
|
||||
]
|
||||
},
|
||||
"service": {
|
||||
"plan": [
|
||||
"30_service_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"31_service_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_service.sh"
|
||||
]
|
||||
},
|
||||
"verify": [
|
||||
"90_verify.sh"
|
||||
],
|
||||
"report": [
|
||||
"99_report.sh"
|
||||
]
|
||||
},
|
||||
"eu_compliance": {
|
||||
"data_residency": "EU",
|
||||
"jurisdiction": "Ireland",
|
||||
"gdpr_applicable": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Cloudflare Tunnel Notes
|
||||
|
||||
## API Token Permissions (recommended)
|
||||
- Account: Cloudflare Tunnel (read/edit)
|
||||
- Zone: DNS (read/edit)
|
||||
|
||||
## Credentials
|
||||
`cloudflared tunnel create` generates a credentials JSON file under:
|
||||
`~/.cloudflared/<tunnel-id>.json`
|
||||
|
||||
This skill's generated `config.yml` references that file directly.
|
||||
|
||||
## Ingress
|
||||
Default pattern:
|
||||
- Hostname -> LOCAL_SERVICE
|
||||
- Fallback -> 404
|
||||
|
||||
## Rollback Order
|
||||
1. Stop/disable service
|
||||
2. Remove DNS route
|
||||
3. Delete tunnel (optional)
|
||||
35
cloudflare-tunnel-manager/scripts/00_preflight.sh
Normal file
35
cloudflare-tunnel-manager/scripts/00_preflight.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${CF_ACCOUNT_ID:=}"
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${LOCAL_SERVICE:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
|
||||
main() {
|
||||
log_info "Starting 00_preflight.sh"
|
||||
cf_env_check
|
||||
[[ -n "$TUNNEL_NAME" ]] || die "TUNNEL_NAME is required."
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME is required."
|
||||
[[ -n "$HOSTNAME" ]] || die "HOSTNAME is required."
|
||||
[[ -n "$LOCAL_SERVICE" ]] || die "LOCAL_SERVICE is required."
|
||||
|
||||
need cloudflared
|
||||
need curl
|
||||
need jq
|
||||
need systemctl || log_warn "systemctl not found (service phase will not work)."
|
||||
|
||||
mkdir -p "$SKILL_ROOT/outputs"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
log_info "Preflight OK."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
22
cloudflare-tunnel-manager/scripts/10_tunnel_plan.sh
Normal file
22
cloudflare-tunnel-manager/scripts/10_tunnel_plan.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
[[ -n "$TUNNEL_NAME" ]] || die "TUNNEL_NAME is required."
|
||||
|
||||
echo "[PLAN] $(date -Iseconds) Tunnel plan"
|
||||
echo "[PLAN] Ensure a tunnel exists named: $TUNNEL_NAME"
|
||||
echo "[PLAN] If missing, create:"
|
||||
echo " cloudflared tunnel create \"$TUNNEL_NAME\""
|
||||
echo "[PLAN] Capture tunnel id + credentials to:"
|
||||
echo " $CONFIG_DIR/tunnel.json and $CONFIG_DIR/<id>.json"
|
||||
echo "[PLAN] Next: ./scripts/11_tunnel_apply.sh (requires DRY_RUN=0)"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
55
cloudflare-tunnel-manager/scripts/11_tunnel_apply.sh
Normal file
55
cloudflare-tunnel-manager/scripts/11_tunnel_apply.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${CF_ACCOUNT_ID:=}"
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
cf_env_check
|
||||
[[ -n "$TUNNEL_NAME" ]] || die "TUNNEL_NAME is required."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# List tunnels and locate by name
|
||||
log_info "Looking for existing tunnel: $TUNNEL_NAME"
|
||||
local list_json
|
||||
list_json="$(cloudflared tunnel --origincert /dev/null list --output json 2>/dev/null || true)"
|
||||
|
||||
# If list command fails (often needs login), fall back to API call
|
||||
local tunnel_id=""
|
||||
if [[ -n "$list_json" ]]; then
|
||||
tunnel_id="$(echo "$list_json" | jq -r --arg n "$TUNNEL_NAME" '.[] | select(.name==$n) | .id' | head -n 1 || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$tunnel_id" || "$tunnel_id" == "null" ]]; then
|
||||
log_warn "Tunnel not found via CLI list (or CLI not logged in). Creating tunnel via cloudflared..."
|
||||
# cloudflared tunnel create requires local credentials; this will prompt if needed.
|
||||
cloudflared tunnel create "$TUNNEL_NAME"
|
||||
# After creation, attempt list again
|
||||
list_json="$(cloudflared tunnel list --output json 2>/dev/null || true)"
|
||||
tunnel_id="$(echo "$list_json" | jq -r --arg n "$TUNNEL_NAME" '.[] | select(.name==$n) | .id' | head -n 1 || true)"
|
||||
fi
|
||||
|
||||
[[ -n "$tunnel_id" && "$tunnel_id" != "null" ]] || die "Unable to determine tunnel id for $TUNNEL_NAME. Ensure cloudflared is authenticated."
|
||||
|
||||
log_info "Tunnel id: $tunnel_id"
|
||||
|
||||
# Snapshot tunnel info
|
||||
cat > "$CONFIG_DIR/tunnel.json" <<EOF
|
||||
{
|
||||
"name": "$(json_escape "$TUNNEL_NAME")",
|
||||
"id": "$(json_escape "$tunnel_id")",
|
||||
"generated": "$(date -Iseconds)"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote tunnel snapshot: $CONFIG_DIR/tunnel.json"
|
||||
log_info "Next: ./scripts/20_dns_plan.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
23
cloudflare-tunnel-manager/scripts/20_dns_plan.sh
Normal file
23
cloudflare-tunnel-manager/scripts/20_dns_plan.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME is required."
|
||||
[[ -n "$HOSTNAME" ]] || die "HOSTNAME is required."
|
||||
[[ -f "$CONFIG_DIR/tunnel.json" ]] || log_warn "Missing tunnel snapshot (run 11_tunnel_apply.sh first)."
|
||||
|
||||
echo "[PLAN] $(date -Iseconds) DNS route plan"
|
||||
echo "[PLAN] Ensure CNAME exists for hostname:"
|
||||
echo " $HOSTNAME -> <tunnel-id>.cfargotunnel.com"
|
||||
echo "[PLAN] Cloudflare API will be used to find zone id for: $ZONE_NAME"
|
||||
echo "[PLAN] Next: ./scripts/21_dns_apply.sh (requires DRY_RUN=0)"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
68
cloudflare-tunnel-manager/scripts/21_dns_apply.sh
Normal file
68
cloudflare-tunnel-manager/scripts/21_dns_apply.sh
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
api() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
curl -sS -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
[[ -n "$CF_API_TOKEN" ]] || die "CF_API_TOKEN is required."
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME is required."
|
||||
[[ -n "$HOSTNAME" ]] || die "HOSTNAME is required."
|
||||
[[ -f "$CONFIG_DIR/tunnel.json" ]] || die "Missing tunnel snapshot: $CONFIG_DIR/tunnel.json"
|
||||
|
||||
local tunnel_id; tunnel_id="$(jq -r '.id' "$CONFIG_DIR/tunnel.json")"
|
||||
[[ -n "$tunnel_id" && "$tunnel_id" != "null" ]] || die "Invalid tunnel id in tunnel.json"
|
||||
|
||||
log_info "Resolving zone id for: $ZONE_NAME"
|
||||
local z; z="$(api GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE_NAME" | jq -r '.result[0].id' )"
|
||||
[[ -n "$z" && "$z" != "null" ]] || die "Unable to resolve zone id for $ZONE_NAME"
|
||||
|
||||
local cname_target="${tunnel_id}.cfargotunnel.com"
|
||||
log_info "Ensuring CNAME: $HOSTNAME -> $cname_target"
|
||||
|
||||
# Find existing record
|
||||
local rec; rec="$(api GET "https://api.cloudflare.com/client/v4/zones/$z/dns_records?type=CNAME&name=$HOSTNAME")"
|
||||
local rec_id; rec_id="$(echo "$rec" | jq -r '.result[0].id' )"
|
||||
|
||||
if [[ -n "$rec_id" && "$rec_id" != "null" ]]; then
|
||||
log_info "Updating existing DNS record id: $rec_id"
|
||||
api PUT "https://api.cloudflare.com/client/v4/zones/$z/dns_records/$rec_id" \
|
||||
--data "{\"type\":\"CNAME\",\"name\":\"$HOSTNAME\",\"content\":\"$cname_target\",\"ttl\":1,\"proxied\":true}" \
|
||||
| jq -e '.success==true' >/dev/null || die "Failed to update DNS record."
|
||||
else
|
||||
log_info "Creating new DNS record"
|
||||
api POST "https://api.cloudflare.com/client/v4/zones/$z/dns_records" \
|
||||
--data "{\"type\":\"CNAME\",\"name\":\"$HOSTNAME\",\"content\":\"$cname_target\",\"ttl\":1,\"proxied\":true}" \
|
||||
| jq -e '.success==true' >/dev/null || die "Failed to create DNS record."
|
||||
fi
|
||||
|
||||
# Save snapshot
|
||||
cat > "$CONFIG_DIR/dns_route.json" <<EOF
|
||||
{
|
||||
"zone_name": "$(json_escape "$ZONE_NAME")",
|
||||
"hostname": "$(json_escape "$HOSTNAME")",
|
||||
"target": "$(json_escape "$cname_target")",
|
||||
"generated": "$(date -Iseconds)"
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote DNS snapshot: $CONFIG_DIR/dns_route.json"
|
||||
log_info "Next: ./scripts/30_service_plan.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
23
cloudflare-tunnel-manager/scripts/30_service_plan.sh
Normal file
23
cloudflare-tunnel-manager/scripts/30_service_plan.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${LOCAL_SERVICE:=}"
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
[[ -n "$LOCAL_SERVICE" ]] || die "LOCAL_SERVICE is required."
|
||||
[[ -f "$CONFIG_DIR/tunnel.json" ]] || log_warn "Missing tunnel snapshot (run 11_tunnel_apply.sh first)."
|
||||
|
||||
echo "[PLAN] $(date -Iseconds) Service plan"
|
||||
echo "[PLAN] Generate config.yml under: $CONFIG_DIR/config.yml"
|
||||
echo "[PLAN] Create systemd unit: /etc/systemd/system/$SERVICE_NAME.service"
|
||||
echo "[PLAN] Unit will run: cloudflared tunnel --config $CONFIG_DIR/config.yml run"
|
||||
echo "[PLAN] Ingress default: $LOCAL_SERVICE"
|
||||
echo "[PLAN] Next: ./scripts/31_service_apply.sh (requires DRY_RUN=0)"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
72
cloudflare-tunnel-manager/scripts/31_service_apply.sh
Normal file
72
cloudflare-tunnel-manager/scripts/31_service_apply.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${LOCAL_SERVICE:=}"
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
need systemctl
|
||||
|
||||
[[ -n "$TUNNEL_NAME" ]] || die "TUNNEL_NAME is required."
|
||||
[[ -n "$HOSTNAME" ]] || die "HOSTNAME is required."
|
||||
[[ -n "$LOCAL_SERVICE" ]] || die "LOCAL_SERVICE is required."
|
||||
[[ -f "$CONFIG_DIR/tunnel.json" ]] || die "Missing tunnel snapshot: $CONFIG_DIR/tunnel.json"
|
||||
|
||||
local tunnel_id; tunnel_id="$(jq -r '.id' "$CONFIG_DIR/tunnel.json")"
|
||||
[[ -n "$tunnel_id" && "$tunnel_id" != "null" ]] || die "Invalid tunnel id in tunnel.json"
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Generate cloudflared config
|
||||
cat > "$CONFIG_DIR/config.yml" <<EOF
|
||||
tunnel: $tunnel_id
|
||||
credentials-file: $HOME/.cloudflared/$tunnel_id.json
|
||||
|
||||
ingress:
|
||||
- hostname: $HOSTNAME
|
||||
service: $LOCAL_SERVICE
|
||||
- service: http_status:404
|
||||
EOF
|
||||
|
||||
log_info "Wrote config: $CONFIG_DIR/config.yml"
|
||||
log_warn "NOTE: credentials-file expects: $HOME/.cloudflared/$tunnel_id.json"
|
||||
log_warn "If you created the tunnel on a different machine, copy that credentials file."
|
||||
|
||||
# Create systemd unit
|
||||
local unit="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
sudo cp -a "$unit" "$unit.bak.$(date -Iseconds | tr ':' '-')" 2>/dev/null || true
|
||||
|
||||
sudo tee "$unit" >/dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Cloudflare Tunnel ($TUNNEL_NAME)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=HOME=$HOME
|
||||
ExecStart=$(command -v cloudflared) tunnel --config $CONFIG_DIR/config.yml run
|
||||
Restart=on-failure
|
||||
RestartSec=3s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
sudo systemctl restart "$SERVICE_NAME"
|
||||
sudo systemctl --no-pager status "$SERVICE_NAME" | head -n 30 || true
|
||||
|
||||
log_info "Service applied: $SERVICE_NAME"
|
||||
log_info "Next: ./scripts/90_verify.sh"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
54
cloudflare-tunnel-manager/scripts/90_verify.sh
Normal file
54
cloudflare-tunnel-manager/scripts/90_verify.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
local status="$SKILL_ROOT/outputs/status_matrix.json"
|
||||
local ok_tunnel=false ok_dns=false ok_config=false ok_service=false
|
||||
|
||||
if [[ -f "$CONFIG_DIR/tunnel.json" ]]; then ok_tunnel=true; fi
|
||||
if [[ -f "$CONFIG_DIR/dns_route.json" ]]; then ok_dns=true; fi
|
||||
if [[ -f "$CONFIG_DIR/config.yml" ]]; then ok_config=true; fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl is-active "$SERVICE_NAME" >/dev/null 2>&1; then ok_service=true; fi
|
||||
fi
|
||||
|
||||
blockers="[]"
|
||||
if [[ "$ok_tunnel" != "true" ]]; then blockers='["tunnel_not_created"]'
|
||||
elif [[ "$ok_dns" != "true" ]]; then blockers='["dns_route_missing"]'
|
||||
elif [[ "$ok_config" != "true" ]]; then blockers='["config_missing"]'
|
||||
fi
|
||||
|
||||
cat > "$status" <<EOF
|
||||
{
|
||||
"skill": "cloudflare-tunnel-manager",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"checks": [
|
||||
{"name":"tunnel_snapshot", "ok": $ok_tunnel},
|
||||
{"name":"dns_snapshot", "ok": $ok_dns},
|
||||
{"name":"config_present", "ok": $ok_config},
|
||||
{"name":"service_active", "ok": $ok_service}
|
||||
],
|
||||
"blockers": $blockers,
|
||||
"warnings": [],
|
||||
"next_steps": [
|
||||
"Confirm hostname routes to expected service",
|
||||
"Record tunnel id + hostname in LAWCHAIN (optional)",
|
||||
"Proceed to gitea-bootstrap or proof pipeline skills"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote $status"
|
||||
cat "$status"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
82
cloudflare-tunnel-manager/scripts/99_report.sh
Normal file
82
cloudflare-tunnel-manager/scripts/99_report.sh
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${LOCAL_SERVICE:=}"
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
mkdir -p "$SKILL_ROOT/outputs"
|
||||
local report="$SKILL_ROOT/outputs/audit_report.md"
|
||||
local status="$SKILL_ROOT/outputs/status_matrix.json"
|
||||
|
||||
local tunnel_id="(unknown)"
|
||||
[[ -f "$CONFIG_DIR/tunnel.json" ]] && tunnel_id="$(jq -r '.id' "$CONFIG_DIR/tunnel.json")"
|
||||
|
||||
cat > "$report" <<EOF
|
||||
# Cloudflare Tunnel Audit Report
|
||||
|
||||
**Generated:** $(date -Iseconds)
|
||||
**Tunnel Name:** $(json_escape "${TUNNEL_NAME:-}")
|
||||
**Tunnel ID:** $(json_escape "$tunnel_id")
|
||||
**Hostname:** $(json_escape "${HOSTNAME:-}")
|
||||
**Zone:** $(json_escape "${ZONE_NAME:-}")
|
||||
**Local Service:** $(json_escape "${LOCAL_SERVICE:-}")
|
||||
**Service Unit:** $(json_escape "$SERVICE_NAME")
|
||||
**Skill Version:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Artifacts
|
||||
|
||||
| Item | Path |
|
||||
|---|---|
|
||||
| Tunnel Snapshot | \`$CONFIG_DIR/tunnel.json\` |
|
||||
| DNS Snapshot | \`$CONFIG_DIR/dns_route.json\` |
|
||||
| cloudflared Config | \`$CONFIG_DIR/config.yml\` |
|
||||
| Status Matrix | \`$SKILL_ROOT/outputs/status_matrix.json\` |
|
||||
|
||||
---
|
||||
|
||||
## Status Matrix
|
||||
|
||||
$(if [[ -f "$status" ]]; then
|
||||
echo '```json'
|
||||
cat "$status"
|
||||
echo '```'
|
||||
else
|
||||
echo "_Missing status_matrix.json — run 90_verify.sh first._"
|
||||
fi)
|
||||
|
||||
---
|
||||
|
||||
## EU Compliance Declaration
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Data Residency | EU (Ireland - Dublin) |
|
||||
| Jurisdiction | Irish Law |
|
||||
| DNS Provider | Cloudflare |
|
||||
| Tunnel | Encrypted transport |
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
- Undo service: \`./scripts/rollback/undo_service.sh\`
|
||||
- Undo DNS: \`./scripts/rollback/undo_dns.sh\`
|
||||
- Undo tunnel (delete): \`./scripts/rollback/undo_tunnel.sh\`
|
||||
|
||||
EOF
|
||||
|
||||
log_info "Wrote $report"
|
||||
cat "$report"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
41
cloudflare-tunnel-manager/scripts/_common.sh
Normal file
41
cloudflare-tunnel-manager/scripts/_common.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log_info(){ echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_warn(){ echo "[WARN] $(date -Iseconds) $*" >&2; }
|
||||
log_error(){ echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
die(){ log_error "$*"; exit 1; }
|
||||
|
||||
need(){ command -v "$1" >/dev/null 2>&1 || die "Missing required tool: $1"; }
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf "%s" "$s"
|
||||
}
|
||||
|
||||
confirm_gate() {
|
||||
: "${DRY_RUN:=1}"
|
||||
: "${REQUIRE_CONFIRM:=1}"
|
||||
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS CAN CHANGE DNS AND TUNNEL ROUTES}"
|
||||
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)."
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo "Type to confirm:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
read -r input
|
||||
[[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch."
|
||||
fi
|
||||
}
|
||||
|
||||
# Minimal wrapper: prefer explicit token env var over stored login
|
||||
cf_env_check() {
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${CF_ACCOUNT_ID:=}"
|
||||
[[ -n "$CF_API_TOKEN" ]] || die "CF_API_TOKEN is required."
|
||||
[[ -n "$CF_ACCOUNT_ID" ]] || die "CF_ACCOUNT_ID is required."
|
||||
}
|
||||
44
cloudflare-tunnel-manager/scripts/rollback/undo_dns.sh
Normal file
44
cloudflare-tunnel-manager/scripts/rollback/undo_dns.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${HOSTNAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
api() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
curl -sS -X "$method" "$url" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
[[ -n "$CF_API_TOKEN" ]] || die "CF_API_TOKEN is required."
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME is required."
|
||||
[[ -n "$HOSTNAME" ]] || die "HOSTNAME is required."
|
||||
need jq
|
||||
need curl
|
||||
|
||||
local z; z="$(api GET "https://api.cloudflare.com/client/v4/zones?name=$ZONE_NAME" | jq -r '.result[0].id')"
|
||||
[[ -n "$z" && "$z" != "null" ]] || die "Unable to resolve zone id for $ZONE_NAME"
|
||||
|
||||
local rec; rec="$(api GET "https://api.cloudflare.com/client/v4/zones/$z/dns_records?type=CNAME&name=$HOSTNAME")"
|
||||
local rec_id; rec_id="$(echo "$rec" | jq -r '.result[0].id')"
|
||||
if [[ -n "$rec_id" && "$rec_id" != "null" ]]; then
|
||||
log_warn "Deleting DNS record id: $rec_id ($HOSTNAME)"
|
||||
api DELETE "https://api.cloudflare.com/client/v4/zones/$z/dns_records/$rec_id" | jq -e '.success==true' >/dev/null || die "Failed to delete DNS record."
|
||||
else
|
||||
log_warn "No DNS record found for $HOSTNAME"
|
||||
fi
|
||||
|
||||
rm -f "$CONFIG_DIR/dns_route.json" || true
|
||||
log_info "DNS rollback complete."
|
||||
}
|
||||
main "$@"
|
||||
18
cloudflare-tunnel-manager/scripts/rollback/undo_service.sh
Normal file
18
cloudflare-tunnel-manager/scripts/rollback/undo_service.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
: "${SERVICE_NAME:=cloudflared-tunnel}"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
need systemctl
|
||||
log_warn "Stopping/disabling service: $SERVICE_NAME"
|
||||
sudo systemctl stop "$SERVICE_NAME" || true
|
||||
sudo systemctl disable "$SERVICE_NAME" || true
|
||||
sudo systemctl daemon-reload || true
|
||||
log_info "Service rollback complete."
|
||||
}
|
||||
main "$@"
|
||||
28
cloudflare-tunnel-manager/scripts/rollback/undo_tunnel.sh
Normal file
28
cloudflare-tunnel-manager/scripts/rollback/undo_tunnel.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
: "${TUNNEL_NAME:=}"
|
||||
: "${CONFIG_DIR:=$SKILL_ROOT/outputs/config}"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
[[ -n "$TUNNEL_NAME" ]] || die "TUNNEL_NAME is required."
|
||||
|
||||
if [[ -f "$CONFIG_DIR/tunnel.json" ]]; then
|
||||
local tunnel_id; tunnel_id="$(jq -r '.id' "$CONFIG_DIR/tunnel.json")"
|
||||
if [[ -n "$tunnel_id" && "$tunnel_id" != "null" ]]; then
|
||||
log_warn "Deleting tunnel via cloudflared: $TUNNEL_NAME ($tunnel_id)"
|
||||
cloudflared tunnel delete -f "$tunnel_id" || cloudflared tunnel delete -f "$TUNNEL_NAME" || true
|
||||
fi
|
||||
else
|
||||
log_warn "No tunnel.json snapshot; attempting delete by name: $TUNNEL_NAME"
|
||||
cloudflared tunnel delete -f "$TUNNEL_NAME" || true
|
||||
fi
|
||||
|
||||
rm -f "$CONFIG_DIR/tunnel.json" "$CONFIG_DIR/config.yml" || true
|
||||
log_info "Tunnel rollback complete."
|
||||
}
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user