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

79
secrets-vault/SKILL.md Normal file
View File

@@ -0,0 +1,79 @@
---
name: secrets-vault
description: >
Establish a sovereign secrets vault using age + sops (GitOps-friendly).
Generates/installs an age identity, writes .sops.yaml policy, scaffolds encrypted templates,
and provides plan/apply/rollback/verify/report. Triggers: 'set up secrets vault',
'age sops vault', 'encrypt secrets', 'sops policy', 'rotate age key'.
version: 1.0.0
---
# Secrets Vault (age + sops)
This skill forges a **GitOps-native secrets vault**:
- **age**: modern encryption keys (simple UX)
- **sops**: encrypt YAML/JSON/ENV files for storage in git
- `.sops.yaml`: repository policy for automatic encryption recipients
It is designed to be safe on **Node A** and portable to future nodes.
## What it produces
A standard layout (you can commit ciphertext to your infra repo):
```
vault/
.sops.yaml
secrets/
cloudflare.enc.yaml
gitea.enc.yaml
registry.enc.yaml
k8s.enc.yaml
README.md
```
## Quick Start
```bash
cd ~/.claude/skills/secrets-vault
# where the vault lives (repo dir recommended)
export VAULT_ROOT="$HOME/infrastructure/vault"
# safety (apply scripts require DRY_RUN=0 and confirmation)
export DRY_RUN=1
export REQUIRE_CONFIRM=1
export CONFIRM_PHRASE="I UNDERSTAND THIS WILL CREATE A SECRETS VAULT"
./scripts/00_preflight.sh
./scripts/10_plan.sh
export DRY_RUN=0
./scripts/11_apply.sh
./scripts/90_verify.sh
./scripts/99_report.sh
```
## Inputs
| Parameter | Required | Default | Description |
|---|---:|---|---|
| VAULT_ROOT | No | ~/infrastructure/vault | Where to create the vault structure |
| AGE_KEY_DIR | No | ~/.config/sops/age | Where age identities live |
| AGE_KEYS_FILE | No | ~/.config/sops/age/keys.txt | age identity file (0600) |
| RECIPIENTS_FILE | No | outputs/recipients.txt | Generated recipients list |
| DRY_RUN | No | 1 | Apply refuses unless DRY_RUN=0 |
| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase |
| CONFIRM_PHRASE | No | I UNDERSTAND THIS WILL CREATE A SECRETS VAULT | Safety phrase |
## Outputs
- `outputs/status_matrix.json`
- `outputs/audit_report.md`
- `outputs/recipients.txt`
- `outputs/backups/*` (backups of changed files)
## EU Compliance
EU (Ireland - Dublin), Irish jurisdiction. Secrets remain local-first; git stores ciphertext only.

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
: "${VAULT_ROOT:=~/infrastructure/vault}"
vr="$(eval echo "$VAULT_ROOT")"
for f in cloudflare gitea registry k8s; do
p="$vr/secrets/$f.enc.yaml"
[[ -f "$p" ]] || { echo "missing $p"; exit 1; }
grep -q "sops:" "$p" || { echo "not encrypted: $p"; exit 1; }
done
echo "[OK] ciphertext"

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
: "${VAULT_ROOT:=~/infrastructure/vault}"
vr="$(eval echo "$VAULT_ROOT")"
[[ -f "$vr/.sops.yaml" ]] || { echo "missing .sops.yaml"; exit 1; }
grep -q "creation_rules" "$vr/.sops.yaml" || { echo "invalid policy"; exit 1; }
echo "[OK] policy"

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
command -v age >/dev/null || { echo "missing age"; exit 1; }
command -v sops >/dev/null || { echo "missing sops"; exit 1; }
echo "[OK] tools"

