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:
123
dns-sovereign/SKILL.md
Normal file
123
dns-sovereign/SKILL.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: dns-sovereign
|
||||
description: >
|
||||
PowerDNS + Cloudflare hybrid DNS with plan/apply/rollback, audit trail,
|
||||
and verification. Deploys a sovereign PowerDNS authoritative server
|
||||
(Docker) and optionally syncs selected records to Cloudflare.
|
||||
Triggers: 'dns sovereign', 'powerdns', 'authoritative dns', 'dns plan',
|
||||
'dns rollback', 'sync dns to cloudflare'.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# DNS Sovereign (PowerDNS + Cloudflare Hybrid)
|
||||
|
||||
This skill establishes **Node B** (or dedicated DNS node) as your sovereign
|
||||
authoritative DNS, with Cloudflare as an optional edge mirror / public resolver layer.
|
||||
|
||||
## What v1.0.0 Does
|
||||
|
||||
**PowerDNS Authoritative (Docker)**
|
||||
- Deploys PowerDNS authoritative server using sqlite backend
|
||||
- Enables the PowerDNS API
|
||||
- Creates a first zone (optional) via API
|
||||
- Produces an audit report + status matrix
|
||||
|
||||
**Optional Cloudflare Sync**
|
||||
- Push a limited set of records (A/AAAA/CNAME/TXT) to Cloudflare using API token
|
||||
- Designed as a *mirror*, not source of truth
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd ~/.claude/skills/dns-sovereign
|
||||
|
||||
# PowerDNS (required)
|
||||
export MODE="docker"
|
||||
export PDNS_PORT=53
|
||||
export PDNS_WEB_PORT=8081
|
||||
export PDNS_API_KEY="..." # choose a strong random token
|
||||
export PDNS_DATA_DIR="$HOME/pdns"
|
||||
|
||||
# Zone (optional but recommended)
|
||||
export ZONE_NAME="example.com" # authoritative zone name (must end with . in PDNS API ops)
|
||||
export NS1_NAME="ns1.example.com"
|
||||
export NS2_NAME="ns2.example.com"
|
||||
|
||||
# Cloudflare mirror (optional)
|
||||
export CF_API_TOKEN="" # if set, sync scripts can run
|
||||
export CF_ZONE_NAME="example.com" # Cloudflare zone to mirror into
|
||||
|
||||
# Safety
|
||||
export DRY_RUN=1
|
||||
export REQUIRE_CONFIRM=1
|
||||
export CONFIRM_PHRASE="I UNDERSTAND THIS CAN CHANGE DNS"
|
||||
|
||||
./scripts/00_preflight.sh
|
||||
./scripts/10_pdns_plan.sh
|
||||
|
||||
export DRY_RUN=0
|
||||
./scripts/11_pdns_apply.sh
|
||||
|
||||
# Optional: create zone + NS records in PDNS
|
||||
./scripts/20_zone_plan.sh
|
||||
export DRY_RUN=0
|
||||
./scripts/21_zone_apply.sh
|
||||
|
||||
# Optional: mirror records to Cloudflare (does not pull)
|
||||
./scripts/30_cf_plan.sh
|
||||
export DRY_RUN=0
|
||||
./scripts/31_cf_apply.sh
|
||||
|
||||
./scripts/90_verify.sh
|
||||
./scripts/99_report.sh
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---|---:|---|---|
|
||||
| MODE | Yes | docker | docker |
|
||||
| PDNS_API_KEY | Yes | (none) | PowerDNS API key |
|
||||
| PDNS_DATA_DIR | No | ~/pdns | Persistent storage |
|
||||
| PDNS_PORT | No | 53 | DNS port |
|
||||
| PDNS_WEB_PORT | No | 8081 | API/Web port |
|
||||
| ZONE_NAME | No | (empty) | Zone to create (e.g., example.com) |
|
||||
| NS1_NAME | No | ns1.<zone> | Primary NS hostname |
|
||||
| NS2_NAME | No | ns2.<zone> | Secondary NS hostname |
|
||||
| CF_API_TOKEN | No | (empty) | Cloudflare API token (for mirroring) |
|
||||
| CF_ZONE_NAME | No | (empty) | Cloudflare zone name |
|
||||
| DRY_RUN | No | 1 | Apply refuses unless DRY_RUN=0 |
|
||||
| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase |
|
||||
| CONFIRM_PHRASE | No | I UNDERSTAND THIS CAN CHANGE DNS | Safety phrase |
|
||||
|
||||
## Outputs
|
||||
|
||||
- `outputs/compose.yml`
|
||||
- `outputs/pdns.conf`
|
||||
- `outputs/pdns_api_probe.json`
|
||||
- `outputs/status_matrix.json`
|
||||
- `outputs/audit_report.md`
|
||||
- `outputs/backups/<timestamp>/...`
|
||||
|
||||
## Safety Guarantees
|
||||
|
||||
1. Default **DRY_RUN=1**
|
||||
2. Confirmation phrase required
|
||||
3. Backups for compose + config
|
||||
4. Rollback scripts:
|
||||
- stop/remove PDNS container (data preserved)
|
||||
- delete zone (optional)
|
||||
- remove mirrored Cloudflare records created by this skill (best-effort)
|
||||
|
||||
## EU Compliance
|
||||
|
||||
| Aspect | Value |
|
||||
|---|---|
|
||||
| Data Residency | EU (Ireland - Dublin) |
|
||||
| Jurisdiction | Irish Law |
|
||||
| Authoritative Source | PowerDNS on your node |
|
||||
| Mirror | Optional Cloudflare mirror |
|
||||
|
||||
## References
|
||||
- [PowerDNS Notes](references/powerdns_notes.md)
|
||||
- [Cloudflare DNS Mirror Notes](references/cloudflare_dns_mirror_notes.md)
|
||||
63
dns-sovereign/config.json
Normal file
63
dns-sovereign/config.json
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "dns-sovereign",
|
||||
"version": "1.0.0",
|
||||
"description": "PowerDNS authoritative + optional Cloudflare mirror, with plan/apply/rollback.",
|
||||
"defaults": {
|
||||
"MODE": "docker",
|
||||
"PDNS_PORT": "53",
|
||||
"PDNS_WEB_PORT": "8081",
|
||||
"PDNS_DATA_DIR": "~/pdns",
|
||||
"DRY_RUN": "1",
|
||||
"REQUIRE_CONFIRM": "1",
|
||||
"CONFIRM_PHRASE": "I UNDERSTAND THIS CAN CHANGE DNS"
|
||||
},
|
||||
"phases": {
|
||||
"preflight": [
|
||||
"00_preflight.sh"
|
||||
],
|
||||
"pdns": {
|
||||
"plan": [
|
||||
"10_pdns_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"11_pdns_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_pdns.sh"
|
||||
]
|
||||
},
|
||||
"zone": {
|
||||
"plan": [
|
||||
"20_zone_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"21_zone_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_zone.sh"
|
||||
]
|
||||
},
|
||||
"cloudflare": {
|
||||
"plan": [
|
||||
"30_cf_plan.sh"
|
||||
],
|
||||
"apply": [
|
||||
"31_cf_apply.sh"
|
||||
],
|
||||
"rollback": [
|
||||
"rollback/undo_cloudflare.sh"
|
||||
]
|
||||
},
|
||||
"verify": [
|
||||
"90_verify.sh"
|
||||
],
|
||||
"report": [
|
||||
"99_report.sh"
|
||||
]
|
||||
},
|
||||
"eu_compliance": {
|
||||
"data_residency": "EU",
|
||||
"jurisdiction": "Ireland",
|
||||
"gdpr_applicable": true
|
||||
}
|
||||
}
|
||||
16
dns-sovereign/references/cloudflare_dns_mirror_notes.md
Normal file
16
dns-sovereign/references/cloudflare_dns_mirror_notes.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Cloudflare DNS Mirror Notes
|
||||
|
||||
This skill treats Cloudflare as a **mirror**, not the source of truth.
|
||||
|
||||
## Mirror Records File
|
||||
Create: outputs/mirror_records.json
|
||||
|
||||
Example:
|
||||
[
|
||||
{"type":"A","name":"app","content":"1.2.3.4","ttl":120},
|
||||
{"type":"CNAME","name":"git","content":"app.example.com","ttl":120}
|
||||
]
|
||||
|
||||
## Rollback
|
||||
When mirroring, record IDs are saved in outputs/cloudflare_record_ids.txt.
|
||||
undo_cloudflare.sh will delete those IDs (best effort).
|
||||
12
dns-sovereign/references/powerdns_notes.md
Normal file
12
dns-sovereign/references/powerdns_notes.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# PowerDNS Notes
|
||||
|
||||
## v1 Design
|
||||
- Authoritative server: powerdns/pdns-auth (Docker)
|
||||
- Backend: sqlite3 in PDNS_DATA_DIR
|
||||
- API enabled and published to localhost only
|
||||
|
||||
## Production Hardening
|
||||
- Run behind firewall; restrict UDP/TCP 53 to known resolvers or public as needed
|
||||
- Keep API bound to localhost
|
||||
- Consider a second NS (ns2) on a separate node/provider for resilience
|
||||
- Back up PDNS_DATA_DIR using backup-sovereign
|
||||
29
dns-sovereign/scripts/00_preflight.sh
Normal file
29
dns-sovereign/scripts/00_preflight.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${MODE:=docker}"
|
||||
: "${PDNS_API_KEY:=}"
|
||||
: "${PDNS_DATA_DIR:=$HOME/pdns}"
|
||||
: "${PDNS_PORT:=53}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
|
||||
main() {
|
||||
[[ -n "$PDNS_API_KEY" ]] || die "PDNS_API_KEY is required."
|
||||
need curl
|
||||
need jq
|
||||
|
||||
if [[ "$MODE" == "docker" ]]; then
|
||||
need docker
|
||||
else
|
||||
die "MODE must be docker in v1.0.0"
|
||||
fi
|
||||
|
||||
mkdir -p "$SKILL_ROOT/outputs" "$SKILL_ROOT/outputs/backups"
|
||||
mkdir -p "$PDNS_DATA_DIR"
|
||||
|
||||
log_info "Preflight OK."
|
||||
}
|
||||
main "$@"
|
||||
21
dns-sovereign/scripts/10_pdns_plan.sh
Normal file
21
dns-sovereign/scripts/10_pdns_plan.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${PDNS_DATA_DIR:=$HOME/pdns}"
|
||||
: "${PDNS_PORT:=53}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
|
||||
main() {
|
||||
echo "[PLAN] $(date -Iseconds) PowerDNS Authoritative (Docker)"
|
||||
echo "[PLAN] Data dir: $PDNS_DATA_DIR"
|
||||
echo "[PLAN] DNS port: $PDNS_PORT/udp + $PDNS_PORT/tcp"
|
||||
echo "[PLAN] API/Web: 127.0.0.1:$PDNS_WEB_PORT (recommended to keep private)"
|
||||
echo "[PLAN] Outputs:"
|
||||
echo " outputs/compose.yml"
|
||||
echo " outputs/pdns.conf"
|
||||
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/11_pdns_apply.sh"
|
||||
}
|
||||
main "$@"
|
||||
71
dns-sovereign/scripts/11_pdns_apply.sh
Normal file
71
dns-sovereign/scripts/11_pdns_apply.sh
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${PDNS_API_KEY:=}"
|
||||
: "${PDNS_DATA_DIR:=$HOME/pdns}"
|
||||
: "${PDNS_PORT:=53}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
need docker
|
||||
[[ -n "$PDNS_API_KEY" ]] || die "PDNS_API_KEY is required."
|
||||
|
||||
local ts; ts="$(date -Iseconds | tr ':' '-')"
|
||||
local backup_dir="$SKILL_ROOT/outputs/backups/$ts"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
# pdns.conf (mounted into container)
|
||||
cat > "$SKILL_ROOT/outputs/pdns.conf" <<EOF
|
||||
launch=gsqlite3
|
||||
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
|
||||
|
||||
api=yes
|
||||
api-key=$PDNS_API_KEY
|
||||
webserver=yes
|
||||
webserver-address=0.0.0.0
|
||||
webserver-port=8081
|
||||
|
||||
# security posture
|
||||
disable-syslog=yes
|
||||
loglevel=4
|
||||
|
||||
# allow API only from container network; bind published port to localhost in compose
|
||||
webserver-allow-from=127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
EOF
|
||||
|
||||
# compose
|
||||
cat > "$SKILL_ROOT/outputs/compose.yml" <<EOF
|
||||
version: "3.8"
|
||||
services:
|
||||
pdns:
|
||||
image: powerdns/pdns-auth-49:latest
|
||||
container_name: pdns-auth
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PDNS_PORT}:53/udp"
|
||||
- "${PDNS_PORT}:53/tcp"
|
||||
- "127.0.0.1:${PDNS_WEB_PORT}:8081/tcp"
|
||||
volumes:
|
||||
- ${PDNS_DATA_DIR}:/var/lib/powerdns
|
||||
- ${SKILL_ROOT}/outputs/pdns.conf:/etc/powerdns/pdns.conf:ro
|
||||
EOF
|
||||
|
||||
cp -a "$SKILL_ROOT/outputs/pdns.conf" "$backup_dir/pdns.conf"
|
||||
cp -a "$SKILL_ROOT/outputs/compose.yml" "$backup_dir/compose.yml"
|
||||
|
||||
log_info "Starting PowerDNS..."
|
||||
cd "$SKILL_ROOT/outputs"
|
||||
$(compose_cmd) -f compose.yml up -d
|
||||
|
||||
# Probe API
|
||||
log_info "Probing PDNS API..."
|
||||
local api="http://127.0.0.1:${PDNS_WEB_PORT}/api/v1/servers/localhost"
|
||||
curl -fsS -H "X-API-Key: $PDNS_API_KEY" "$api" | jq '.' > "$SKILL_ROOT/outputs/pdns_api_probe.json"
|
||||
log_info "PDNS API probe saved: outputs/pdns_api_probe.json"
|
||||
log_info "PDNS apply complete."
|
||||
}
|
||||
main "$@"
|
||||
23
dns-sovereign/scripts/20_zone_plan.sh
Normal file
23
dns-sovereign/scripts/20_zone_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:=}"
|
||||
: "${NS1_NAME:=}"
|
||||
: "${NS2_NAME:=}"
|
||||
|
||||
main() {
|
||||
if [[ -z "$ZONE_NAME" ]]; then
|
||||
log_warn "ZONE_NAME not set; zone creation will be skipped."
|
||||
exit 0
|
||||
fi
|
||||
echo "[PLAN] $(date -Iseconds) Create zone in PowerDNS"
|
||||
echo "[PLAN] Zone: $ZONE_NAME"
|
||||
echo "[PLAN] NS1: ${NS1_NAME:-ns1.$ZONE_NAME}"
|
||||
echo "[PLAN] NS2: ${NS2_NAME:-ns2.$ZONE_NAME}"
|
||||
echo "[PLAN] Note: PowerDNS API expects trailing dot for zone operations."
|
||||
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/21_zone_apply.sh"
|
||||
}
|
||||
main "$@"
|
||||
45
dns-sovereign/scripts/21_zone_apply.sh
Normal file
45
dns-sovereign/scripts/21_zone_apply.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${PDNS_API_KEY:=}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${NS1_NAME:=}"
|
||||
: "${NS2_NAME:=}"
|
||||
|
||||
api() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
curl -sS -X "$method" "$url" -H "X-API-Key: $PDNS_API_KEY" -H "Content-Type: application/json" "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
[[ -n "$PDNS_API_KEY" ]] || die "PDNS_API_KEY is required."
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME is required."
|
||||
|
||||
local zone="${ZONE_NAME%\.}."
|
||||
local ns1="${NS1_NAME:-ns1.${ZONE_NAME}}"
|
||||
local ns2="${NS2_NAME:-ns2.${ZONE_NAME}}"
|
||||
|
||||
local base="http://127.0.0.1:${PDNS_WEB_PORT}/api/v1/servers/localhost"
|
||||
# Check if zone exists
|
||||
if api GET "$base/zones/$zone" | jq -e '.name' >/dev/null 2>&1; then
|
||||
log_warn "Zone already exists: $zone"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_info "Creating zone: $zone"
|
||||
api POST "$base/zones" --data "{
|
||||
\"name\": \"$zone\",
|
||||
\"kind\": \"Native\",
|
||||
\"masters\": [],
|
||||
\"nameservers\": [\"$ns1.\", \"$ns2.\"]
|
||||
}" | jq '.' > "$SKILL_ROOT/outputs/zone_create_result.json"
|
||||
|
||||
log_info "Zone created; output saved: outputs/zone_create_result.json"
|
||||
}
|
||||
main "$@"
|
||||
23
dns-sovereign/scripts/30_cf_plan.sh
Normal file
23
dns-sovereign/scripts/30_cf_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"
|
||||
|
||||
: "${CF_API_TOKEN:=}"
|
||||
: "${CF_ZONE_NAME:=}"
|
||||
: "${ZONE_NAME:=}"
|
||||
|
||||
main() {
|
||||
if [[ -z "$CF_API_TOKEN" || -z "$CF_ZONE_NAME" ]]; then
|
||||
log_warn "Cloudflare mirror not configured (CF_API_TOKEN/CF_ZONE_NAME). Skipping CF plan."
|
||||
exit 0
|
||||
fi
|
||||
echo "[PLAN] $(date -Iseconds) Cloudflare DNS mirror"
|
||||
echo "[PLAN] Mirror target zone in Cloudflare: $CF_ZONE_NAME"
|
||||
echo "[PLAN] Source zone (PowerDNS): ${ZONE_NAME:-<unset>}"
|
||||
echo "[PLAN] v1 mirrors only records listed in outputs/mirror_records.json if present."
|
||||
echo "[PLAN] Create that file to define records (A/AAAA/CNAME/TXT)."
|
||||
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/31_cf_apply.sh"
|
||||
}
|
||||
main "$@"
|
||||
73
dns-sovereign/scripts/31_cf_apply.sh
Normal file
73
dns-sovereign/scripts/31_cf_apply.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/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_ZONE_NAME:=}"
|
||||
|
||||
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 "$CF_ZONE_NAME" ]] || die "CF_ZONE_NAME is required."
|
||||
need jq
|
||||
need curl
|
||||
|
||||
local mirror_file="$SKILL_ROOT/outputs/mirror_records.json"
|
||||
if [[ ! -f "$mirror_file" ]]; then
|
||||
die "Missing $mirror_file. Create it like: [{\"type\":\"A\",\"name\":\"app\",\"content\":\"1.2.3.4\",\"ttl\":120}]"
|
||||
fi
|
||||
|
||||
log_info "Resolving Cloudflare zone id for: $CF_ZONE_NAME"
|
||||
local zid; zid="$(api GET "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE_NAME" | jq -r '.result[0].id')"
|
||||
[[ -n "$zid" && "$zid" != "null" ]] || die "Unable to resolve zone id."
|
||||
|
||||
# For each record, create/update in CF
|
||||
created_ids=[]
|
||||
results=[]
|
||||
while IFS= read -r rec; do
|
||||
rtype="$(echo "$rec" | jq -r '.type')"
|
||||
rname="$(echo "$rec" | jq -r '.name')"
|
||||
rcontent="$(echo "$rec" | jq -r '.content')"
|
||||
rttl="$(echo "$rec" | jq -r '.ttl // 120')"
|
||||
|
||||
# Convert short name to FQDN if needed
|
||||
if [[ "$rname" != *"."* ]]; then
|
||||
fqdn="${rname}.${CF_ZONE_NAME}"
|
||||
else
|
||||
fqdn="$rname"
|
||||
fi
|
||||
|
||||
# check existing
|
||||
existing="$(api GET "https://api.cloudflare.com/client/v4/zones/$zid/dns_records?type=$rtype&name=$fqdn")"
|
||||
rid="$(echo "$existing" | jq -r '.result[0].id')"
|
||||
|
||||
if [[ -n "$rid" && "$rid" != "null" ]]; then
|
||||
log_info "Updating $rtype $fqdn"
|
||||
api PUT "https://api.cloudflare.com/client/v4/zones/$zid/dns_records/$rid" \
|
||||
--data "{\"type\":\"$rtype\",\"name\":\"$fqdn\",\"content\":\"$rcontent\",\"ttl\":$rttl,\"proxied\":true}" \
|
||||
| jq -e '.success==true' >/dev/null || die "Failed update for $fqdn"
|
||||
echo "$rid" >> "$SKILL_ROOT/outputs/cloudflare_record_ids.txt"
|
||||
else
|
||||
log_info "Creating $rtype $fqdn"
|
||||
resp="$(api POST "https://api.cloudflare.com/client/v4/zones/$zid/dns_records" \
|
||||
--data "{\"type\":\"$rtype\",\"name\":\"$fqdn\",\"content\":\"$rcontent\",\"ttl\":$rttl,\"proxied\":true}")"
|
||||
echo "$resp" | jq -e '.success==true' >/dev/null || die "Failed create for $fqdn"
|
||||
new_id="$(echo "$resp" | jq -r '.result.id')"
|
||||
echo "$new_id" >> "$SKILL_ROOT/outputs/cloudflare_record_ids.txt"
|
||||
fi
|
||||
done < <(jq -c '.[]' "$mirror_file")
|
||||
|
||||
log_info "Cloudflare mirror applied. IDs saved to outputs/cloudflare_record_ids.txt"
|
||||
}
|
||||
main "$@"
|
||||
52
dns-sovereign/scripts/90_verify.sh
Normal file
52
dns-sovereign/scripts/90_verify.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
: "${PDNS_API_KEY:=}"
|
||||
: "${PDNS_PORT:=53}"
|
||||
|
||||
main() {
|
||||
local status="$SKILL_ROOT/outputs/status_matrix.json"
|
||||
local ok_container=false ok_api=false ok_probe=false
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q '^pdns-auth$'; then ok_container=true; fi
|
||||
if [[ -n "${PDNS_API_KEY:-}" ]]; then
|
||||
if curl -fsS -H "X-API-Key: $PDNS_API_KEY" "http://127.0.0.1:${PDNS_WEB_PORT}/api/v1/servers/localhost" >/dev/null 2>&1; then
|
||||
ok_api=true
|
||||
fi
|
||||
fi
|
||||
[[ -f "$SKILL_ROOT/outputs/pdns_api_probe.json" ]] && ok_probe=true
|
||||
|
||||
blockers="[]"
|
||||
if [[ "$ok_container" != "true" ]]; then blockers='["pdns_container_not_running"]'
|
||||
elif [[ "$ok_api" != "true" ]]; then blockers='["pdns_api_unreachable_or_key_missing"]'
|
||||
fi
|
||||
|
||||
cat > "$status" <<EOF
|
||||
{
|
||||
"skill": "dns-sovereign",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"checks": [
|
||||
{"name":"pdns_container_running", "ok": $ok_container},
|
||||
{"name":"pdns_api_reachable", "ok": $ok_api},
|
||||
{"name":"api_probe_saved", "ok": $ok_probe}
|
||||
],
|
||||
"blockers": $blockers,
|
||||
"warnings": [
|
||||
"PowerDNS API is bound to localhost only in compose; keep it private"
|
||||
],
|
||||
"next_steps": [
|
||||
"Create/verify zones and NS records",
|
||||
"Point domain registrar to your NS hosts when ready",
|
||||
"Optionally mirror select records to Cloudflare"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote $status"
|
||||
cat "$status"
|
||||
}
|
||||
main "$@"
|
||||
77
dns-sovereign/scripts/99_report.sh
Normal file
77
dns-sovereign/scripts/99_report.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
source "$SCRIPT_DIR/_common.sh"
|
||||
|
||||
: "${PDNS_PORT:=53}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
: "${PDNS_DATA_DIR:=$HOME/pdns}"
|
||||
: "${ZONE_NAME:=}"
|
||||
: "${CF_ZONE_NAME:=}"
|
||||
|
||||
main() {
|
||||
mkdir -p "$SKILL_ROOT/outputs"
|
||||
local report="$SKILL_ROOT/outputs/audit_report.md"
|
||||
local status="$SKILL_ROOT/outputs/status_matrix.json"
|
||||
|
||||
cat > "$report" <<EOF
|
||||
# DNS Sovereign Audit Report
|
||||
|
||||
**Generated:** $(date -Iseconds)
|
||||
**PDNS DNS Port:** $PDNS_PORT
|
||||
**PDNS API Port (localhost):** $PDNS_WEB_PORT
|
||||
**PDNS Data Dir:** $(json_escape "$PDNS_DATA_DIR")
|
||||
**Zone (PDNS):** $(json_escape "${ZONE_NAME:-}")
|
||||
**Cloudflare Mirror Zone:** $(json_escape "${CF_ZONE_NAME:-}")
|
||||
**Skill Version:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Artifacts
|
||||
|
||||
| Item | Path |
|
||||
|---|---|
|
||||
| Compose | \`$SKILL_ROOT/outputs/compose.yml\` |
|
||||
| pdns.conf | \`$SKILL_ROOT/outputs/pdns.conf\` |
|
||||
| API Probe | \`$SKILL_ROOT/outputs/pdns_api_probe.json\` |
|
||||
| Status Matrix | \`$SKILL_ROOT/outputs/status_matrix.json\` |
|
||||
| Backups | \`$SKILL_ROOT/outputs/backups/\` |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| Authoritative DNS | PowerDNS on your node |
|
||||
| Mirror | Optional Cloudflare mirror |
|
||||
|
||||
---
|
||||
|
||||
## Rollback
|
||||
|
||||
- PDNS stop/remove: \`./scripts/rollback/undo_pdns.sh\`
|
||||
- Delete zone (optional): \`./scripts/rollback/undo_zone.sh\`
|
||||
- Remove CF records created by this skill: \`./scripts/rollback/undo_cloudflare.sh\`
|
||||
|
||||
EOF
|
||||
|
||||
log_info "Wrote $report"
|
||||
cat "$report"
|
||||
}
|
||||
main "$@"
|
||||
38
dns-sovereign/scripts/_common.sh
Normal file
38
dns-sovereign/scripts/_common.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/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}"
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0)."
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo "Type to confirm:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
read -r input
|
||||
[[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch."
|
||||
fi
|
||||
}
|
||||
|
||||
compose_cmd() {
|
||||
if command -v docker-compose >/dev/null 2>&1; then
|
||||
echo "docker-compose"
|
||||
else
|
||||
echo "docker compose"
|
||||
fi
|
||||
}
|
||||
44
dns-sovereign/scripts/rollback/undo_cloudflare.sh
Normal file
44
dns-sovereign/scripts/rollback/undo_cloudflare.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:=}"
|
||||
: "${CF_ZONE_NAME:=}"
|
||||
|
||||
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 required."
|
||||
[[ -n "$CF_ZONE_NAME" ]] || die "CF_ZONE_NAME required."
|
||||
need jq
|
||||
need curl
|
||||
|
||||
local ids_file="$SKILL_ROOT/outputs/cloudflare_record_ids.txt"
|
||||
if [[ ! -f "$ids_file" ]]; then
|
||||
log_warn "No cloudflare_record_ids.txt found; nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local zid; zid="$(api GET "https://api.cloudflare.com/client/v4/zones?name=$CF_ZONE_NAME" | jq -r '.result[0].id')"
|
||||
[[ -n "$zid" && "$zid" != "null" ]] || die "Unable to resolve zone id."
|
||||
|
||||
while IFS= read -r rid; do
|
||||
[[ -n "$rid" ]] || continue
|
||||
log_warn "Deleting Cloudflare DNS record id: $rid"
|
||||
api DELETE "https://api.cloudflare.com/client/v4/zones/$zid/dns_records/$rid" | jq -e '.success==true' >/dev/null || log_warn "Failed delete for $rid"
|
||||
done < "$ids_file"
|
||||
|
||||
rm -f "$ids_file" || true
|
||||
log_info "Cloudflare rollback complete."
|
||||
}
|
||||
main "$@"
|
||||
17
dns-sovereign/scripts/rollback/undo_pdns.sh
Normal file
17
dns-sovereign/scripts/rollback/undo_pdns.sh
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
source "$SKILL_ROOT/scripts/_common.sh"
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
if docker ps -a --format '{{.Names}}' | grep -q '^pdns-auth$'; then
|
||||
log_warn "Stopping/removing pdns-auth container..."
|
||||
docker rm -f pdns-auth || true
|
||||
else
|
||||
log_warn "pdns-auth container not found."
|
||||
fi
|
||||
log_info "PDNS rollback complete. Data preserved in PDNS_DATA_DIR."
|
||||
}
|
||||
main "$@"
|
||||
28
dns-sovereign/scripts/rollback/undo_zone.sh
Normal file
28
dns-sovereign/scripts/rollback/undo_zone.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"
|
||||
|
||||
: "${PDNS_API_KEY:=}"
|
||||
: "${PDNS_WEB_PORT:=8081}"
|
||||
: "${ZONE_NAME:=}"
|
||||
|
||||
api() {
|
||||
local method="$1"; shift
|
||||
local url="$1"; shift
|
||||
curl -sS -X "$method" "$url" -H "X-API-Key: $PDNS_API_KEY" -H "Content-Type: application/json" "$@"
|
||||
}
|
||||
|
||||
main() {
|
||||
confirm_gate
|
||||
[[ -n "$PDNS_API_KEY" ]] || die "PDNS_API_KEY required."
|
||||
[[ -n "$ZONE_NAME" ]] || die "ZONE_NAME required."
|
||||
|
||||
local zone="${ZONE_NAME%\.}."
|
||||
local base="http://127.0.0.1:${PDNS_WEB_PORT}/api/v1/servers/localhost"
|
||||
log_warn "Deleting zone: $zone"
|
||||
api DELETE "$base/zones/$zone" | jq '.' || die "Failed to delete zone."
|
||||
log_info "Zone rollback complete."
|
||||
}
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user