18 KiB
Company Name — Disaster Recovery Plan v3 (Consolidated)
Author: [Lead Sysadmin] / Network Services Team, incorporating third-party DR review Date: July 9, 2026 Status: LIVE — replaces v1.0 Supersedes: server-dr-plans.md v1.0, server-provisioning-standard-v1.md, hermes-dr-plan-v2.md
Table of Contents
- Architecture & Shared Dependencies
- Server Standards
- Backup Strategy
- Failover / Failback
- Monitoring & Alerting
- Restore Runbooks
- Testing & Validation
- Security
- Immediate Action Items
- Appendix: Third-Party Review Responses
1. Architecture & Shared Dependencies
Internet ──► CDN/DNS Provider ──► Caddy ──► Service (HTTP/HTTPS)
│
┌───────────┼───────────┐
│ │ │
SSH/WG Tailscale Management
(company-prefix- (Primary Host ↔ VPN (WireGuard
infra) Standby Host) to home router)
│ │ │
└───────────┼───────────┘
│
┌───────────┴───────────┐
│ │
S3 Primary S3 Secondary
(Object Storage Provider us-east-1) (Object Storage Provider us-west-2
versioning ON or Backblaze B2)
object lock ON object lock ON
Shared Dependencies — Documented
| Dependency | Primary | Backup / Fallback | Credentials Location |
|---|---|---|---|
| SSH access | company-prefix-infra key (Secondary Provider vault) | Rescue mode / console | ~/.hermes/.env + Hudu |
| DNS | CDN/DNS Provider (API token in .env) | Manual via web UI | ~/.hermes/.env |
| S3 backup (primary) | Object Storage Provider us-east-1 | Object Storage Provider us-west-2 (to be created) | ~/.aws/credentials |
| S3 backup (secondary) | Object Storage Provider us-west-2 | Backblaze B2 (evaluate) | TBD |
| SMTP relay | [MAIL-SERVER-DOMAIN]:2525 | Direct Email Provider (port 465/587) | ~/.hermes/.env |
| Auth/SSO | CDN/DNS Provider Access | Local portal auth fallback | CDN/DNS Provider dashboard |
| Monitoring | Prometheus node_exporter | Uptime Kuma (docker box) | N/A (pull model) |
| VPN management | WireGuard (wg0, port 51821) | Tailscale direct tunnel | /etc/wireguard/ |
3.1 Backup Matrix
| Data Type | Frequency | Target | Retention | Status |
|---|---|---|---|---|
| Hermes state | Every 15 min | S3 primary (live/) | 48 hours | ✅ LIVE |
| Hermes full | Daily 5 AM | S3 primary (full/) | 90 days | ✅ LIVE |
| Memory snapshots | Every 10 min | S3 primary (memories/) | 48 hours | ✅ LIVE (new) |
| Memory history | Every 10 min | S3 primary (memories/history/) | 90 days | ✅ LIVE (new) |
| Router configs | Daily 6 AM | S3 primary (mikrotik/) | 90 days | ✅ LIVE |
| System configs | Daily | S3 primary (/) | 90 days | ⚠️ NOT AUTOMATED |
| Docker volumes | Daily | S3 primary (/) | 90 days | ❌ NOT DONE |
| Database dumps | Every 6 hours | S3 primary (/) | 90 days | ❌ NOT DONE |
| Application data | Per-app | S3 primary (/) | 90 days | ⚠️ PARTIAL |
| S3 secondary | Daily sync | Object Storage Provider us-west-2 | 90 days | ❌ NOT SET UP |
3.3 Backup Security
- Versioning: ON (all 3 buckets) ✅
- Object Lock: NOT enabled — needs implementation
- Delete protection: NOT configured
- Backup credentials: Write-only IAM user — NOT implemented (using admin credentials)
- Encryption at rest: Object Storage Provider server-side (default) — no client-side encryption
- Credentials escrow: Partial (in .env + Hudu)
Fixes needed:
- Create IAM sub-user with
s3:PutObjectonly (no delete, no list) - Enable Object Lock with 7-day minimum retention
- Store write-only credentials separately from admin credentials
- Client-side encrypt backup before upload (age/gpg) for the secondary copy
4. Failover / Failback
4.1 Warm Standby — Corrected Timing
The DR reviewer flagged this correctly: you cannot detect a 2-minute outage with a 5-minute check interval.
Old: Check every 5 min, failover after 2 min unreachable
New: Check every 30 seconds, failover after 4 failed checks (~2 min)
# Pseudocode for corrected watchdog
CHECK_INTERVAL = 30 # seconds
FAILURE_THRESHOLD = 4 # consecutive failures
CONFIRMATION_WINDOW = 120 # seconds (2 min)
# Detection time: 4 * 30s = 120s = 2 minutes
# Sync + start time: ~45-90 seconds
# Effective RTO: ~3-3.5 minutes
# Stated RTO target: 5 minutes (ACHIEVABLE)
4.2 Health Check — Two Layers
Layer 1 (Ping): ICMP to Primary Host ([IP-REDACTED]) every 30s
Layer 2 (HTTP): GET /[GENERIC-HEALTH-ENDPOINT] on Tailscale IP — checks:
- Gateway heartbeat ≤ 5s old
- SQLite
PRAGMA quick_checkreturns "ok" - Local Ollama responding
Decision Matrix:
| Server | Hermes | Action |
|---|---|---|
| ✅ UP | ✅ UP | Stay dormant |
| ❌ DOWN | N/A | Auto-takeover — unambiguous |
| ✅ UP | ❌ DOWN | ALERT + fence — attempt SSH fence, only takeover if Primary Host Hermes confirmed stopped |
4.3 Fencing Protocol
# SSH Fence (preferred — server is up)
ssh [USER-REDACTED]@[IP-REDACTED] \
"sudo systemctl stop hermes && sudo systemctl mask hermes"
# Verify: returns "inactive"
ssh [USER-REDACTED]@[IP-REDACTED] "sudo systemctl is-active hermes"
# S3 Ownership Lock
# Object: s3://hermes-vps-backups/ACTIVE_OWNER
# Primary Host refreshes every 60s while healthy
# Standby writes itself as owner on takeover
# Primary Host reads on startup — if standby owns it, Primary Host stays dormant
4.4 Failback Protocol
⚠️ Auto-failback is FORBIDDEN. Human clearance required.
# FAILBACK STEPS (manual):
# 1. Confirm Primary Host is healthy: SSH + systemctl is-active hermes
# 2. Stop standby gateway: ssh Standby Host "hermes gateway stop"
# 3. Sync state from standby back to Primary Host (if needed):
# Rsync /root/.hermes/ from standby to Primary Host
# 4. Unmask Hermes on Primary Host:
# ssh Primary Host "sudo systemctl unmask hermes && sudo systemctl start hermes"
# 5. Verify Primary Host gateway is active (Telegram responds)
# 6. Standby returns to dormant state
# 7. Update S3 ownership lock to "core"
4.5 Provider/Account Outage Scenario
If the entire hosting provider (Primary Provider) is unavailable:
- Standby Host (Secondary Provider) takes over as active Hermes host
- DNS records updated to point to Standby Host IP ([IP-REDACTED])
- Services migrate to Primary Provider replacement when available
- No auto-failback — human decision
5. Monitoring & Alerting
5.1 Alert Thresholds
| Alert | Threshold | Action | Priority |
|---|---|---|---|
| Disk usage | > 80% | Notify, investigate | 🟡 P2 |
| Disk usage | > 90% | Notify, immediate cleanup | 🔴 P0 |
| Backup age | > 24h since last success | Notify | 🟡 P1 |
| Backup failure | Any failed job | Notify | 🔴 P0 |
| Hermes heartbeat | No heartbeat > 30s | Notify | 🔴 P0 |
| Hermes down + server up | Layer 2 fail, Layer 1 pass | IMMEDIATE alert | 🔴 P0 |
| TLS expiration | < 14 days | Notify | 🟡 P1 |
| TLS expiration | < 7 days | Notify + remind | 🔴 P0 |
| SSH fail spikes | > 10 failures/min | Investigate | 🟡 P1 |
| Service down | Caddy/nginx/Docker inactive | Notify, attempt restart | 🔴 P0 |
| S3 sync failure | Upload fails | Abort (memory consolidate) | 🔴 P0 |
| Standby stale | No sync > 30 min | Notify | 🟡 P1 |
5.2 Monitoring Stack
- Per-server:
prometheus-node-exporter(all boxes) - Dashboard: Prometheus → Grafana ()
- Synthetic checks: Uptime Kuma (on docker box, [IP-REDACTED])
- Hermes watchdog:
service-health-check.sh(every 5 min, Primary Host) - Standby watcher:
home-router-watchdog.sh(every 5 min, Primary Host) - Apex mail:
apex-mail-watchdog.sh(every 5 min, Primary Host) - Alert delivery: Telegram (via Hermes)
6. Restore Runbooks (Per Server)
| Attribute | Detail |
|---|---|
| RTO target | 5 minutes (via failover to Standby Host) |
| RPO target | 15 minutes (Hermes state), 24h (Docker data) |
| Actual RTO | ~3 minutes (corrected watchdog) |
| Actual RPO | Varies by data type (see below) |
Restore steps:
# Option A: Failover to standby (fastest — < 5 min)
# Standby Host detects Primary Host down → auto-takeover (Section 4.1)
# Option B: Full restore on replacement box
# Step 1 — Provision RS 2000 G12 on Primary Provider
# Step 2 — Apply base standard (Section 2.2)
# Step 3 — Install Hermes
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
# Step 4 — Restore Hermes state from latest S3:
aws s3 cp s3://hermes-vps-backups/live/state.db /root/.hermes/state.db \
--endpoint-url https://[S3-STORAGE-DOMAIN]
aws s3 cp s3://hermes-vps-backups/live/config.yaml /root/.hermes/config.yaml \
--endpoint-url https://[S3-STORAGE-DOMAIN]
# Step 5 — Restore .env from backup:
aws s3 cp s3://hermes-vps-backups/live/.env /root/.hermes/.env \
--endpoint-url https://[S3-STORAGE-DOMAIN]
chmod 600 /root/.hermes/.env
# Step 6 — Restore Docker stacks:
for dir in /root/docker/*/; do
(cd "dir" && docker compose up -d)
done
# Step 7 — Restore Caddy config:
aws s3 cp s3://hermes-vps-backups/live/config/Caddyfile /etc/caddy/Caddyfile
systemctl restart caddy
# Step 8 — Start Hermes gateway:
hermes gateway run --replace
# Step 9 — Verify:
curl -s https://[OPS-PORTAL-DOMAIN]/[GENERIC-HEALTH-ENDPOINT]
curl -s https://[DOCUMENT-SIGNING-DOMAIN]
# Check Telegram responds to /ping
6.2 [AI-SERVICE-DOMAIN] (Secondary Provider CPX41) — 🔴 Critical
| Attribute | Detail |
|---|---|
| RTO target | 2 hours |
| RPO target | 24 hours |
| Current disk | 🔴 92% — must clean before migration |
⚠️ Pre-reboot checklist:
# Verify at least one alternative model provider works
curl https://[AI-ADMIN-DOMAIN]/[GENERIC-LLM-ENDPOINT] \
-H "Authorization: Bearer (cat ~/.hermes/.env | grep ADMIN_AI | cut -d= -f2)" \
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"ping"}]}'
# Ensure local Ollama on Primary Host is running
curl -s http://[IP-REDACTED]:11434/[GENERIC-OLLAMA-ENDPOINT] || systemctl start ollama
# Notify all Hermes users ([CEO], [Operations Manager], [Developer])
Restore steps:
# Step 1 — Provision replacement (CPX41 or Primary Provider equivalent)
# Step 2 — Apply base standard (Section 2.2)
# Step 3 — Deploy Docker stacks:
cd /opt/litellm && docker compose up -d
cd /opt/openwebui && docker compose up -d
cd /opt/qdrant && docker compose up -d
# Step 4 — Pull models:
ollama pull llama3.2:3b
# Step 5 — Configure DNS: [AI-ADMIN-DOMAIN] → new IP
# Step 6 — Update all Hermes config.yaml files with new base_url
# Step 7 — Verify model access for all profiles
6.3 Legacy Web Host (Secondary Provider CPX21, Server Management Panel) — 🟡 Important
| Attribute | Detail |
|---|---|
| RTO target | 4 hours |
| RPO target | 24 hours |
| Backup method | Server Management Panel daily backup + manual DB |
Restore steps:
# Step 1 — Provision CPX21 on Secondary Provider
# Step 2 — Connect to Server Management Panel panel, link server
# Step 3 — Server Management Panel provisions nginx, MariaDB, PHP
# Step 4 — Restore WordPress from Server Management Panel backup → verify apex site loads
# Step 5 — Deploy company-prefix-infra SSH key in Server Management Panel → SSH Access
# Step 6 — Whitelist Primary Host IP ([IP-REDACTED]) in Server Management Panel firewall
# Step 7 — Reconfigure Postfix relay:
postconf -e "relayhost = [MAIL-SERVER-DOMAIN]:2525"
postconf -e "smtp_sasl_auth_enable = yes"
echo "[MAIL-SERVER-DOMAIN]:2525 [EMAIL-REDACTED]:[CREDENTIAL-REDACTED]" > /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd && systemctl restart postfix
# Step 8 — Flush postfix queue:
postqueue -f
6.4 UNMS (Secondary Provider CPX21) — 🟡 Important
| Attribute | Detail |
|---|---|
| RTO target | 4 hours |
| RPO target | 24 hours |
| Containers | 9 (Docker compose stack) |
Restore steps:
# Step 1 — Provision CPX21 → Docker
# Step 2 — Restore Postgres data volumes from S3
# Step 3 — Deploy docker-compose.yml
cd /opt/unms && docker compose up -d
# Step 4 — Verify UCRM + UNMS web login
# Step 5 — Re-register tower routers via UNMS
6.5 Hudu (Secondary Provider CPX21) — 🟡 Important
| Attribute | Detail |
|---|---|
| RTO target | 4 hours |
| RPO target | 24 hours |
| Containers | 5 (Docker compose) |
| Auto-backup | Hudu built-in S3 backup |
Restore steps:
# Step 1 — Provision CPX21 → Docker
# Step 2 — Deploy Hudu Docker stack
# Step 3 — Restore Postgres from last backup
# Step 4 — Verify web login + recent documents accessible
6.6 UniFi (Secondary Provider CPX21) — 🟡 Important
| Attribute | Detail |
|---|---|
| RTO target | 4 hours |
| RPO target | 24 hours |
| Type | Native install (not Docker) |
Restore steps:
# Step 1 — Provision CPX21
# Step 2 — Install UniFi Controller (native)
# Step 3 — Restore from UniFi backup file
# Step 4 — Adopt APs and switches
6.7 n8n (Secondary Provider CPX11) — 🟢 Standard
| Attribute | Detail |
|---|---|
| RTO target | 8 hours |
| Containers | 2 (n8n + postgres) |
Restore:
# Provision → Docker → restore postgres volume
cd /opt/n8n && docker compose up -d
6.8 docker box (Secondary Provider CPX11) — 🟢 Standard
| Attribute | Detail |
|---|---|
| RTO target | 8 hours |
| Containers | 8 (NPM, RustDesk, Uptime Kuma, Server Management Panel) |
Restore:
# Provision → Docker → restore compose stacks + volumes
6.9 fleettracker360 (Secondary Provider CPX11) — 🟢 Standard
| Attribute | Detail |
|---|---|
| RTO target | 8 hours |
| Type | Apache + MariaDB (LAMP) |
Restore:
# Provision → Apache + MariaDB → restore web root + DB
6.10 developer-vps (Secondary Provider CPX21) — 🟢 Standard
| Attribute | Detail |
|---|---|
| RTO target | Coordinate with [Developer] |
| Type | Independent Hermes + local LLM |
⚠️ SHADOW IT — needs documentation.
Current known config:
- Hermes gateway with admin-ai model access
- Llama 3.2 1B local fallback (systemd: llama-server)
- Cron: hermes-heartbeat.sh every minute
- Minimal services, no Docker
Actions needed:
- Document [Developer]'s full Hermes config.yaml and .env
- Backup to S3 under s3://hermes-vps-backups/developer-vps/
- Add [Developer]'s config to DR plan once documented
6.11 Standby Host (Secondary Provider CPX21) — 🔴 Standby
| Attribute | Detail |
|---|---|
| RTO target | 2 minutes |
| RPO target | 15 minutes |
| Type | Warm standby — gateway dormant |
Activation:
# Automatic: watchdog detects Primary Host down → S3 sync → start gateway
# Manual:
ssh [USER-REDACTED]@[IP-REDACTED]
sudo -i
hermes gateway run --replace
7. Testing & Validation
7.1 Restore Testing Schedule
| Test | Frequency | Who |
|---|---|---|
| Verify backup job completion (all servers) | Daily | Automated alert |
| Check backup age per server | Daily | Automated |
| Restore one database dump to test environment | Monthly | Network Team |
| Restore one full application stack | Quarterly | Network Team |
| Full DR simulation — Primary Host failover to Standby Host | Quarterly | Network Team |
| S3 credential restore/access test | Quarterly | Network Team |
| Document restore test results (date, tester, duration, findings) | Per test | Network Team |
7.2 Validation Criteria
Each restore test must produce:
- Date + tester name
- Server/application tested
- Backup artifact used (path + timestamp)
- Restore duration (start → fully operational)
- Data validation: check a known record exists (specific row, file, or document)
- Issues encountered
- Remediation ticket (if needed)
7.3 Quarterly DR Simulation
Scenario: "Primary Host is completely gone. Primary Provider account inaccessible. Standby Host has been active for 4 hours. Now Primary Provider is back — fail back."
Steps to validate:
- Simulate Primary Provider provider outage (block Primary Host IP)
- Verify Standby Host takes over within 5 minutes
- Send a Telegram message through standby
- Verify all cron jobs run on standby
- Simulate Primary Provider recovery
- Execute manual failback per Section 4.4
- Verify no data loss, no split-brain
8. Security
8.1 Current Security Posture
| Control | Status | Notes |
|---|---|---|
| SSH keys only | ✅ Done | Password auth disabled |
| Root SSH disabled | ⚠️ Partial | Some servers still allow root |
| UFW host firewall | ✅ All | Standard on all boxes |
| fail2ban | ✅ All | Standard on all boxes |
| S3 versioning | ✅ All buckets | Enabled |
| S3 Object Lock | ❌ Not done | Needs implementation |
| S3 write-only IAM | ❌ Not done | Currently using admin creds |
| Backup encryption | ❌ Not done | Server-side only; no client-side |
| Secrets escrow | 🟡 Partial | .env + Hudu — not all secrets documented |
| TLS everywhere | ✅ All public endpoints | Caddy auto-certs |
8.3 Break-Glass Access
If company-prefix-infra key is lost or revoked:
- Secondary Provider servers: Use Secondary Provider console → Rescue mode → mount filesystem → redeploy key
- Primary Provider servers: Use SCP web console → VNC → log in with root password → redeploy key
- Root passwords: Stored in ~/.hermes/.env (chmod 600) and Hudu