50
secrets-vault/config.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "secrets-vault",
"version": "1.0.0",
"defaults": {
"VAULT_ROOT": "~/infrastructure/vault",
"AGE_KEY_DIR": "~/.config/sops/age",
"AGE_KEYS_FILE": "~/.config/sops/age/keys.txt",
"DRY_RUN": "1",
"REQUIRE_CONFIRM": "1",
"CONFIRM_PHRASE": "I UNDERSTAND THIS WILL CREATE A SECRETS VAULT"
},
"phases": {
"preflight": [
"00_preflight.sh"
],
"vault": {
"plan": [
"10_plan.sh"
],
"apply": [
"11_apply.sh"
],
"rollback": [
"rollback/undo_last_changes.sh"
]
},
"verify": [
"90_verify.sh"
],
"report": [
"99_report.sh"
]
},
"checks": {
"tools": [
"checks/check_tools.sh"
],
"policy": [
"checks/check_policy.sh"
],
"ciphertext": [
"checks/check_ciphertext.sh"
]
},
"eu_compliance": {
"data_residency": "EU",
"jurisdiction": "Ireland",
"gdpr_applicable": true
}
}

View File

@@ -0,0 +1,13 @@
# Recovery Notes (age + sops)
## If you lose the age identity
You cannot decrypt existing secrets without the age private key stored in:
- `~/.config/sops/age/keys.txt`
Keep an offline recovery copy (USB/QR/printed).
## Rotating recipients
Add additional recipients to `.sops.yaml` and re-encrypt:
- `sops updatekeys -y secrets/*.enc.yaml`

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_common.sh"
main() {
need age
need sops
need mkdir
need chmod
need stat
need grep
need sed
need date
log_info "Preflight OK."
}
main "$@"

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_common.sh"
: "${VAULT_ROOT:=~/infrastructure/vault}"
: "${AGE_KEY_DIR:=~/.config/sops/age}"
: "${AGE_KEYS_FILE:=~/.config/sops/age/keys.txt}"
main() {
vr="$(expand_path "$VAULT_ROOT")"
kd="$(expand_path "$AGE_KEY_DIR")"
kf="$(expand_path "$AGE_KEYS_FILE")"
echo "[PLAN] $(date -Iseconds) secrets-vault (age+sops)"
echo "[PLAN] VAULT_ROOT: $vr"
echo "[PLAN] AGE_KEY_DIR: $kd"
echo "[PLAN] AGE_KEYS_FILE: $kf"
echo
echo "[PLAN] Will ensure age identity exists (keys.txt)."
echo "[PLAN] Will write vault/.sops.yaml and encrypted templates under vault/secrets/."
echo "[PLAN] Will NOT print secret plaintext."
echo
echo "[PLAN] Next: export DRY_RUN=0 && ./scripts/11_apply.sh"
}
main "$@"

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
source "$SCRIPT_DIR/_common.sh"
: "${VAULT_ROOT:=~/infrastructure/vault}"
: "${AGE_KEY_DIR:=~/.config/sops/age}"
: "${AGE_KEYS_FILE:=~/.config/sops/age/keys.txt}"
: "${RECIPIENTS_FILE:=$SKILL_ROOT/outputs/recipients.txt}"
: "${BACKUP_DIR:=$SKILL_ROOT/outputs/backups}"
backup_file() {
local f="$1"
mkdir -p "$BACKUP_DIR"
if [[ -f "$f" ]]; then
ts="$(date -Iseconds | tr ':' '-')"
cp -p "$f" "$BACKUP_DIR/$(basename "$f").${ts}.bak"
fi
}
main() {
confirm_gate
mkdir -p "$SKILL_ROOT/outputs" "$BACKUP_DIR"
vr="$(expand_path "$VAULT_ROOT")"
kd="$(expand_path "$AGE_KEY_DIR")"
kf="$(expand_path "$AGE_KEYS_FILE")"
mkdir -p "$vr/secrets"
mkdir -p "$kd"
# 1) ensure age identity exists
if [[ ! -f "$kf" ]]; then
log_info "Generating age identity: $kf"
age-keygen -o "$kf" >/dev/null
chmod 600 "$kf"
else
log_info "Using existing age identity: $kf"
chmod 600 "$kf" || true
fi
# extract recipient (public key)
recipient="$(grep -E '^# public key: ' "$kf" | head -n1 | sed 's/^# public key: //')"
[[ -n "$recipient" ]] || die "Could not parse public key from $kf"
echo "$recipient" > "$RECIPIENTS_FILE"
# 2) write .sops.yaml policy
policy="$vr/.sops.yaml"
backup_file "$policy"
cat > "$policy" <<EOF
creation_rules:
- path_regex: secrets/.*\\.enc\\.(yaml|yml|json|env)$
encrypted_regex: '^(data|stringData|secrets|values)$'
age: ["$recipient"]
EOF
# 3) scaffold encrypted templates
# plaintext templates are generated into a temp file then immediately encrypted and removed.
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
# cloudflare
cat > "$tmpdir/cloudflare.yaml" <<EOF
data:
account_id: "REPLACE_ME"
tunnel_token: "REPLACE_ME"
api_token: "REPLACE_ME"
EOF
sops --encrypt --age "$recipient" "$tmpdir/cloudflare.yaml" > "$vr/secrets/cloudflare.enc.yaml"
# gitea
cat > "$tmpdir/gitea.yaml" <<EOF
data:
admin_username: "REPLACE_ME"
admin_password: "REPLACE_ME"
admin_email: "REPLACE_ME"
secret_key: "REPLACE_ME"
EOF
sops --encrypt --age "$recipient" "$tmpdir/gitea.yaml" > "$vr/secrets/gitea.enc.yaml"
# registry
cat > "$tmpdir/registry.yaml" <<EOF
data:
htpasswd: "REPLACE_ME"
tls_cert_pem: "REPLACE_ME"
tls_key_pem: "REPLACE_ME"
EOF
sops --encrypt --age "$recipient" "$tmpdir/registry.yaml" > "$vr/secrets/registry.enc.yaml"
# k8s
cat > "$tmpdir/k8s.yaml" <<EOF
data:
kubeconfig: "REPLACE_ME"
cluster_token: "REPLACE_ME"
EOF
sops --encrypt --age "$recipient" "$tmpdir/k8s.yaml" > "$vr/secrets/k8s.enc.yaml"
# 4) vault README
readme="$vr/README.md"
backup_file "$readme"
cat > "$readme" <<EOF
# Vault (age + sops)
This folder stores **encrypted secrets** for GitOps.
## Edit a secret
\`\`\`bash
sops secrets/cloudflare.enc.yaml
\`\`\`
## Decrypt to stdout (careful)
\`\`\`bash
sops -d secrets/cloudflare.enc.yaml
\`\`\`
## Policy
See \`.sops.yaml\`. Only files matching \`secrets/*.enc.(yaml|json|env)\` are covered.
## Key location
- age identity: \`$kf\` (0600)
- public recipient: stored in \`outputs/recipients.txt\` by the skill
EOF
echo "$vr" > "$SKILL_ROOT/outputs/vault_root.txt"
log_info "Vault forged at: $vr"
log_info "Recipient: $recipient"
}
main "$@"

View 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"
: "${VAULT_ROOT:=~/infrastructure/vault}"
: "${AGE_KEYS_FILE:=~/.config/sops/age/keys.txt}"
: "${RECIPIENTS_FILE:=$SKILL_ROOT/outputs/recipients.txt}"
main() {
vr="$(expand_path "$VAULT_ROOT")"
kf="$(expand_path "$AGE_KEYS_FILE")"
status="$SKILL_ROOT/outputs/status_matrix.json"
ok_keys=false; ok_perm=false; ok_policy=false; ok_cipher=false
[[ -f "$kf" ]] && ok_keys=true
if [[ -f "$kf" ]]; then
perm="$(stat -c '%a' "$kf" 2>/dev/null || echo "")"
[[ "$perm" == "600" ]] && ok_perm=true
fi
[[ -f "$vr/.sops.yaml" ]] && ok_policy=true
if [[ -f "$vr/secrets/cloudflare.enc.yaml" && -f "$vr/secrets/gitea.enc.yaml" && -f "$vr/secrets/registry.enc.yaml" && -f "$vr/secrets/k8s.enc.yaml" ]]; then
ok_cipher=true
fi
blockers="[]"
if [[ "$ok_keys" != "true" ]]; then blockers='["missing_age_identity"]'
elif [[ "$ok_policy" != "true" ]]; then blockers='["missing_sops_policy"]'
elif [[ "$ok_cipher" != "true" ]]; then blockers='["missing_encrypted_templates"]'
fi
cat > "$status" <<EOF
{
"skill":"secrets-vault",
"timestamp":"$(date -Iseconds)",
"vault_root":"$(json_escape "$vr")",
"checks":[
{"name":"age_identity_present", "ok": $ok_keys, "path":"$(json_escape "$kf")"},
{"name":"age_identity_perm_600", "ok": $ok_perm},
{"name":"sops_policy_present", "ok": $ok_policy, "path":"$(json_escape "$vr/.sops.yaml")"},
{"name":"encrypted_templates_present", "ok": $ok_cipher}
],
"blockers": $blockers,
"warnings": [],
"next_steps": ["commit vault ciphertext to git", "cloudflare-tunnel-manager", "container-registry"]
}
EOF
log_info "Wrote $status"
cat "$status"
}
main "$@"

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
source "$SCRIPT_DIR/_common.sh"
main() {
status="$SKILL_ROOT/outputs/status_matrix.json"
report="$SKILL_ROOT/outputs/audit_report.md"
vault_root="$(cat "$SKILL_ROOT/outputs/vault_root.txt" 2>/dev/null || echo "")"
cat > "$report" <<EOF
# Secrets Vault Audit Report (age + sops)
**Generated:** $(date -Iseconds)
**Vault Root:** \`$vault_root\`
**Skill Version:** 1.0.0
## Status Matrix
$(if [[ -f "$status" ]]; then echo '```json'; cat "$status"; echo '```'; else echo "_Missing status_matrix.json_"; fi)
## Operating Rules
- Commit **only ciphertext** (\`*.enc.*\`) to git.
- Never store plaintext secrets in the repo.
- Keep \`~/.config/sops/age/keys.txt\` at **0600**.
- Keep an offline recovery copy of the age identity.
## EU Compliance
EU (Ireland - Dublin), Irish jurisdiction. Secrets remain local-first; git stores ciphertext only.
EOF
log_info "Wrote $report"
cat "$report"
}
main "$@"

View File

@@ -0,0 +1,32 @@
#!/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"; }
expand_path() {
local p="$1"
# shellcheck disable=SC2086
eval echo "$p"
}
confirm_gate() {
: "${DRY_RUN:=1}"
: "${REQUIRE_CONFIRM:=1}"
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE A SECRETS VAULT}"
[[ "$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
}
json_escape() {
local s="$1"
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//$'\n'/\\n}"
printf "%s" "$s"
}

View File

@@ -0,0 +1,22 @@
#!/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"
: "${BACKUP_DIR:=$SKILL_ROOT/outputs/backups}"
main() {
confirm_gate
if [[ ! -d "$BACKUP_DIR" ]]; then
log_warn "No backups found; nothing to restore."
exit 0
fi
log_warn "Backups exist in: $BACKUP_DIR"
log_warn "This rollback restores only files we backed up (.sops.yaml, README.md)."
log_warn "Encrypted templates are safe to keep; remove manually if desired."
echo "Listing backups:"
ls -1 "$BACKUP_DIR" || true
log_info "Rollback is conservative by design. Restore manually by copying desired *.bak to target."
}
main "$@"