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:
177
backup-sovereign/SKILL.md
Normal file
177
backup-sovereign/SKILL.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
name: backup-sovereign
|
||||
description: >
|
||||
Create encrypted, verifiable backups with proof receipts (BLAKE3 + ROOT.txt)
|
||||
and mandatory restore drill. Uses age encryption for modern, simple UX.
|
||||
Designed for sovereign EU infrastructure. Use after node-hardening completes.
|
||||
Triggers: 'backup node', 'encrypted backup', 'create backup', 'restore drill',
|
||||
'generate proof receipts', 'verify backup', 'backup with proof'.
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Backup Sovereign
|
||||
|
||||
High-risk Tier 1 skill for creating encrypted, verifiable backups. All backups include BLAKE3 proof receipts and require a mandatory restore drill to verify recoverability.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set required parameters
|
||||
export BACKUP_SOURCES="$HOME/infrastructure,$HOME/.claude/skills"
|
||||
export AGE_RECIPIENT_FILE="$HOME/.config/age/recipients.txt"
|
||||
export AGE_IDENTITY_FILE="$HOME/.config/age/identity.txt"
|
||||
|
||||
# Optional: customize
|
||||
export NODE_NAME="node-a"
|
||||
export BACKUP_LABEL="daily"
|
||||
|
||||
# Run preflight
|
||||
./scripts/00_preflight.sh
|
||||
|
||||
# Plan phases (safe to run, shows what WILL happen)
|
||||
./scripts/10_backup_plan.sh
|
||||
./scripts/20_encrypt_plan.sh
|
||||
|
||||
# Apply phases (REQUIRES DRY_RUN=0 and confirmation)
|
||||
export DRY_RUN=0
|
||||
./scripts/11_backup_apply.sh # Type confirmation phrase
|
||||
./scripts/21_encrypt_apply.sh # Type confirmation phrase
|
||||
|
||||
# Generate proof receipts
|
||||
./scripts/30_generate_proof.sh
|
||||
|
||||
# Verify artifacts
|
||||
./scripts/40_verify_backup.sh
|
||||
|
||||
# MANDATORY: Restore drill
|
||||
./scripts/50_restore_drill.sh # Type confirmation phrase
|
||||
|
||||
# Status and report
|
||||
./scripts/90_verify.sh
|
||||
./scripts/99_report.sh
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 0: Preflight (00)
|
||||
Check dependencies: tar, gzip, age, b3sum.
|
||||
Verify BACKUP_SOURCES paths exist.
|
||||
Check available disk space.
|
||||
|
||||
### Phase 1: Backup (10-11)
|
||||
**Two-phase operation with DRY_RUN gate.**
|
||||
|
||||
Plan phase shows:
|
||||
- Source paths to archive
|
||||
- Exclude patterns
|
||||
- Output directory and run ID
|
||||
- Estimated archive size
|
||||
|
||||
Apply phase executes:
|
||||
- Creates tar.gz archive
|
||||
- Generates manifest.json with BLAKE3 hashes
|
||||
- Records excludes.txt
|
||||
|
||||
### Phase 2: Encrypt (20-21)
|
||||
**Two-phase operation with DRY_RUN gate.**
|
||||
|
||||
Plan phase shows:
|
||||
- Encryption method (age)
|
||||
- Recipient file location
|
||||
- Output file path
|
||||
|
||||
Apply phase executes:
|
||||
- Encrypts archive with age
|
||||
- Creates archive.tar.gz.age
|
||||
|
||||
### Phase 3: Proof (30)
|
||||
Generate cryptographic proof receipts:
|
||||
- BLAKE3 hash of manifest.json
|
||||
- BLAKE3 hash of encrypted archive
|
||||
- ROOT.txt (composite hash for anchoring)
|
||||
- PROOF.json (metadata receipt)
|
||||
|
||||
### Phase 4: Verify (40)
|
||||
Verify all artifacts exist and ROOT.txt is valid.
|
||||
|
||||
### Phase 5: Restore Drill (50) **MANDATORY**
|
||||
**DRY_RUN gate + CONFIRM_PHRASE**
|
||||
|
||||
This phase is required to validate backup recoverability:
|
||||
- Decrypts archive to temp directory
|
||||
- Extracts and verifies file count
|
||||
- Records restore location
|
||||
|
||||
### Phase 6: Status + Report (90-99)
|
||||
Generate JSON status matrix and markdown audit report.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|-----------|----------|---------|-------------|
|
||||
| BACKUP_SOURCES | Yes | - | Comma-separated paths to backup |
|
||||
| AGE_RECIPIENT_FILE | Yes | - | File with age public key(s) |
|
||||
| AGE_IDENTITY_FILE | Yes | - | File with age private key (for restore) |
|
||||
| NODE_NAME | No | node-a | Node identifier |
|
||||
| BACKUP_LABEL | No | manual | Label for this backup run |
|
||||
| BACKUP_EXCLUDES | No | .git,node_modules,target,dist,outputs | Exclude patterns |
|
||||
| OUTPUT_DIR | No | outputs | Output directory |
|
||||
| DRY_RUN | No | 1 | Set to 0 to enable apply scripts |
|
||||
| REQUIRE_CONFIRM | No | 1 | Require confirmation phrase |
|
||||
| CONFIRM_PHRASE | No | I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS | Safety phrase |
|
||||
|
||||
## Outputs
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `outputs/runs/<run_id>/archive.tar.gz` | Unencrypted archive |
|
||||
| `outputs/runs/<run_id>/archive.tar.gz.age` | Encrypted archive |
|
||||
| `outputs/runs/<run_id>/manifest.json` | File list + sizes + BLAKE3 hashes |
|
||||
| `outputs/runs/<run_id>/ROOT.txt` | BLAKE3 root (for anchoring) |
|
||||
| `outputs/runs/<run_id>/PROOF.json` | Metadata receipt |
|
||||
| `outputs/runs/<run_id>/excludes.txt` | Exclude patterns used |
|
||||
| `outputs/status_matrix.json` | Verification results |
|
||||
| `outputs/audit_report.md` | Human-readable audit trail |
|
||||
|
||||
## Safety Guarantees
|
||||
|
||||
1. **DRY_RUN=1 by default** - Apply scripts refuse to run without explicit DRY_RUN=0
|
||||
2. **CONFIRM_PHRASE required** - Must type exact phrase to proceed
|
||||
3. **Mandatory restore drill** - Untested backups are not trusted
|
||||
4. **BLAKE3 hashes** - Cryptographic integrity verification
|
||||
5. **ROOT.txt for anchoring** - Can be submitted to merkle-forest/rfc3161-anchor
|
||||
6. **Per-run isolation** - Each backup is immutable once created
|
||||
7. **All scripts idempotent** - Safe to run multiple times
|
||||
|
||||
## age Key Setup
|
||||
|
||||
If you don't have age keys yet:
|
||||
|
||||
```bash
|
||||
# Generate identity (private key)
|
||||
age-keygen -o ~/.config/age/identity.txt
|
||||
|
||||
# Extract public key to recipients file
|
||||
age-keygen -y ~/.config/age/identity.txt > ~/.config/age/recipients.txt
|
||||
```
|
||||
|
||||
## EU Compliance
|
||||
|
||||
| Aspect | Value |
|
||||
|--------|-------|
|
||||
| Data Residency | EU (Ireland - Dublin) |
|
||||
| GDPR Applicable | Yes (depends on backup content) |
|
||||
| Jurisdiction | Irish Law |
|
||||
| Encryption at Rest | Yes (age) |
|
||||
|
||||
## References
|
||||
|
||||
- [Recovery Notes](references/recovery_notes.md)
|
||||
|
||||
## Next Steps
|
||||
|
||||
After completing backup-sovereign:
|
||||
1. Store encrypted bundle off-node (secondary disk / object store)
|
||||
2. Test restore on a different machine (recommended)
|
||||
3. Optionally anchor ROOT.txt with rfc3161-anchor skill
|
||||
4. Proceed to **disaster-recovery** skill
|
||||
18
backup-sovereign/checks/check_restore.sh
Executable file
18
backup-sovereign/checks/check_restore.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check: Last backup has passed restore drill
|
||||
# Returns 0 if restore drill completed, 1 otherwise
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# Check for last run pointer
|
||||
[[ -f "$OUTPUT_DIR/last_run_dir.txt" ]] || exit 1
|
||||
|
||||
run_dir="$(cat "$OUTPUT_DIR/last_run_dir.txt")"
|
||||
|
||||
# Check for restore drill completion
|
||||
[[ -f "$run_dir/last_restore_dir.txt" ]]
|
||||
19
backup-sovereign/checks/check_space.sh
Executable file
19
backup-sovereign/checks/check_space.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check: Sufficient disk space available
|
||||
# Returns 0 if >100MB available, 1 otherwise
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
|
||||
# Ensure output dir exists for df check
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Get available space in KB
|
||||
avail=$(df -P "$OUTPUT_DIR" | awk 'NR==2 {print $4}')
|
||||
|
||||
# Require at least 100MB (102400 KB)
|
||||
[[ "$avail" -ge 102400 ]]
|
||||
10
backup-sovereign/checks/check_tools.sh
Executable file
10
backup-sovereign/checks/check_tools.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Check: Required tools are installed
|
||||
# Returns 0 if all tools present, 1 otherwise
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
command -v tar &>/dev/null && \
|
||||
command -v gzip &>/dev/null && \
|
||||
command -v age &>/dev/null && \
|
||||
(command -v b3sum &>/dev/null || command -v blake3 &>/dev/null)
|
||||
50
backup-sovereign/config.json
Normal file
50
backup-sovereign/config.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"skill": "backup-sovereign",
|
||||
"description": "Encrypted, verifiable backups with BLAKE3 receipts + mandatory restore drill",
|
||||
"parameters": {
|
||||
"required": [
|
||||
"BACKUP_SOURCES",
|
||||
"AGE_RECIPIENT_FILE",
|
||||
"AGE_IDENTITY_FILE"
|
||||
],
|
||||
"optional": {
|
||||
"NODE_NAME": "node-a",
|
||||
"BACKUP_LABEL": "manual",
|
||||
"BACKUP_EXCLUDES": ".git,node_modules,target,dist,outputs",
|
||||
"OUTPUT_DIR": "outputs",
|
||||
"DRY_RUN": 1,
|
||||
"REQUIRE_CONFIRM": 1,
|
||||
"CONFIRM_PHRASE": "I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS"
|
||||
}
|
||||
},
|
||||
"phases": {
|
||||
"preflight": ["00_preflight.sh"],
|
||||
"backup": {
|
||||
"plan": ["10_backup_plan.sh"],
|
||||
"apply": ["11_backup_apply.sh"]
|
||||
},
|
||||
"encrypt": {
|
||||
"plan": ["20_encrypt_plan.sh"],
|
||||
"apply": ["21_encrypt_apply.sh"]
|
||||
},
|
||||
"proof": ["30_generate_proof.sh"],
|
||||
"verify": ["40_verify_backup.sh", "50_restore_drill.sh"],
|
||||
"status": ["90_verify.sh"],
|
||||
"report": ["99_report.sh"]
|
||||
},
|
||||
"checks": {
|
||||
"tools": ["check_tools.sh"],
|
||||
"space": ["check_space.sh"],
|
||||
"restore": ["check_restore.sh"]
|
||||
},
|
||||
"rollback_order": [
|
||||
"undo_last_backup.sh",
|
||||
"purge_outputs.sh"
|
||||
],
|
||||
"eu_compliance": {
|
||||
"data_residency": "EU",
|
||||
"jurisdiction": "Ireland",
|
||||
"gdpr_applicable": true
|
||||
}
|
||||
}
|
||||
135
backup-sovereign/references/recovery_notes.md
Normal file
135
backup-sovereign/references/recovery_notes.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Recovery Notes
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes recovery procedures for backup-sovereign backups.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `age` installed (for decryption)
|
||||
- Access to AGE_IDENTITY_FILE (private key)
|
||||
- Sufficient disk space for extraction
|
||||
|
||||
## Standard Recovery
|
||||
|
||||
### 1. Locate Backup
|
||||
|
||||
Find your encrypted backup:
|
||||
```bash
|
||||
ls ~/.claude/skills/backup-sovereign/outputs/runs/
|
||||
```
|
||||
|
||||
### 2. Decrypt Archive
|
||||
|
||||
```bash
|
||||
# Set identity file
|
||||
export AGE_IDENTITY_FILE="$HOME/.config/age/identity.txt"
|
||||
|
||||
# Decrypt
|
||||
age -d -i "$AGE_IDENTITY_FILE" \
|
||||
-o archive.tar.gz \
|
||||
archive.tar.gz.age
|
||||
```
|
||||
|
||||
### 3. Extract
|
||||
|
||||
```bash
|
||||
# Extract to current directory
|
||||
tar -xzf archive.tar.gz
|
||||
|
||||
# Or extract to specific location
|
||||
tar -xzf archive.tar.gz -C /path/to/restore/
|
||||
```
|
||||
|
||||
### 4. Verify Integrity
|
||||
|
||||
Compare BLAKE3 hash with manifest:
|
||||
```bash
|
||||
# Compute hash of archive
|
||||
b3sum archive.tar.gz
|
||||
|
||||
# Compare with value in manifest.json
|
||||
cat manifest.json | grep blake3
|
||||
```
|
||||
|
||||
## Disaster Recovery
|
||||
|
||||
If you've lost access to your primary system:
|
||||
|
||||
1. **Obtain encrypted backup** from off-site storage
|
||||
2. **Obtain identity file** from secure backup location
|
||||
3. Follow standard recovery steps above
|
||||
|
||||
## Verify ROOT
|
||||
|
||||
To verify the backup hasn't been tampered with:
|
||||
|
||||
```bash
|
||||
# Compute manifest hash
|
||||
MANIFEST_B3=$(b3sum manifest.json | awk '{print $1}')
|
||||
|
||||
# Compute encrypted archive hash
|
||||
ENC_B3=$(b3sum archive.tar.gz.age | awk '{print $1}')
|
||||
|
||||
# Compute ROOT
|
||||
echo -n "${MANIFEST_B3}${ENC_B3}" | b3sum
|
||||
|
||||
# Compare with ROOT.txt
|
||||
cat ROOT.txt
|
||||
```
|
||||
|
||||
## Key Management
|
||||
|
||||
### age Keys
|
||||
|
||||
- **Identity file** (private key): Keep secure, backed up separately
|
||||
- **Recipients file** (public key): Can be shared, used for encryption
|
||||
|
||||
### Generate New Keys
|
||||
|
||||
If you need new keys:
|
||||
```bash
|
||||
# Generate identity
|
||||
age-keygen -o ~/.config/age/identity.txt
|
||||
|
||||
# Extract public key
|
||||
age-keygen -y ~/.config/age/identity.txt > ~/.config/age/recipients.txt
|
||||
```
|
||||
|
||||
### Key Rotation
|
||||
|
||||
1. Generate new keypair
|
||||
2. Add new public key to recipients file
|
||||
3. Keep old identity file for decrypting old backups
|
||||
4. New backups will be encrypted to all recipients
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "age: error: no identity matched any of the recipients"
|
||||
|
||||
- Wrong identity file
|
||||
- Backup was encrypted with different key
|
||||
- Solution: Use correct identity file
|
||||
|
||||
### "tar: Error opening archive"
|
||||
|
||||
- Corrupted archive
|
||||
- Incomplete download
|
||||
- Solution: Verify BLAKE3 hash, re-download if needed
|
||||
|
||||
### "b3sum: command not found"
|
||||
|
||||
- Install b3sum: `cargo install b3sum` or use package manager
|
||||
- Alternative: Use `blake3` CLI if available
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Never store identity file with encrypted backups**
|
||||
2. **Use passphrase-protected identity** for extra security
|
||||
3. **Test restore drill regularly** - backups that haven't been tested aren't backups
|
||||
4. **Store backups off-site** - same location defeats the purpose
|
||||
|
||||
## References
|
||||
|
||||
- [age encryption](https://age-encryption.org/)
|
||||
- [BLAKE3 hash](https://github.com/BLAKE3-team/BLAKE3)
|
||||
114
backup-sovereign/scripts/00_preflight.sh
Executable file
114
backup-sovereign/scripts/00_preflight.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/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}"
|
||||
: "${BACKUP_SOURCES:=}"
|
||||
: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,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_tool() {
|
||||
local tool="$1"
|
||||
if command -v "$tool" &>/dev/null; then
|
||||
log_info "Found: $tool ($(command -v "$tool"))"
|
||||
return 0
|
||||
else
|
||||
log_warn "Missing: $tool"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_b3sum() {
|
||||
if command -v b3sum &>/dev/null; then
|
||||
log_info "Found: b3sum ($(command -v b3sum))"
|
||||
return 0
|
||||
elif command -v blake3 &>/dev/null; then
|
||||
log_info "Found: blake3 ($(command -v blake3))"
|
||||
return 0
|
||||
else
|
||||
log_warn "Missing: b3sum or blake3"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "Starting $SCRIPT_NAME..."
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
local missing=0
|
||||
|
||||
log_info "=== Required Tools ==="
|
||||
check_tool tar || ((missing++))
|
||||
check_tool gzip || ((missing++))
|
||||
check_tool age || ((missing++))
|
||||
check_b3sum || ((missing++))
|
||||
check_tool stat || ((missing++))
|
||||
check_tool find || ((missing++))
|
||||
|
||||
log_info "=== Backup Sources ==="
|
||||
if [[ -z "$BACKUP_SOURCES" ]]; then
|
||||
log_warn "BACKUP_SOURCES not set (required for backup)"
|
||||
else
|
||||
IFS=',' read -r -a sources <<< "$BACKUP_SOURCES"
|
||||
for src in "${sources[@]}"; do
|
||||
# Expand ~ if present
|
||||
src="${src/#\~/$HOME}"
|
||||
if [[ -e "$src" ]]; then
|
||||
log_info "Source exists: $src"
|
||||
else
|
||||
log_warn "Source missing: $src"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
log_info "=== Encryption Files ==="
|
||||
if [[ -n "${AGE_RECIPIENT_FILE:-}" ]]; then
|
||||
if [[ -f "$AGE_RECIPIENT_FILE" ]]; then
|
||||
log_info "AGE_RECIPIENT_FILE exists: $AGE_RECIPIENT_FILE"
|
||||
else
|
||||
log_warn "AGE_RECIPIENT_FILE missing: $AGE_RECIPIENT_FILE"
|
||||
fi
|
||||
else
|
||||
log_warn "AGE_RECIPIENT_FILE not set (required for encryption)"
|
||||
fi
|
||||
|
||||
if [[ -n "${AGE_IDENTITY_FILE:-}" ]]; then
|
||||
if [[ -f "$AGE_IDENTITY_FILE" ]]; then
|
||||
log_info "AGE_IDENTITY_FILE exists: $AGE_IDENTITY_FILE"
|
||||
else
|
||||
log_warn "AGE_IDENTITY_FILE missing: $AGE_IDENTITY_FILE"
|
||||
fi
|
||||
else
|
||||
log_warn "AGE_IDENTITY_FILE not set (required for restore drill)"
|
||||
fi
|
||||
|
||||
log_info "=== Disk Space ==="
|
||||
local avail
|
||||
avail=$(df -P "$OUTPUT_DIR" | awk 'NR==2 {print $4}')
|
||||
log_info "Available space in $OUTPUT_DIR: $((avail / 1024)) MB"
|
||||
|
||||
log_info "=== Parameters ==="
|
||||
log_info "NODE_NAME=${NODE_NAME:-node-a}"
|
||||
log_info "BACKUP_LABEL=${BACKUP_LABEL:-manual}"
|
||||
log_info "BACKUP_EXCLUDES=$BACKUP_EXCLUDES"
|
||||
log_info "DRY_RUN=${DRY_RUN:-1} (apply scripts require DRY_RUN=0)"
|
||||
|
||||
if [[ $missing -gt 0 ]]; then
|
||||
die "Missing $missing required tools. Install them before proceeding."
|
||||
fi
|
||||
|
||||
log_info "Preflight OK."
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
89
backup-sovereign/scripts/10_backup_plan.sh
Executable file
89
backup-sovereign/scripts/10_backup_plan.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/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}"
|
||||
: "${BACKUP_LABEL:=manual}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${BACKUP_SOURCES:=}"
|
||||
: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_plan() { echo "[PLAN] $(date -Iseconds) $*"; }
|
||||
die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; }
|
||||
|
||||
estimate_size() {
|
||||
local total=0
|
||||
IFS=',' read -r -a sources <<< "$BACKUP_SOURCES"
|
||||
IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES"
|
||||
|
||||
for src in "${sources[@]}"; do
|
||||
src="${src/#\~/$HOME}"
|
||||
if [[ -e "$src" ]]; then
|
||||
# Build find exclude args
|
||||
local find_excludes=()
|
||||
for ex in "${excludes[@]}"; do
|
||||
find_excludes+=(-name "$ex" -prune -o)
|
||||
done
|
||||
|
||||
local size
|
||||
size=$(find "$src" "${find_excludes[@]}" -type f -print0 2>/dev/null | \
|
||||
xargs -0 stat -c%s 2>/dev/null | \
|
||||
awk '{sum+=$1} END {print sum+0}')
|
||||
total=$((total + size))
|
||||
fi
|
||||
done
|
||||
echo "$total"
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ -n "$BACKUP_SOURCES" ]] || die "BACKUP_SOURCES is required (comma-separated paths)."
|
||||
|
||||
local ts run_id run_dir
|
||||
ts="$(date -Iseconds | tr ':' '-')"
|
||||
run_id="${NODE_NAME}_${BACKUP_LABEL}_${ts}"
|
||||
run_dir="$OUTPUT_DIR/runs/$run_id"
|
||||
|
||||
log_plan "=== Backup Plan ==="
|
||||
log_plan "Run ID: $run_id"
|
||||
log_plan "Run directory: $run_dir"
|
||||
log_plan "Archive: $run_dir/archive.tar.gz"
|
||||
log_plan "Manifest: $run_dir/manifest.json"
|
||||
echo ""
|
||||
|
||||
log_plan "=== Sources ==="
|
||||
IFS=',' read -r -a sources <<< "$BACKUP_SOURCES"
|
||||
for src in "${sources[@]}"; do
|
||||
src="${src/#\~/$HOME}"
|
||||
if [[ -e "$src" ]]; then
|
||||
log_plan " [OK] $src"
|
||||
else
|
||||
log_plan " [MISSING] $src"
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
|
||||
log_plan "=== Excludes ==="
|
||||
IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES"
|
||||
for ex in "${excludes[@]}"; do
|
||||
log_plan " - $ex"
|
||||
done
|
||||
echo ""
|
||||
|
||||
log_plan "=== Size Estimate ==="
|
||||
local est_bytes est_mb
|
||||
est_bytes=$(estimate_size)
|
||||
est_mb=$((est_bytes / 1024 / 1024))
|
||||
log_plan "Estimated uncompressed: ${est_mb} MB ($est_bytes bytes)"
|
||||
log_plan "Compressed size will be smaller (typically 30-70% of original)"
|
||||
echo ""
|
||||
|
||||
log_plan "Next: ./scripts/11_backup_apply.sh (requires DRY_RUN=0)"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
136
backup-sovereign/scripts/11_backup_apply.sh
Executable file
136
backup-sovereign/scripts/11_backup_apply.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/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}"
|
||||
: "${BACKUP_LABEL:=manual}"
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${BACKUP_SOURCES:=}"
|
||||
: "${BACKUP_EXCLUDES:=.git,node_modules,target,dist,outputs}"
|
||||
: "${DRY_RUN:=1}"
|
||||
: "${REQUIRE_CONFIRM:=1}"
|
||||
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
die() { log_error "$@"; exit 1; }
|
||||
|
||||
b3_file() {
|
||||
if command -v b3sum &>/dev/null; then
|
||||
b3sum "$1" | awk '{print $1}'
|
||||
else
|
||||
blake3 "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
require_confirm() {
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)."
|
||||
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo ""
|
||||
echo "CONFIRMATION REQUIRED"
|
||||
echo "Type the phrase exactly to continue:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
read -r input
|
||||
[[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting."
|
||||
fi
|
||||
}
|
||||
|
||||
json_array() {
|
||||
# Convert comma-separated string to JSON array
|
||||
local input="$1"
|
||||
local first=true
|
||||
echo -n "["
|
||||
IFS=',' read -r -a items <<< "$input"
|
||||
for item in "${items[@]}"; do
|
||||
if [[ "$first" == "true" ]]; then
|
||||
first=false
|
||||
else
|
||||
echo -n ","
|
||||
fi
|
||||
echo -n "\"$item\""
|
||||
done
|
||||
echo -n "]"
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ -n "$BACKUP_SOURCES" ]] || die "BACKUP_SOURCES is required (comma-separated paths)."
|
||||
|
||||
require_confirm
|
||||
|
||||
local ts run_id run_dir archive excludes_file manifest
|
||||
ts="$(date -Iseconds | tr ':' '-')"
|
||||
run_id="${NODE_NAME}_${BACKUP_LABEL}_${ts}"
|
||||
run_dir="$OUTPUT_DIR/runs/$run_id"
|
||||
archive="$run_dir/archive.tar.gz"
|
||||
excludes_file="$run_dir/excludes.txt"
|
||||
manifest="$run_dir/manifest.json"
|
||||
|
||||
mkdir -p "$run_dir"
|
||||
|
||||
# Write excludes file
|
||||
log_info "Writing excludes: $excludes_file"
|
||||
: > "$excludes_file"
|
||||
IFS=',' read -r -a excludes <<< "$BACKUP_EXCLUDES"
|
||||
for ex in "${excludes[@]}"; do
|
||||
echo "$ex" >> "$excludes_file"
|
||||
done
|
||||
|
||||
# Build tar exclude args
|
||||
local tar_excludes=()
|
||||
while IFS= read -r pat; do
|
||||
[[ -n "$pat" ]] && tar_excludes+=("--exclude=$pat")
|
||||
done < "$excludes_file"
|
||||
|
||||
# Expand sources
|
||||
local expanded_sources=()
|
||||
IFS=',' read -r -a sources <<< "$BACKUP_SOURCES"
|
||||
for src in "${sources[@]}"; do
|
||||
expanded_sources+=("${src/#\~/$HOME}")
|
||||
done
|
||||
|
||||
# Create archive
|
||||
log_info "Creating archive: $archive"
|
||||
tar -czf "$archive" "${tar_excludes[@]}" "${expanded_sources[@]}"
|
||||
|
||||
local archive_size archive_b3
|
||||
archive_size=$(stat -c%s "$archive")
|
||||
archive_b3=$(b3_file "$archive")
|
||||
|
||||
log_info "Archive size: $archive_size bytes"
|
||||
log_info "Archive BLAKE3: $archive_b3"
|
||||
|
||||
# Create manifest (pure bash JSON)
|
||||
log_info "Writing manifest: $manifest"
|
||||
cat > "$manifest" <<EOF
|
||||
{
|
||||
"version": 1,
|
||||
"node": "$NODE_NAME",
|
||||
"label": "$BACKUP_LABEL",
|
||||
"run_id": "$run_id",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"sources": $(json_array "$BACKUP_SOURCES"),
|
||||
"excludes": $(json_array "$BACKUP_EXCLUDES"),
|
||||
"archive": {
|
||||
"path": "archive.tar.gz",
|
||||
"bytes": $archive_size,
|
||||
"blake3": "$archive_b3"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Save last run pointer
|
||||
echo "$run_dir" > "$OUTPUT_DIR/last_run_dir.txt"
|
||||
log_info "Saved last run pointer: $OUTPUT_DIR/last_run_dir.txt"
|
||||
|
||||
log_info "Backup complete."
|
||||
log_info "Next: ./scripts/20_encrypt_plan.sh then 21_encrypt_apply.sh"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
63
backup-sovereign/scripts/20_encrypt_plan.sh
Executable file
63
backup-sovereign/scripts/20_encrypt_plan.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/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}"
|
||||
: "${AGE_RECIPIENT_FILE:=}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_plan() { echo "[PLAN] $(date -Iseconds) $*"; }
|
||||
die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; }
|
||||
|
||||
main() {
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
[[ -d "$run_dir" ]] || die "Run directory missing: $run_dir"
|
||||
[[ -f "$run_dir/archive.tar.gz" ]] || die "Archive missing: $run_dir/archive.tar.gz"
|
||||
|
||||
log_plan "=== Encryption Plan ==="
|
||||
log_plan "Method: age"
|
||||
log_plan "Run directory: $run_dir"
|
||||
log_plan "Input: $run_dir/archive.tar.gz"
|
||||
log_plan "Output: $run_dir/archive.tar.gz.age"
|
||||
echo ""
|
||||
|
||||
log_plan "=== Recipient File ==="
|
||||
if [[ -n "$AGE_RECIPIENT_FILE" ]]; then
|
||||
if [[ -f "$AGE_RECIPIENT_FILE" ]]; then
|
||||
log_plan "File: $AGE_RECIPIENT_FILE"
|
||||
log_plan "Recipients:"
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" =~ ^# ]] && continue
|
||||
[[ -z "$line" ]] && continue
|
||||
# Show truncated public key
|
||||
log_plan " - ${line:0:20}..."
|
||||
done < "$AGE_RECIPIENT_FILE"
|
||||
else
|
||||
log_plan "[MISSING] $AGE_RECIPIENT_FILE"
|
||||
fi
|
||||
else
|
||||
log_plan "[NOT SET] AGE_RECIPIENT_FILE required for encryption"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
log_plan "=== Archive Info ==="
|
||||
local size
|
||||
size=$(stat -c%s "$run_dir/archive.tar.gz")
|
||||
log_plan "Archive size: $size bytes ($((size / 1024 / 1024)) MB)"
|
||||
echo ""
|
||||
|
||||
log_plan "Next: ./scripts/21_encrypt_apply.sh (requires DRY_RUN=0)"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
66
backup-sovereign/scripts/21_encrypt_apply.sh
Executable file
66
backup-sovereign/scripts/21_encrypt_apply.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/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}"
|
||||
: "${AGE_RECIPIENT_FILE:=}"
|
||||
: "${DRY_RUN:=1}"
|
||||
: "${REQUIRE_CONFIRM:=1}"
|
||||
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
die() { log_error "$@"; exit 1; }
|
||||
|
||||
require_confirm() {
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)."
|
||||
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo ""
|
||||
echo "CONFIRMATION REQUIRED"
|
||||
echo "Type the phrase exactly to continue:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
read -r input
|
||||
[[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting."
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_confirm
|
||||
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
local archive="$run_dir/archive.tar.gz"
|
||||
[[ -f "$archive" ]] || die "Missing archive: $archive"
|
||||
|
||||
[[ -n "$AGE_RECIPIENT_FILE" ]] || die "AGE_RECIPIENT_FILE is required for encryption."
|
||||
[[ -f "$AGE_RECIPIENT_FILE" ]] || die "AGE_RECIPIENT_FILE not found: $AGE_RECIPIENT_FILE"
|
||||
|
||||
local encrypted="$run_dir/archive.tar.gz.age"
|
||||
|
||||
log_info "Encrypting with age..."
|
||||
log_info "Input: $archive"
|
||||
log_info "Output: $encrypted"
|
||||
log_info "Recipients: $AGE_RECIPIENT_FILE"
|
||||
|
||||
age -R "$AGE_RECIPIENT_FILE" -o "$encrypted" "$archive"
|
||||
|
||||
local enc_size
|
||||
enc_size=$(stat -c%s "$encrypted")
|
||||
log_info "Encrypted size: $enc_size bytes"
|
||||
|
||||
log_info "Encryption complete."
|
||||
log_info "Next: ./scripts/30_generate_proof.sh"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
96
backup-sovereign/scripts/30_generate_proof.sh
Executable file
96
backup-sovereign/scripts/30_generate_proof.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/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}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
log_error() { echo "[ERROR] $(date -Iseconds) $*" >&2; }
|
||||
die() { log_error "$@"; exit 1; }
|
||||
|
||||
b3_file() {
|
||||
if command -v b3sum &>/dev/null; then
|
||||
b3sum "$1" | awk '{print $1}'
|
||||
else
|
||||
blake3 "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
b3_string() {
|
||||
if command -v b3sum &>/dev/null; then
|
||||
echo -n "$1" | b3sum | awk '{print $1}'
|
||||
else
|
||||
echo -n "$1" | blake3
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
local manifest="$run_dir/manifest.json"
|
||||
[[ -f "$manifest" ]] || die "Missing manifest: $manifest"
|
||||
|
||||
# Find encrypted archive
|
||||
local encrypted=""
|
||||
if [[ -f "$run_dir/archive.tar.gz.age" ]]; then
|
||||
encrypted="$run_dir/archive.tar.gz.age"
|
||||
else
|
||||
die "Missing encrypted archive. Run 21_encrypt_apply.sh first."
|
||||
fi
|
||||
|
||||
log_info "Generating proof receipts..."
|
||||
log_info "Run directory: $run_dir"
|
||||
|
||||
# Compute BLAKE3 hashes
|
||||
local manifest_b3 encrypted_b3
|
||||
manifest_b3=$(b3_file "$manifest")
|
||||
encrypted_b3=$(b3_file "$encrypted")
|
||||
|
||||
log_info "Manifest BLAKE3: $manifest_b3"
|
||||
log_info "Encrypted BLAKE3: $encrypted_b3"
|
||||
|
||||
# Compute ROOT = BLAKE3(manifest_b3 || encrypted_b3)
|
||||
# Using stable text concatenation
|
||||
local concat root_b3
|
||||
concat="${manifest_b3}${encrypted_b3}"
|
||||
root_b3=$(b3_string "$concat")
|
||||
|
||||
log_info "ROOT BLAKE3: $root_b3"
|
||||
|
||||
# Write ROOT.txt
|
||||
echo "$root_b3" > "$run_dir/ROOT.txt"
|
||||
log_info "Wrote: $run_dir/ROOT.txt"
|
||||
|
||||
# Write PROOF.json
|
||||
cat > "$run_dir/PROOF.json" <<EOF
|
||||
{
|
||||
"version": 1,
|
||||
"node": "$NODE_NAME",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"artifacts": {
|
||||
"manifest_blake3": "$manifest_b3",
|
||||
"encrypted_archive_blake3": "$encrypted_b3",
|
||||
"root_blake3": "$root_b3"
|
||||
},
|
||||
"computation": "ROOT = BLAKE3(manifest_blake3 || encrypted_archive_blake3)",
|
||||
"notes": "ROOT can be anchored via merkle-forest/rfc3161-anchor skills."
|
||||
}
|
||||
EOF
|
||||
log_info "Wrote: $run_dir/PROOF.json"
|
||||
|
||||
log_info "Proof generation complete."
|
||||
log_info "Next: ./scripts/40_verify_backup.sh"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
74
backup-sovereign/scripts/40_verify_backup.sh
Executable file
74
backup-sovereign/scripts/40_verify_backup.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/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_file() {
|
||||
local path="$1"
|
||||
local name="$2"
|
||||
if [[ -f "$path" ]]; then
|
||||
local size
|
||||
size=$(stat -c%s "$path")
|
||||
log_info "[OK] $name ($size bytes)"
|
||||
return 0
|
||||
else
|
||||
log_warn "[MISSING] $name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
log_info "Verifying backup artifacts..."
|
||||
log_info "Run directory: $run_dir"
|
||||
echo ""
|
||||
|
||||
local missing=0
|
||||
|
||||
log_info "=== Core Artifacts ==="
|
||||
check_file "$run_dir/archive.tar.gz" "archive.tar.gz" || ((missing++))
|
||||
check_file "$run_dir/archive.tar.gz.age" "archive.tar.gz.age" || ((missing++))
|
||||
check_file "$run_dir/manifest.json" "manifest.json" || ((missing++))
|
||||
echo ""
|
||||
|
||||
log_info "=== Proof Artifacts ==="
|
||||
check_file "$run_dir/ROOT.txt" "ROOT.txt" || ((missing++))
|
||||
check_file "$run_dir/PROOF.json" "PROOF.json" || ((missing++))
|
||||
echo ""
|
||||
|
||||
log_info "=== Metadata ==="
|
||||
check_file "$run_dir/excludes.txt" "excludes.txt" || true
|
||||
echo ""
|
||||
|
||||
if [[ $missing -gt 0 ]]; then
|
||||
die "Verification failed: $missing missing artifacts."
|
||||
fi
|
||||
|
||||
log_info "=== ROOT Value ==="
|
||||
local root
|
||||
root=$(cat "$run_dir/ROOT.txt")
|
||||
log_info "ROOT: $root"
|
||||
echo ""
|
||||
|
||||
log_info "Verification complete. All artifacts present."
|
||||
log_info "Next: ./scripts/50_restore_drill.sh (MANDATORY)"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
99
backup-sovereign/scripts/50_restore_drill.sh
Executable file
99
backup-sovereign/scripts/50_restore_drill.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/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}"
|
||||
: "${AGE_IDENTITY_FILE:=}"
|
||||
: "${DRY_RUN:=1}"
|
||||
: "${REQUIRE_CONFIRM:=1}"
|
||||
: "${CONFIRM_PHRASE:=I UNDERSTAND THIS WILL CREATE AND ENCRYPT BACKUPS}"
|
||||
|
||||
# === 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; }
|
||||
|
||||
require_confirm() {
|
||||
[[ "$DRY_RUN" == "0" ]] || die "DRY_RUN=$DRY_RUN (set DRY_RUN=0 to apply)."
|
||||
|
||||
if [[ "$REQUIRE_CONFIRM" == "1" ]]; then
|
||||
echo ""
|
||||
echo "RESTORE DRILL - CONFIRMATION REQUIRED"
|
||||
echo "This will decrypt and extract the backup to verify recoverability."
|
||||
echo "Type the phrase exactly to continue:"
|
||||
echo " $CONFIRM_PHRASE"
|
||||
read -r input
|
||||
[[ "$input" == "$CONFIRM_PHRASE" ]] || die "Confirmation phrase mismatch; aborting."
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
require_confirm
|
||||
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
# Find encrypted archive
|
||||
local encrypted=""
|
||||
if [[ -f "$run_dir/archive.tar.gz.age" ]]; then
|
||||
encrypted="$run_dir/archive.tar.gz.age"
|
||||
else
|
||||
die "Missing encrypted archive. Run 21_encrypt_apply.sh first."
|
||||
fi
|
||||
|
||||
[[ -n "$AGE_IDENTITY_FILE" ]] || die "AGE_IDENTITY_FILE is required for restore drill."
|
||||
[[ -f "$AGE_IDENTITY_FILE" ]] || die "AGE_IDENTITY_FILE not found: $AGE_IDENTITY_FILE"
|
||||
|
||||
# Create temp directory for restore
|
||||
local restore_dir
|
||||
restore_dir=$(mktemp -d)
|
||||
log_info "Restore drill temp directory: $restore_dir"
|
||||
|
||||
local decrypted="$restore_dir/archive.tar.gz"
|
||||
|
||||
# Decrypt
|
||||
log_info "Decrypting archive..."
|
||||
age -d -i "$AGE_IDENTITY_FILE" -o "$decrypted" "$encrypted"
|
||||
log_info "Decryption successful."
|
||||
|
||||
# Extract
|
||||
log_info "Extracting archive..."
|
||||
mkdir -p "$restore_dir/extract"
|
||||
tar -xzf "$decrypted" -C "$restore_dir/extract"
|
||||
|
||||
# Count files
|
||||
local file_count
|
||||
file_count=$(find "$restore_dir/extract" -type f | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$file_count" -eq 0 ]]; then
|
||||
die "Restore drill FAILED: No files extracted."
|
||||
fi
|
||||
|
||||
log_info "Extracted $file_count files."
|
||||
|
||||
# Save restore pointer
|
||||
echo "$restore_dir" > "$run_dir/last_restore_dir.txt"
|
||||
log_info "Saved restore pointer: $run_dir/last_restore_dir.txt"
|
||||
|
||||
echo ""
|
||||
log_info "=========================================="
|
||||
log_info " RESTORE DRILL: PASSED"
|
||||
log_info " Files restored: $file_count"
|
||||
log_info " Location: $restore_dir/extract"
|
||||
log_info "=========================================="
|
||||
echo ""
|
||||
|
||||
log_info "Restore drill complete."
|
||||
log_info "Next: ./scripts/90_verify.sh then ./scripts/99_report.sh"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
139
backup-sovereign/scripts/90_verify.sh
Executable file
139
backup-sovereign/scripts/90_verify.sh
Executable file
@@ -0,0 +1,139 @@
|
||||
#!/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 ===
|
||||
: "${OUTPUT_DIR:=$SKILL_ROOT/outputs}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
die() { echo "[ERROR] $(date -Iseconds) $*" >&2; exit 1; }
|
||||
|
||||
run_check() {
|
||||
local script="$1"
|
||||
if [[ -x "$CHECKS_DIR/$script" ]]; then
|
||||
if "$CHECKS_DIR/$script" &>/dev/null; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
else
|
||||
echo "skip"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
[[ -f "$last_run_file" ]] || die "No last run pointer. Run 11_backup_apply.sh first."
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
local status="$OUTPUT_DIR/status_matrix.json"
|
||||
|
||||
# Check artifacts
|
||||
local has_archive has_encrypted has_manifest has_proof has_root has_restore
|
||||
|
||||
[[ -f "$run_dir/archive.tar.gz" ]] && has_archive="true" || has_archive="false"
|
||||
[[ -f "$run_dir/archive.tar.gz.age" ]] && has_encrypted="true" || has_encrypted="false"
|
||||
[[ -f "$run_dir/manifest.json" ]] && has_manifest="true" || has_manifest="false"
|
||||
[[ -f "$run_dir/PROOF.json" ]] && has_proof="true" || has_proof="false"
|
||||
[[ -f "$run_dir/ROOT.txt" ]] && has_root="true" || has_root="false"
|
||||
[[ -f "$run_dir/last_restore_dir.txt" ]] && has_restore="true" || has_restore="false"
|
||||
|
||||
# Run check scripts
|
||||
local tools_ok space_ok restore_ok
|
||||
tools_ok=$(run_check "check_tools.sh")
|
||||
space_ok=$(run_check "check_space.sh")
|
||||
restore_ok=$(run_check "check_restore.sh")
|
||||
|
||||
# Determine blockers and warnings
|
||||
local blockers="" warnings="" next_steps=""
|
||||
|
||||
if [[ "$has_restore" == "false" ]]; then
|
||||
blockers="${blockers}\"Restore drill not completed\","
|
||||
fi
|
||||
if [[ "$has_encrypted" == "false" ]]; then
|
||||
blockers="${blockers}\"Archive not encrypted\","
|
||||
fi
|
||||
if [[ "$has_manifest" == "false" ]]; then
|
||||
warnings="${warnings}\"Manifest missing\","
|
||||
fi
|
||||
if [[ "$has_proof" == "false" ]]; then
|
||||
warnings="${warnings}\"Proof receipts missing\","
|
||||
fi
|
||||
|
||||
# Determine next steps
|
||||
if [[ "$has_restore" == "true" && "$has_encrypted" == "true" ]]; then
|
||||
next_steps="${next_steps}\"Store encrypted bundle off-node\","
|
||||
next_steps="${next_steps}\"Anchor ROOT.txt with rfc3161-anchor\","
|
||||
next_steps="${next_steps}\"Proceed to disaster-recovery skill\","
|
||||
else
|
||||
if [[ "$has_encrypted" == "false" ]]; then
|
||||
next_steps="${next_steps}\"Run 21_encrypt_apply.sh\","
|
||||
fi
|
||||
if [[ "$has_restore" == "false" ]]; then
|
||||
next_steps="${next_steps}\"Run 50_restore_drill.sh (MANDATORY)\","
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove trailing commas
|
||||
blockers="[${blockers%,}]"
|
||||
warnings="[${warnings%,}]"
|
||||
next_steps="[${next_steps%,}]"
|
||||
|
||||
# Get ROOT value if exists
|
||||
local root_value="null"
|
||||
if [[ -f "$run_dir/ROOT.txt" ]]; then
|
||||
root_value="\"$(cat "$run_dir/ROOT.txt")\""
|
||||
fi
|
||||
|
||||
cat > "$status" <<EOF
|
||||
{
|
||||
"skill": "backup-sovereign",
|
||||
"node": "$NODE_NAME",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"run_dir": "$run_dir",
|
||||
"root": $root_value,
|
||||
"checks": {
|
||||
"archive": $has_archive,
|
||||
"encrypted": $has_encrypted,
|
||||
"manifest": $has_manifest,
|
||||
"proof": $has_proof,
|
||||
"root": $has_root,
|
||||
"restore_drill": $has_restore,
|
||||
"tools": $tools_ok,
|
||||
"space": $space_ok
|
||||
},
|
||||
"blockers": $blockers,
|
||||
"warnings": $warnings,
|
||||
"next_steps": $next_steps
|
||||
}
|
||||
EOF
|
||||
|
||||
log_info "Wrote status matrix: $status"
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " VERIFICATION SUMMARY"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo " Archive: $has_archive"
|
||||
echo " Encrypted: $has_encrypted"
|
||||
echo " Manifest: $has_manifest"
|
||||
echo " Proof: $has_proof"
|
||||
echo " ROOT: $has_root"
|
||||
echo " Restore Drill: $has_restore"
|
||||
echo ""
|
||||
|
||||
# Return success only if restore drill passed
|
||||
[[ "$has_restore" == "true" ]]
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
157
backup-sovereign/scripts/99_report.sh
Executable file
157
backup-sovereign/scripts/99_report.sh
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/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}"
|
||||
: "${NODE_NAME:=node-a}"
|
||||
|
||||
# === FUNCTIONS ===
|
||||
log_info() { echo "[INFO] $(date -Iseconds) $*"; }
|
||||
|
||||
get_file_size() {
|
||||
local path="$1"
|
||||
if [[ -f "$path" ]]; then
|
||||
stat -c%s "$path"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
local report="$OUTPUT_DIR/audit_report.md"
|
||||
local status="$OUTPUT_DIR/status_matrix.json"
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
|
||||
local run_dir="(unknown)"
|
||||
[[ -f "$last_run_file" ]] && run_dir="$(cat "$last_run_file")"
|
||||
|
||||
local root_value="(not generated)"
|
||||
[[ -f "$run_dir/ROOT.txt" ]] && root_value="$(cat "$run_dir/ROOT.txt")"
|
||||
|
||||
local archive_size enc_size
|
||||
archive_size=$(get_file_size "$run_dir/archive.tar.gz")
|
||||
enc_size=$(get_file_size "$run_dir/archive.tar.gz.age")
|
||||
|
||||
local restore_status="NOT COMPLETED"
|
||||
[[ -f "$run_dir/last_restore_dir.txt" ]] && restore_status="PASSED"
|
||||
|
||||
cat > "$report" <<EOF
|
||||
# Backup Sovereign Audit Report
|
||||
|
||||
**Generated:** $(date -Iseconds)
|
||||
**Node:** $NODE_NAME
|
||||
**Run Directory:** $run_dir
|
||||
**Skill Version:** 1.0.0
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report documents the backup operations performed on **$NODE_NAME**
|
||||
for sovereign EU infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Backup Artifacts
|
||||
|
||||
| Artifact | Path | Size |
|
||||
|----------|------|------|
|
||||
| Archive | archive.tar.gz | $archive_size bytes |
|
||||
| Encrypted | archive.tar.gz.age | $enc_size bytes |
|
||||
| Manifest | manifest.json | $(get_file_size "$run_dir/manifest.json") bytes |
|
||||
| Proof | PROOF.json | $(get_file_size "$run_dir/PROOF.json") bytes |
|
||||
| ROOT | ROOT.txt | $(get_file_size "$run_dir/ROOT.txt") bytes |
|
||||
|
||||
---
|
||||
|
||||
## Proof Receipt
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ROOT (BLAKE3) | \`$root_value\` |
|
||||
|
||||
This ROOT value can be anchored via:
|
||||
- merkle-forest skill (aggregate with other proofs)
|
||||
- rfc3161-anchor skill (RFC 3161 timestamp)
|
||||
- eth-anchor / btc-anchor skills (blockchain anchoring)
|
||||
|
||||
---
|
||||
|
||||
## Restore Drill
|
||||
|
||||
| Status | Result |
|
||||
|--------|--------|
|
||||
| Restore Drill | **$restore_status** |
|
||||
|
||||
$(if [[ -f "$run_dir/last_restore_dir.txt" ]]; then
|
||||
echo "Restore location: \`$(cat "$run_dir/last_restore_dir.txt")\`"
|
||||
else
|
||||
echo "**WARNING:** Backup has not been verified via restore drill."
|
||||
echo "Run \`./scripts/50_restore_drill.sh\` to validate recoverability."
|
||||
fi)
|
||||
|
||||
---
|
||||
|
||||
## Verification Results
|
||||
|
||||
$(if [[ -f "$status" ]]; then
|
||||
echo '```json'
|
||||
cat "$status"
|
||||
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 (depends on backup content) |
|
||||
| Jurisdiction | Irish Law |
|
||||
| Encryption at Rest | Yes (age) |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Store encrypted bundle off-node (secondary disk / object store)
|
||||
2. Test restore on a different machine (recommended)
|
||||
3. Anchor ROOT.txt with rfc3161-anchor skill
|
||||
4. Proceed to **disaster-recovery** skill
|
||||
|
||||
---
|
||||
|
||||
## Artifact Locations
|
||||
|
||||
| Artifact | Path |
|
||||
|----------|------|
|
||||
| Archive | $run_dir/archive.tar.gz |
|
||||
| Encrypted Archive | $run_dir/archive.tar.gz.age |
|
||||
| Manifest | $run_dir/manifest.json |
|
||||
| ROOT | $run_dir/ROOT.txt |
|
||||
| Proof | $run_dir/PROOF.json |
|
||||
| Status Matrix | $OUTPUT_DIR/status_matrix.json |
|
||||
| This Report | $OUTPUT_DIR/audit_report.md |
|
||||
|
||||
---
|
||||
|
||||
*Report generated by backup-sovereign skill v1.0.0*
|
||||
*$(date -Iseconds)*
|
||||
EOF
|
||||
|
||||
log_info "Audit report written to $report"
|
||||
echo ""
|
||||
cat "$report"
|
||||
log_info "Completed $SCRIPT_NAME"
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
59
backup-sovereign/scripts/rollback/purge_outputs.sh
Executable file
59
backup-sovereign/scripts/rollback/purge_outputs.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(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; }
|
||||
|
||||
main() {
|
||||
if [[ ! -d "$OUTPUT_DIR" ]]; then
|
||||
log_info "Output directory does not exist. Nothing to purge."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local run_count=0
|
||||
if [[ -d "$OUTPUT_DIR/runs" ]]; then
|
||||
run_count=$(find "$OUTPUT_DIR/runs" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
log_warn "This will PERMANENTLY DELETE all backup outputs:"
|
||||
log_warn " Directory: $OUTPUT_DIR"
|
||||
log_warn " Backup runs: $run_count"
|
||||
log_warn ""
|
||||
log_warn "This action cannot be undone!"
|
||||
echo ""
|
||||
echo "Type 'PURGE ALL' to confirm:"
|
||||
read -r confirm
|
||||
[[ "$confirm" == "PURGE ALL" ]] || die "Aborted."
|
||||
|
||||
# Clean up any restore drill temp directories
|
||||
if [[ -d "$OUTPUT_DIR/runs" ]]; then
|
||||
for run_dir in "$OUTPUT_DIR/runs"/*; do
|
||||
if [[ -f "$run_dir/last_restore_dir.txt" ]]; then
|
||||
local restore_dir
|
||||
restore_dir="$(cat "$run_dir/last_restore_dir.txt")"
|
||||
if [[ -d "$restore_dir" ]]; then
|
||||
log_info "Removing restore temp: $restore_dir"
|
||||
rm -rf "$restore_dir"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
log_info "Purging outputs directory..."
|
||||
rm -rf "$OUTPUT_DIR"/*
|
||||
|
||||
log_info "Purge complete."
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
61
backup-sovereign/scripts/rollback/undo_last_backup.sh
Executable file
61
backup-sovereign/scripts/rollback/undo_last_backup.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === METADATA ===
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(dirname "$(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; }
|
||||
|
||||
main() {
|
||||
local last_run_file="$OUTPUT_DIR/last_run_dir.txt"
|
||||
|
||||
if [[ ! -f "$last_run_file" ]]; then
|
||||
log_warn "No last run pointer found. Nothing to undo."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local run_dir
|
||||
run_dir="$(cat "$last_run_file")"
|
||||
|
||||
if [[ ! -d "$run_dir" ]]; then
|
||||
log_warn "Run directory does not exist: $run_dir"
|
||||
rm -f "$last_run_file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_warn "This will remove the last backup run:"
|
||||
log_warn " $run_dir"
|
||||
echo ""
|
||||
echo "Type 'DELETE' to confirm:"
|
||||
read -r confirm
|
||||
[[ "$confirm" == "DELETE" ]] || die "Aborted."
|
||||
|
||||
# Clean up restore drill temp directory if it exists
|
||||
if [[ -f "$run_dir/last_restore_dir.txt" ]]; then
|
||||
local restore_dir
|
||||
restore_dir="$(cat "$run_dir/last_restore_dir.txt")"
|
||||
if [[ -d "$restore_dir" ]]; then
|
||||
log_info "Removing restore drill temp: $restore_dir"
|
||||
rm -rf "$restore_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "Removing run directory: $run_dir"
|
||||
rm -rf "$run_dir"
|
||||
|
||||
log_info "Removing last run pointer"
|
||||
rm -f "$last_run_file"
|
||||
|
||||
log_info "Undo complete."
|
||||
}
|
||||
|
||||
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"
|
||||
60
backup-sovereign/templates/manifest.schema.json
Normal file
60
backup-sovereign/templates/manifest.schema.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Backup Manifest",
|
||||
"description": "Schema for backup-sovereign manifest.json files",
|
||||
"type": "object",
|
||||
"required": ["version", "node", "label", "run_id", "created_at", "sources", "archive"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "integer",
|
||||
"description": "Manifest schema version",
|
||||
"const": 1
|
||||
},
|
||||
"node": {
|
||||
"type": "string",
|
||||
"description": "Node identifier"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Backup label (e.g., daily, weekly, manual)"
|
||||
},
|
||||
"run_id": {
|
||||
"type": "string",
|
||||
"description": "Unique run identifier (node_label_timestamp)"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "ISO 8601 timestamp of backup creation"
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of source paths included in backup"
|
||||
},
|
||||
"excludes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of exclude patterns"
|
||||
},
|
||||
"archive": {
|
||||
"type": "object",
|
||||
"required": ["path", "bytes", "blake3"],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to archive file"
|
||||
},
|
||||
"bytes": {
|
||||
"type": "integer",
|
||||
"description": "Archive size in bytes"
|
||||
},
|
||||
"blake3": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$",
|
||||
"description": "BLAKE3 hash of archive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user