docs: add legal storage policy, DocuSeal deploy template, and app4 migration plan

This commit is contained in:
root
2026-08-15 15:45:36 -04:00
parent 4d43623db6
commit 26b985f6a4
5 changed files with 619 additions and 0 deletions
+33
View File
@@ -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"