docs: add legal storage policy, DocuSeal deploy template, and app4 migration plan
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# app4 Scoping and Migration Plan
|
||||
|
||||
**Owner:** IT Pro Partner (Germaine Brown)
|
||||
**Created:** 2026-08-15
|
||||
**Status:** Draft (for review)
|
||||
**Objective:** Move every customer-facing app off Core onto a new `app4` host. Core becomes the Hermes AI assistant home only, with no customer-facing apps long term.
|
||||
|
||||
---
|
||||
|
||||
## 1. End State
|
||||
|
||||
| Host | Role |
|
||||
| --- | --- |
|
||||
| **Core** (RS 2000, 152.53.192.33) | Hermes + its direct dependencies + internal monitoring + Caddy for Core-local routes only |
|
||||
| **app4** (new, netcup) | All customer-facing apps, their databases, and all customer-facing Caddy routes |
|
||||
|
||||
Core keeps: browserless, camofox-browser, Super Search + SearXNG, the Prometheus/Telegraf/Grafana monitoring stack, mikrotik-exporter, Caddy itself, and core.itpropartner.com. Everything else moves.
|
||||
|
||||
---
|
||||
|
||||
## 2. Classification
|
||||
|
||||
### 2.1 STAYS ON CORE (Hermes and its direct dependencies)
|
||||
|
||||
| Item | Type | Current | Rationale |
|
||||
| --- | --- | --- | --- |
|
||||
| Caddy reverse proxy | systemd (80/443) | Core | Edge proxy; customer site blocks removed after cutover, `default_bind 152.53.192.33` retained |
|
||||
| browserless | Docker (:3000) | Core | Hermes headless browser dependency |
|
||||
| camofox-browser | Docker (:9377) | Core | Hermes stealth browser dependency |
|
||||
| SearXNG | Docker (127.0.0.1:8888) | Core | Super Search search backend |
|
||||
| Super Search MCP | systemd (:8899) | Core | Hermes `web_search` / `web_extract` MCP |
|
||||
| Prometheus | Docker | Core | Internal fleet monitoring (scrapes node_exporter) |
|
||||
| Telegraf | Docker | Core | Internal metrics collection |
|
||||
| Grafana | Docker | Core | Internal monitoring dashboards |
|
||||
| mikrotik-exporter | Docker (127.0.0.1:9436) | Core | MikroTik router metrics for Prometheus |
|
||||
| core.itpropartner.com | Caddy site | Core | Hermes / Core admin endpoint |
|
||||
|
||||
### 2.2 MOVES TO APP4 (customer-facing apps and routes)
|
||||
|
||||
| Item | Type | Current | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| DocuSeal | Docker (127.0.0.1:8091->3000) | Core | e-sign platform; SQLite (bind-mounted ./data) + internal Redis/Sidekiq |
|
||||
| TimeTrex | Docker (127.0.0.1:8085) | Core | Time tracking; Postgres backed |
|
||||
| microbin | Docker (127.0.0.1:8260) | Core | Paste bin; lightweight, low blast radius |
|
||||
| Uptime Kuma | Docker (:3001) | Core | Public status monitor |
|
||||
| Ops Portal backend | systemd / uvicorn (:8090) | Core | FastAPI; SQLite (ops.db) |
|
||||
| Postgres | host service (:5432) | Core | Shared; customer schemas move to app4. Core keeps a minimal instance only if a STAYS service still needs it, else decommission |
|
||||
| Redis | host service (:6379) | Core | Shared cache; enumerate consumers in Phase 0 |
|
||||
| sign.itpropartner.com | Caddy site | Core | DocuSeal frontend |
|
||||
| ops.itpropartner.com | Caddy site | Core | Ops Portal frontend |
|
||||
| uptimekuma.itpropartner.com | Caddy site | Core | Uptime Kuma frontend |
|
||||
| my.itpropartner.com | Caddy site | Core | Customer hub |
|
||||
| status.itpropartner.com | Caddy site | Core | Public status page |
|
||||
| auth.itpropartner.com | Caddy site | Core | Centralized auth |
|
||||
| voice.itpropartner.com | Caddy site | Core | Voice agent |
|
||||
| voice-open.itpropartner.com | Caddy site | Core | Voice agent (open) |
|
||||
| *.iamgmb.com | Caddy sites | Core | Customer sites |
|
||||
| *.intelsight.io | Caddy sites | Core | Customer sites |
|
||||
| *.fleettracker360.com | Caddy sites | Core | Customer sites |
|
||||
| *.debtrecoveryexperts.com | Caddy sites | Core | Customer sites |
|
||||
|
||||
**Postgres / Redis split note:** both are shared instances today. They move per-app, not wholesale. Customer databases and caches are provisioned fresh on app4 and populated from dumps. Core keeps a Postgres/Redis instance only if a STAYS service (none currently identified) depends on it, otherwise they are decommissioned on Core after cutover.
|
||||
|
||||
**Inventory caveat (resolve in Phase 0):** the architecture plan (`server-architecture-plan` skill) records DocuSeal and SearXNG as having moved off Core in 2024/Aug 2026. This plan treats the supplied Core inventory as authoritative and classifies both on Core. Phase 0 must reconcile with `docker ps` and the live Caddyfile before any move.
|
||||
|
||||
---
|
||||
|
||||
## 3. app4 Sizing Recommendation
|
||||
|
||||
**Recommendation: netcup RS 4000 G12 (12 vCPU / 32 GB DDR5 ECC / 1 TB NVMe), ~$44/mo, Manassas VA.**
|
||||
|
||||
Justification:
|
||||
|
||||
- Matches the ITPP standard app tier. app1, app2, and app3 are all RS 4000 G12. Consistency simplifies provisioning, monitoring, DR, and cost accounting.
|
||||
- The moved workload is app-tier, not hub-tier. app4 will host a dedicated Postgres + Redis, DocuSeal (Ruby on Rails), TimeTrex (PHP), the Ops Portal backend (FastAPI/uvicorn), the voice stack, and a dozen-plus customer Caddy sites. That is comparable to app1, which already runs an RS 4000.
|
||||
- RS 2000 (8 vCPU / 16 GB) is too small. Core today runs everything on an RS 2000 and is being relieved precisely because it is overloaded. Squeezing the entire customer tier back onto a single RS 2000 would recreate the problem.
|
||||
- 1 TB NVMe provides headroom for Postgres growth, Docker volumes, voice/audio assets, and backup retention without immediate pressure.
|
||||
- Fault isolation: a customer-app outage on app4 no longer competes with Hermes on Core.
|
||||
|
||||
**Upsize trigger:** if the voice stack or customer site count grows materially, or Postgres usage exceeds ~40% of 32 GB, re-evaluate for RS 8000 (16 vCPU / 64 GB / 2 TB). Start at RS 4000.
|
||||
|
||||
---
|
||||
|
||||
## 4. Phased Migration Plan
|
||||
|
||||
### Phase 0: Inventory and Recon (Core, read-only)
|
||||
|
||||
- Confirm live inventory: `docker ps`, `docker volume ls`, `ss -tlnp`, `systemctl list-units --type=service`.
|
||||
- Extract every moving site block from `/etc/caddy/Caddyfile` (site name, backend, TLS, redirects).
|
||||
- Catalog data locations: Docker named volumes + bind mounts for DocuSeal, TimeTrex, microbin, Uptime Kuma, Ops Portal.
|
||||
- Enumerate Postgres databases (`psql -l`) and map each to its app; enumerate Redis keyspace consumers.
|
||||
- Record env files and secret references (Hudu / Vaultwarden) for each moving app.
|
||||
- Record cron entries that touch the moving apps or their backups.
|
||||
- Verify DNS authority per domain with `dig NS <domain>` (itpropartner.com is SiteGround manual; fleettracker360.com and voipsimplicity.com are Cloudflare; check iamgmb.com, intelsight.io, debtrecoveryexperts.com individually).
|
||||
- Reconcile the DocuSeal / SearXNG inventory caveat from section 2.
|
||||
- Produce the runbook: per-app data migration command, per-domain DNS record, and an acceptance checklist.
|
||||
|
||||
### Phase 1: Provision app4 + Monitoring First
|
||||
|
||||
- Order RS 4000 G12 per `server-provisioning-standard`: Debian 13, ippadmin user + sudo, itpp-infra SSH key, UFW (open 22, 80, 443), Fail2Ban, unattended-upgrades, Docker + compose plugin, Python, AWS CLI with the cron PATH fix, node_exporter on :9100, Tailscale.
|
||||
- Install Caddy on app4 with `default_bind <app4-ipv4>` to avoid the Tailscale port 443 conflict.
|
||||
- Enroll app4 in root-essentials-backup; run one manual backup and verify it lands in S3 (do not rely on the cron entry alone).
|
||||
- Add app4:9100 to Core Prometheus targets and Grafana dashboards. Observability exists before any app moves.
|
||||
- Do not move any customer app in this phase.
|
||||
|
||||
### Phase 2: Low-Risk Apps (prove the pattern)
|
||||
|
||||
- Move microbin and Uptime Kuma first. Small, self-contained, low blast radius.
|
||||
- microbin: rsync volume, start on app4, verify via `curl --resolve`.
|
||||
- Uptime Kuma: move after microbin; its monitors continue running and its own cutover is the first DNS flip of the whole project.
|
||||
- Validate the rsync + healthcheck + rollback playbook on these two before touching customer apps.
|
||||
- Confirm app4 S3 backups for these two are working.
|
||||
|
||||
### Phase 3: Customer Apps + Data
|
||||
|
||||
- Foundation first: provision Postgres and Redis on app4 (least-privilege, internal-only networks).
|
||||
- Move Ops Portal backend (:8090), then DocuSeal, then TimeTrex.
|
||||
- Bring each up on app4 on internal ports and test side-by-side with Core using `curl --resolve <domain>:443:<app4-ip>`.
|
||||
- Move the voice stack (voice.itpropartner.com, voice-open.itpropartner.com), including any audio assets and external webhook/Twilio endpoint updates.
|
||||
- Move static customer sites (*.iamgmb.com, *.intelsight.io, *.fleettracker360.com, *.debtrecoveryexperts.com) by rsyncing web roots.
|
||||
- Verify Postgres row counts and Redis state after each app move (see section 5).
|
||||
|
||||
### Phase 4: DNS Cutover + Decommission on Core
|
||||
|
||||
- Lower TTL on all moving records to 300 (or 60) at least 24h before cutover.
|
||||
- Flip DNS per domain, one at a time, low-traffic first, verifying each before the next.
|
||||
- Keep critical Core Caddy blocks as a temporary 301 redirect to app4 during a 24 to 72h soak window; remove after verification.
|
||||
- After soak: remove customer site blocks from Core Caddyfile (use targeted edits + caddy-audit hook, never rewrite the whole file), stop and remove moved containers on Core, retain volumes and images for 30 days as rollback.
|
||||
- Decommission customer schemas in Core Postgres/Redis (or the whole instance if unused by Core).
|
||||
- Update Prometheus targets, Uptime Kuma, docs, `app-inventory.csv`, and the recovery manual.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Migration Steps
|
||||
|
||||
Docker volumes (rsync, app stopped):
|
||||
|
||||
```bash
|
||||
# On Core, stop the app, then delta-sync the volume data to app4
|
||||
docker compose -f /root/docker/<app>/docker-compose.yml stop
|
||||
rsync -az --delete \
|
||||
/var/lib/docker/volumes/<volume>/_data/ \
|
||||
ippadmin@app4:/var/lib/docker/volumes/<volume>/_data/
|
||||
# On app4
|
||||
docker compose -f /root/docker/<app>/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
SQLite (online-safe backup, never `cp` a live DB):
|
||||
|
||||
```bash
|
||||
sqlite3 /path/app.db ".backup '/tmp/app-backup.db'"
|
||||
rsync -az /tmp/app-backup.db ippadmin@app4:/path/app.db
|
||||
```
|
||||
|
||||
Postgres (per-database custom-format dump):
|
||||
|
||||
```bash
|
||||
# On Core
|
||||
pg_dump -Fc -d <dbname> -f /tmp/<dbname>.dump
|
||||
rsync -az /tmp/<dbname>.dump ippadmin@app4:/tmp/
|
||||
# On app4
|
||||
pg_restore -d <dbname> /tmp/<dbname>.dump
|
||||
```
|
||||
|
||||
For a full-instance move, use `pg_dumpall` instead of per-database dumps.
|
||||
|
||||
Redis:
|
||||
|
||||
```bash
|
||||
# If cache only: rebuild empty on app4. If state matters:
|
||||
redis-cli BGSAVE # then rsync dump.rdb with Redis stopped, or configure replication during cutover
|
||||
```
|
||||
|
||||
Post-migration verification (mandatory, per the Hudu lesson):
|
||||
|
||||
- Compare Postgres row counts for every major table between Core and app4, not just a spot check.
|
||||
- Compare Docker volume sizes and file counts after rsync.
|
||||
- Hit each domain through app4 with `curl --resolve` and compare responses against Core side-by-side.
|
||||
- Do not declare a migration done on container health alone.
|
||||
|
||||
---
|
||||
|
||||
## 6. Caddy / DNS Change Checklist
|
||||
|
||||
- [ ] Verify authoritative nameservers per domain (`dig NS`). itpropartner.com is SiteGround manual; do not create records via Cloudflare for it.
|
||||
- [ ] Lower TTL to 300 (or 60) on every moving record at least 24h before cutover.
|
||||
- [ ] Pre-write the app4 Caddyfile with all moving site blocks; `caddy validate` it.
|
||||
- [ ] Pre-issue TLS certs on app4 (on-demand or staging) before DNS flip.
|
||||
- [ ] Open UFW 80/443 on app4 (netcup blocks them by default) and verify from an external network.
|
||||
- [ ] Set `default_bind <app4-ipv4>` in app4 Caddy global block to avoid Tailscale :443 conflict.
|
||||
- [ ] Flip each A/AAAA record to app4 in the domain's authoritative panel (SiteGround manual or Cloudflare API, per domain).
|
||||
- [ ] Verify propagation: `dig +short @1.1.1.1 <domain>`.
|
||||
- [ ] Verify service and cert on app4: `curl -sI https://<domain>`.
|
||||
- [ ] Apply the caddy-audit hook before any Core Caddyfile edit; use targeted edits or `patch`, never a full rewrite.
|
||||
- [ ] After soak, remove stale Core blocks and reload Caddy.
|
||||
|
||||
---
|
||||
|
||||
## 7. DR / Backup Implications
|
||||
|
||||
- Repoint backup scripts and cron from Core to app4 for every moved app (docker-volume-sync, any per-app backup jobs, root-essentials-backup).
|
||||
- app4 gets its own S3 backup path under the existing Wasabi bucket, keyed by hostname, with least-privilege credentials.
|
||||
- The daily Hermes backup on Core stops backing up customer app volumes once they move; confirm the app4 cron owns them before removing Core entries.
|
||||
- Add app4 to the DR plan (`server-dr-plans.md`) and the recovery manual; document what to restore in what order.
|
||||
- Decide standby scope: app1-bu is a warm standby for Core, not for customer apps. app4 relies on S3 backups unless a customer-app standby is separately approved.
|
||||
- Use `/opt/awscli-venv/bin/aws` (full path) in every app4 backup script to avoid the silent cron PATH failure.
|
||||
- After the first real backup on app4, perform a test restore of one app to prove the backups work, not just the cron entry.
|
||||
|
||||
---
|
||||
|
||||
## 8. Risks and Rollback
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| DNS authority confusion (SiteGround vs Cloudflare) | Silent no-op record changes, outage | Verify `dig NS` per domain first; route changes through the correct panel |
|
||||
| Shared Postgres/Redis partial migration | Core app breaks mid-move | Move per-app dumps, verify row counts, keep Core DB intact until cutover |
|
||||
| Live-file copy corruption (cp on running SQLite/Postgres) | Data loss | Always stop app or use `.backup` / `pg_dump` |
|
||||
| Voice stack hidden dependencies (Twilio webhooks, TTS/STT endpoints) | Voice breaks after cutover | Enumerate external webhooks in Phase 0, update endpoints before DNS flip |
|
||||
| TLS issuance failure on app4 | Site unreachable | Pre-issue certs, confirm UFW 80/443 open, test externally |
|
||||
| Caddyfile fragility (whole-file rewrite drops sites) | Silent domain loss | Targeted edits + caddy-audit hook, never full rewrite |
|
||||
| Backup silently failing on app4 (aws not in PATH) | No restorable backup | Full-path AWS, manual test restore after first backup |
|
||||
|
||||
### Rollback
|
||||
|
||||
- Before each phase, snapshot: current DNS records, Core Caddyfile, and Core Docker state.
|
||||
- Phase 2/3 rollback: stop the app on app4, flip DNS back to Core, restart the Core container. Core volumes are untouched and the app returns to its pre-move state.
|
||||
- Phase 4 rollback: with low TTL, flipping the A record back to Core propagates in minutes; Core Caddy blocks are retained during the soak window for exactly this purpose.
|
||||
- Data rollback: Core volumes and images are retained for 30 days after cutover, so any container can be restarted on Core instantly.
|
||||
- After 30 days: restore from app4 S3 backups (this is why a test restore is mandatory in Phase 1).
|
||||
@@ -0,0 +1,33 @@
|
||||
# DocuSeal environment template. Replace every <...> placeholder.
|
||||
# Never commit real values. chmod 600 after filling.
|
||||
|
||||
# Public base URL
|
||||
HOST=https://sign.<entity>.example.com
|
||||
FORCE_SSL=true
|
||||
|
||||
# Encryption root. Generate with: openssl rand -hex 64
|
||||
# NEVER rotate on a live instance (ActiveRecord encryption root).
|
||||
SECRET_KEY_BASE=<openssl rand -hex 64>
|
||||
|
||||
# SMTP (netcup smarthost: only port 2525 works; 25/465/587 are blocked)
|
||||
SMTP_ADDRESS=<smtp-relay-hostname>
|
||||
SMTP_PORT=2525
|
||||
SMTP_USERNAME=<noreply@entity-domain>
|
||||
SMTP_PASSWORD=<smtp-relay-password>
|
||||
SMTP_AUTHENTICATION=login
|
||||
SMTP_ENABLE_STARTTLS=true
|
||||
SMTP_DOMAIN=<entity-domain>
|
||||
SMTP_SSL_VERIFY=true
|
||||
|
||||
# SMTP_FROM is inert: DocuSeal hardcodes the mailer from as
|
||||
# "DocuSeal <info@docuseal.com>". Do not expect it to change the sender.
|
||||
|
||||
# S3/Wasabi attachments. Uncomment and fill once the <entity>-legal bucket exists.
|
||||
# Presence of S3_ATTACHMENTS_BUCKET activates S3 storage. Left unset, DocuSeal
|
||||
# uses local disk storage under ./data/attachments.
|
||||
#S3_ATTACHMENTS_BUCKET=<entity>-legal
|
||||
#S3_ENDPOINT=s3.us-east-1.wasabisys.com
|
||||
#AWS_ACCESS_KEY_ID=<wasabi-access-key-id>
|
||||
#AWS_SECRET_ACCESS_KEY=<wasabi-secret-access-key>
|
||||
#AWS_REGION=us-east-1
|
||||
#ACTIVE_STORAGE_PUBLIC=false
|
||||
@@ -0,0 +1,176 @@
|
||||
# DocuSeal Deployment Template and Runbook
|
||||
|
||||
Spin up one self-hosted DocuSeal instance per legal entity. Reference live deployment: Core `sign.itpropartner.com` (container `docuseal`, image `docuseal/docuseal:latest`, bound `127.0.0.1:8091:3000`, volume `./data:/data`, `env_file .env`).
|
||||
|
||||
## Hard isolation rule
|
||||
|
||||
One instance per legal entity. Do NOT multi-tenant a single DocuSeal across entities. Distinct legal entities require hard isolation: signatures, templates, and audit data must never mix. DocuSeal has a multitenant mode but it is not approved for cross-entity use here. Deploy a separate container, data volume, subdomain, and S3 bucket for each entity.
|
||||
|
||||
First target: Model Ortho. Future entities follow the same steps with a new slug, port, subdomain, and bucket.
|
||||
|
||||
## Port remap
|
||||
|
||||
Core host port 3000 is occupied by browserless. Bind each instance to a unique loopback port, mapping to container port 3000:
|
||||
|
||||
- Core `sign.itpropartner.com`: `127.0.0.1:8091`
|
||||
- Model Ortho: next free port, e.g. `127.0.0.1:8092`
|
||||
|
||||
List loopback listeners and pick an unused port:
|
||||
|
||||
```
|
||||
ss -tln | grep 127.0.0.1
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Compose on the host (Core today, app4 in future)
|
||||
- DNS record for the sign subdomain
|
||||
- Wasabi S3 bucket named `<entity>-legal` (e.g. `modelortho-legal`) for attachments
|
||||
- SMTP relay credentials (netcup smarthost: only port 2525 works; 25/465/587 are blocked)
|
||||
- Vaultwarden (`bw` CLI) for credential storage
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create the instance directory:
|
||||
|
||||
```
|
||||
mkdir -p /root/docker/docuseal-<entity> && cd /root/docker/docuseal-<entity>
|
||||
```
|
||||
|
||||
2. Copy the templates and rename:
|
||||
|
||||
```
|
||||
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/docker-compose.yml.template docker-compose.yml
|
||||
cp /root/projects/itpp-infrastructure/docs/infrastructure/docuseal/.env.example .env
|
||||
chmod 600 .env
|
||||
```
|
||||
|
||||
3. Edit `docker-compose.yml`. Replace `__ENTITY_SLUG__` with the entity slug and `__HOST_PORT__` with the unique loopback port.
|
||||
|
||||
4. Fill `.env`. Set HOST, the SMTP_* vars, SECRET_KEY_BASE, and (once the bucket exists) the S3_/AWS_ vars. Generate the secret:
|
||||
|
||||
```
|
||||
openssl rand -hex 64
|
||||
```
|
||||
|
||||
Record it in Vaultwarden (step 10). NEVER rotate it once the instance is live.
|
||||
|
||||
5. Start the container:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
6. Verify boot. A clean instance returns HTTP 302 to /setup:
|
||||
|
||||
```
|
||||
curl -sI http://127.0.0.1:<port> | head -1
|
||||
```
|
||||
|
||||
A 500 means a stale encrypted DB under a different SECRET_KEY_BASE. Move data/ aside and boot clean:
|
||||
|
||||
```
|
||||
mv data data.old-$(date +%s) && docker compose up -d
|
||||
```
|
||||
|
||||
7. Complete setup at `https://<host>/setup` (owner email and password). If the submit button does not advance, run in the browser console:
|
||||
|
||||
```
|
||||
document.querySelector('form').requestSubmit()
|
||||
```
|
||||
|
||||
8. Add the Caddy block for the sign subdomain:
|
||||
|
||||
```
|
||||
<host> {
|
||||
reverse_proxy 127.0.0.1:<port>
|
||||
}
|
||||
```
|
||||
|
||||
Then validate and reload:
|
||||
|
||||
```
|
||||
caddy validate && systemctl reload caddy
|
||||
```
|
||||
|
||||
9. Point DNS (A record to origin; grey-cloud for `*.iamgmb.com`). Verify:
|
||||
|
||||
```
|
||||
curl -sI https://<host> | head -1
|
||||
```
|
||||
|
||||
Expected: HTTP/2 200.
|
||||
|
||||
10. Store admin credentials and the secret in Vaultwarden:
|
||||
|
||||
```
|
||||
bw create item '{"type":1,"name":"DocuSeal <entity> admin","login":{"username":"owner@<entity>.com","password":"<admin password>","uris":[{"uri":"https://<host>"}]},"notes":"SECRET_KEY_BASE never rotate"}'
|
||||
bw create item '{"type":2,"name":"DocuSeal <entity> SECRET_KEY_BASE","notes":"<secret key base>\nNEVER rotate on a live instance. ActiveRecord encryption root."}'
|
||||
```
|
||||
|
||||
11. Wire backup. Copy the Core script pattern (see Backup section) to `/root/.hermes/scripts/docuseal-<entity>-backup.sh` and add a Hermes cron job (daily).
|
||||
|
||||
## SMTP env vars (exact names, verified against running container)
|
||||
|
||||
- `SMTP_ADDRESS`: mail.itpropartner.com (or entity relay). NOT SMTP_HOST.
|
||||
- `SMTP_PORT`: 2525 (netcup smarthost; 25/465/587 blocked).
|
||||
- `SMTP_USERNAME`: noreply@<domain>. NOT SMTP_USER_NAME.
|
||||
- `SMTP_PASSWORD`: relay password.
|
||||
- `SMTP_AUTHENTICATION`: login (honored only when SMTP_PASSWORD present).
|
||||
- `SMTP_ENABLE_STARTTLS`: true.
|
||||
- `SMTP_DOMAIN`: <domain>.
|
||||
- `SMTP_SSL_VERIFY`: true (false = VERIFY_NONE).
|
||||
|
||||
SMTP_FROM is INERT. DocuSeal does not read it. The mailer default from is hardcoded as `DocuSeal <info@docuseal.com>` in `app/mailers/application_mailer.rb`. Branding the From address requires a fork, which conflicts with the AGPL rule below.
|
||||
|
||||
## S3/Wasabi attachment vars
|
||||
|
||||
S3 storage activates when S3_ATTACHMENTS_BUCKET is present. Exact vars read from `config/storage.yml`:
|
||||
|
||||
- `S3_ATTACHMENTS_BUCKET`: <entity>-legal (presence triggers S3 storage).
|
||||
- `S3_ENDPOINT`: s3.us-east-1.wasabisys.com (sets force_path_style true).
|
||||
- `AWS_ACCESS_KEY_ID`: Wasabi access key.
|
||||
- `AWS_SECRET_ACCESS_KEY`: Wasabi secret key.
|
||||
- `AWS_REGION`: us-east-1 (default).
|
||||
- `ACTIVE_STORAGE_PUBLIC`: false (optional).
|
||||
|
||||
Leave these unset to use local disk storage under `./data/attachments`.
|
||||
|
||||
## SECRET_KEY_BASE rule
|
||||
|
||||
SECRET_KEY_BASE is the encryption root for ActiveRecord encrypted columns in the SQLite DB. NEVER rotate it on a live instance. Rotating it breaks decryption (AEAD authentication tag verification failed, HTTP 500). Back it up with the instance and record it in Vaultwarden with a never-rotate note.
|
||||
|
||||
## Backup script pattern
|
||||
|
||||
Runs locally on the host (no SSH). Archive the data dir, compose file, and .env so a restore is fully self-contained. Upload to the entity legal/ops bucket:
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
if [ -f /opt/awscli-venv/bin/activate ]; then source /opt/awscli-venv/bin/activate; fi
|
||||
S3_BUCKET="s3://<entity>-legal/docuseal"
|
||||
S3_ENDPOINT="--endpoint-url https://s3.us-east-1.wasabisys.com"
|
||||
DOCUSEAL_DIR="/root/docker/docuseal-<entity>"
|
||||
TSTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
tar czf "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" -C "$DOCUSEAL_DIR" data docker-compose.yml .env
|
||||
aws s3 cp $S3_ENDPOINT "$WORKDIR/docuseal-backup-$TSTAMP.tar.gz" "$S3_BUCKET/docuseal-backup-$TSTAMP.tar.gz"
|
||||
```
|
||||
|
||||
Reference implementation: `/root/.hermes/scripts/docuseal-backup.sh`. Drive with a Hermes cron job.
|
||||
|
||||
## AGPL-3.0 constraint
|
||||
|
||||
DocuSeal is AGPL-3.0. Run it UNMODIFIED and integrate via API only. Do not fork or patch the source. Consequences: no branded From address on email (see SMTP_FROM above), and all automation goes through DocuSeal's API rather than code changes.
|
||||
|
||||
## docuseal:latest image note
|
||||
|
||||
Newer `docuseal:latest` bundles Redis and Sidekiq inside the single container. It may log a harmless memory overcommit warning (`vm.overcommit_memory`). This is cosmetic; the container runs fine. No separate Redis or Sidekiq services are needed.
|
||||
|
||||
## Teardown
|
||||
|
||||
1. Remove the Caddy block, then `caddy validate` and `systemctl reload caddy`.
|
||||
2. `docker compose down` (NOT `stop`: `restart: always` resurrects stopped containers on reboot; `down` removes the container while the bind-mounted data/ is preserved).
|
||||
3. Delete the DNS record.
|
||||
4. Never delete an old encrypted DB. Preserve as `data.old-<epoch>`.
|
||||
@@ -0,0 +1,22 @@
|
||||
# DocuSeal compose template. Replace __ENTITY_SLUG__ and __HOST_PORT__.
|
||||
# One instance per legal entity. Do not reuse a container_name or host port.
|
||||
# Newer docuseal:latest bundles Redis + Sidekiq internally, so no separate
|
||||
# redis/sidekiq services are needed. It may log a harmless memory overcommit
|
||||
# warning.
|
||||
services:
|
||||
docuseal:
|
||||
image: docuseal/docuseal:latest
|
||||
container_name: docuseal-__ENTITY_SLUG__
|
||||
restart: always
|
||||
ports:
|
||||
# 3000 inside the container. Host 3000 is browserless on Core.
|
||||
- "127.0.0.1:__HOST_PORT__:3000"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
env_file:
|
||||
- .env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
@@ -0,0 +1,157 @@
|
||||
# Legal Document and Customer Data Storage Policy
|
||||
|
||||
**Owner:** Germaine Brown, IT Pro Partner
|
||||
**Status:** Draft for review and approval
|
||||
**Effective date:** Pending approval
|
||||
**Supersedes:** none (first formal storage policy)
|
||||
**Source of truth:** `/root/itpp-backup-storage-recommendation.md` (2026-08-15)
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose and scope
|
||||
|
||||
This policy defines how IT Pro Partner stores, retains, protects, and disposes of long-term legal documents and customer data in Wasabi S3 object storage. It turns the SME storage recommendation into binding, actionable rules.
|
||||
|
||||
Scope covers all data under IT Pro Partner control across current and future legal entities (ITPP parent, TransitPin, Model Ortho) and applies to every bucket, prefix, IAM policy, and retention rule created after approval.
|
||||
|
||||
Out of scope: application databases in active production, the existing legacy backup layer (`hermes-vps-backups`, `itpropartner-*`, `mikrotik-ccr-backups`), and any on-premises file shares. Those keep operating until migrated per the action checklist.
|
||||
|
||||
## 2. Data classification
|
||||
|
||||
Every object stored under this policy is assigned exactly one class. The class determines bucket, retention, immutability, and encryption.
|
||||
|
||||
| Class | Examples | Bucket | Immutability | Encryption |
|
||||
|---|---|---|---|---|
|
||||
| Legal / signed documents | DocuSeal NDAs, MSAs, quotes, SOWs, completion certificates, embedded audit trail | `*-legal` | Compliance, locked ON | AES-256 + client-side |
|
||||
| Customer / tenant data | TransitPin tenant SQLite DBs, routes, drivers, children, registrations | `*-ops` (tenant prefix) | Object Lock governance on monthlies only | AES-256, SSE-C for EU PII |
|
||||
| Operational / IP | Hudu dumps, scripts, architecture docs, configs, Caddyfile, env | `*-ops` | Object Lock governance on monthly/yearly | AES-256, SSE-C for `configs/` and `env/` |
|
||||
| DR / full-server images | Hetzner standby sync, full tarballs | `*-ops/dr/` | Object Lock governance on latest full only | AES-256 |
|
||||
|
||||
## 3. Storage bucket organization
|
||||
|
||||
Primary split is **bucket-per-legal-entity**, not per-app and never per-tenant. Within each entity, two buckets are required: one operational (`-ops`) and one legal (`-legal`), because Wasabi Compliance mode and Object Lock are mutually exclusive per bucket. Legal records need bucket-wide WORM; operational backups need lifecycle-expirable objects. One bucket cannot serve both.
|
||||
|
||||
| Bucket | Entity | Purpose | Immutability | Region |
|
||||
|---|---|---|---|---|
|
||||
| `itpp-ops` | ITPP parent | Internal IP, Hudu dumps, scripts, configs, DR, server backups | Object Lock governance on monthly only | us-east-1 |
|
||||
| `itpp-legal` | ITPP parent | DocuSeal signed contracts + audit trail | Compliance, locked ON | us-east-1 |
|
||||
| `transitpin-ops` | TransitPin | Tenant DB + app data, per-tenant prefix | Object Lock governance on monthly only | eu-central-1 |
|
||||
| `transitpin-legal` | TransitPin | Client MSAs/SOWs, completion certificates | Compliance, locked ON | us-east-1 |
|
||||
| `modelortho-ops` | Model Ortho | Consulting records, client data | Object Lock governance | eu-central-1 if EU clients |
|
||||
| `modelortho-legal` | Model Ortho | Signed engagement letters | Compliance, locked ON | us-east-1 |
|
||||
|
||||
Prefix rules:
|
||||
|
||||
- Level 1 = data class or app: `app/<name>/`, `legal/`, `configs/`, `ip/`.
|
||||
- Level 2 = tenant (multi-tenant apps only): `app/transitpin/tenants/<tenant-id>/`.
|
||||
- Level 3 = retention tier: `daily/`, `weekly/`, `monthly/`, `archive/`.
|
||||
|
||||
Migration note: bucket names are immutable on Wasabi. Do not rename. Copy to the new entity bucket, then delete the source. Migrate high-value prefixes (legal, tenant data) first.
|
||||
|
||||
## 4. Retention schedule
|
||||
|
||||
Retention follows a GFS (grandfather-father-son) cadence.
|
||||
|
||||
| Record type | Retention window | Notes |
|
||||
|---|---|---|
|
||||
| Legal contracts (NDA, MSA, Quote, SOW) | Duration of contract + 7 years | Computed per contract end date |
|
||||
| Key / founding contracts | Permanent | Never expire or delete |
|
||||
| Completion certificates and audit trail | Life of record | 50+ years for insurance-class; use PDF/A |
|
||||
| IRS financial and tax records | 7 years | Payroll and tax exports go to `itpp-ops` |
|
||||
| Daily operational snapshots | 14 days | Applies to `daily/` prefixes |
|
||||
| Weekly operational snapshots | 8 weeks | Applies to `weekly/` prefixes |
|
||||
| Monthly operational snapshots | 13 months | Applies to `monthly/` prefixes |
|
||||
| Yearly configs / IP snapshots | 7 years | Applies to `configs/` and `ip/` |
|
||||
| DR full-server images | Last 3 fulls + 90-day window | `itpp-ops/dr/` |
|
||||
|
||||
## 5. Immutability rules
|
||||
|
||||
| Rule | Requirement |
|
||||
|---|---|
|
||||
| `-legal` buckets | Wasabi Compliance mode, locked ON. Bucket-wide WORM on every object. |
|
||||
| `-ops` buckets | Object Lock in governance mode on monthly (and yearly) snapshots only. |
|
||||
| `-ops` daily snapshots | No immutability. |
|
||||
| `-ops` bucket itself | Never apply Compliance lock. You keep paying for undeletable objects. |
|
||||
| Object Lock enablement | Must be enabled at bucket creation. Cannot be added to an existing bucket. |
|
||||
| Compliance lock unlock | Only Wasabi support can unlock once locked ON. Treat as irreversible. |
|
||||
|
||||
Rationale: governance mode on ops blocks ransomware and accidental deletion while still allowing deliberate correction. Compliance lock on legal is the WORM guarantee a contract dispute needs.
|
||||
|
||||
## 6. Encryption
|
||||
|
||||
| Item | Rule |
|
||||
|---|---|
|
||||
| Encryption at rest | Wasabi AES-256 automatic and free on every object. No action required. |
|
||||
| EU customer PII | Add SSE-C or client-side encryption before upload. |
|
||||
| Legal records | Add client-side encryption (belt and suspenders over default AES-256). |
|
||||
| `configs/` and `env/` prefixes | Add SSE-C (contains secrets). |
|
||||
| SSE-KMS | Not available on Wasabi. Do not attempt. Use SSE-C or client-side encryption. |
|
||||
|
||||
## 7. GDPR and EU data residency
|
||||
|
||||
| Rule | Requirement |
|
||||
|---|---|
|
||||
| EU customer PII storage | Must go to the eu-central-1 bucket (`s3.eu-central-1.wasabisys.com`). |
|
||||
| TransitPin child route data | Special category (Art. 9). Store in eu-central-1 only. Never in US region. |
|
||||
| US storage of EU personal data | Requires SCCs plus a Transfer Impact Assessment before transfer. |
|
||||
| Legal-bucket region | EU legal documents follow the contract entity's region, not the PII rule. |
|
||||
|
||||
No GDPR residency mandate exists, but transfers to the US are tightly regulated. Default is to keep EU PII in the EU region and avoid the transfer burden.
|
||||
|
||||
## 8. Access control
|
||||
|
||||
| Rule | Requirement |
|
||||
|---|---|
|
||||
| IAM policy scope | One IAM user and policy per legal entity. |
|
||||
| Policy structure | Two statement blocks: bucket-level and object-level. |
|
||||
| Cross-entity access | Denied by default. No shared credentials across entities. |
|
||||
| Divestiture | Hand over the entity's bucket plus its IAM user credentials only. |
|
||||
| Backup controller | Only the backup controller writes archive/legal tiers, never application servers. |
|
||||
|
||||
## 9. Backup vs archive separation
|
||||
|
||||
| Tier | Purpose | RPO | Retention | Immutability |
|
||||
|---|---|---|---|---|
|
||||
| Operational backup | Fast restore, short retention | 15 min live sync | 14 days daily | None, or governance on monthly rollup |
|
||||
| Long-term archive | Cold, ransomware-safe copy | Monthly rollup | 13 months + yearly 7 years | Object Lock governance/compliance |
|
||||
| Legal hold | Contract evidence | On signature | Contract life + 7 years | Compliance, locked ON |
|
||||
|
||||
Archive and legal tiers are never written directly by application servers. Only the backup controller copies into them.
|
||||
|
||||
## 10. Disposal and deletion
|
||||
|
||||
| Rule | Requirement |
|
||||
|---|---|
|
||||
| Legal records | Deletion is a deliberate, authorized event after retention lapses. Never lifecycle auto-expire. |
|
||||
| Operational daily/weekly | Lifecycle rules may expire objects past their window. |
|
||||
| Compliance-locked objects | Cannot be deleted until retention lapses. Plan storage cost accordingly. |
|
||||
| Deletion authorization | Owner (Germaine Brown) approval required before deleting any legal record. |
|
||||
| Deletion record | Log the deletion event (object key, date, reason, approver). |
|
||||
|
||||
## 11. Roles and responsibilities
|
||||
|
||||
| Role | Responsibilities |
|
||||
|---|---|
|
||||
| Owner (Germaine Brown) | Approves policy, approves legal-record deletions, approves new entities and buckets. |
|
||||
| Backup controller / admin | Creates buckets, enables versioning and Object Lock at creation, runs archive and lifecycle jobs. |
|
||||
| Application developers | Never write directly to archive or legal tiers. Export SQLite via `sqlite3 .backup`, never raw WAL sync. |
|
||||
| DPO / compliance (if retained) | Maintains SCCs and TIAs for US storage of EU data, reviews residency annually. |
|
||||
| Auditor | Annual review of bucket, IAM, and retention configuration against this policy. |
|
||||
|
||||
## 12. Action checklist
|
||||
|
||||
Complete in order. This is the immediate work to operationalize the policy.
|
||||
|
||||
1. Create the six buckets with versioning enabled, using the exact names and regions in section 3.
|
||||
2. Enable Object Lock at creation on every `-ops` bucket; enable and lock Compliance mode on every `-legal` bucket.
|
||||
3. Create one IAM user per legal entity with a two-statement policy scoped to its two buckets.
|
||||
4. Create the `transitpin-ops` bucket in eu-central-1 and route all TransitPin EU tenant PII there.
|
||||
5. Sign SCCs and complete a Transfer Impact Assessment for any remaining US-region storage of EU personal data.
|
||||
6. Point the DocuSeal signed-document pipeline at `itpp-legal/contracts/` as the first consumer of the legal bucket.
|
||||
7. Embed the DocuSeal audit trail inside the signed PDF before upload, and store PDFs as PDF/A.
|
||||
8. Add `archive-monthly.sh` to copy each app's latest monthly snapshot to the archive prefix with Object Lock.
|
||||
9. Add a lifecycle rule to expire `daily/` objects older than 14 days in the `-ops` buckets.
|
||||
10. Export payroll and tax records to `itpp-ops` with a 7-year monthly archive.
|
||||
11. Replicate each `-legal` bucket to a second Wasabi region via Object Replication.
|
||||
12. Migrate existing high-value prefixes (legal, tenant data) from the legacy buckets first; leave the rest until later.
|
||||
13. Schedule an annual review of buckets, IAM, and retention against this policy.
|
||||
Reference in New Issue
Block a user