docs(admin): live deployment section for the running license server

Documents the post-deploy state of PR 1: live URLs (datatools and
licenses subdomains on unalogix.com), the on-box filesystem layout
under /srv/datatools-license/, where the admin token lives and how
to retrieve / rotate it, the laptop-side SSH-tunnel + admin_cli
mint workflow, inspection commands (logs, psql, container status),
restart / rebuild procedures, manual backup commands until cron
lands, the production-key rotation outline, and a deployed-vs-queued
capability matrix.

Secrets are NEVER pasted into this doc — the admin token's literal
value lives only on disk (mode 400, UID 10001). Committing it to
git would mean permanent leakage via history even after rotation;
documenting its location + rotation procedure achieves the same
operational outcome without the residual exposure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 01:19:57 +00:00
parent 1cf69dd23b
commit b5cd74d474

View File

@@ -2,9 +2,177 @@
Creator/operator-only reference. End users should read `USER-GUIDE.md` instead.
This doc covers everything the creator does that buyers never see: generating
the signing keypair, minting license blobs, the dev vs. production key story,
and how to recover from key loss.
This doc covers everything the creator does that buyers never see: minting
through the live server, where state lives on the box, how to rotate secrets,
generating the signing keypair, the dev vs. production key story, and how to
recover from key loss.
---
## Live deployment (PR 1)
The license server is running at:
| URL | What it serves |
|---|---|
| `https://datatools.unalogix.com/` | Marketing site (placeholder — "DataTools — coming soon") |
| `https://licenses.datatools.unalogix.com/health` | Liveness + DB reachability probe |
| `https://licenses.datatools.unalogix.com/internal/*` | nginx-blocked on the public side — accessible only via SSH tunnel |
| Postgres @ `127.0.0.1:5433` (localhost) | DB containing the authoritative `licenses` table |
**Host**: `46.225.166.142` (Ubuntu 24.04), nginx 1.24, Postgres 16-alpine + FastAPI in Docker.
**Cert**: Let's Encrypt, covers both subdomains, expires 2026-08-12, auto-renews via `certbot.timer`.
### On-box state
| Path | Contents |
|---|---|
| `/srv/datatools-license/` | Deploy root, mode 750, owned by `datatools-api` |
| `/srv/datatools-license/compose.yml` | Production docker-compose definition |
| `/srv/datatools-license/app/` | Git clone of this repo (re-clone or `git pull` to update) |
| `/srv/datatools-license/secrets/` | Mode 750 dir holding `pg_password`, `admin_token`. Files are mode 400, owned UID 10001 (container app user) |
| `/srv/datatools-license/backups/` | Postgres dumps land here (cron not yet wired — see §"Backups" below) |
| `/etc/nginx/sites-available/unalogix` | nginx config for both subdomains |
| `/etc/letsencrypt/live/datatools.unalogix.com/` | TLS cert + key |
Container names: `datatools-api`, `datatools-postgres`. Both use
`restart: unless-stopped`.
### Get the admin token
```bash
ssh michael@46.225.166.142 'sudo cat /srv/datatools-license/secrets/admin_token'
```
The token is **never** in git, in environment-variable dumps, or in
`docker inspect`. It lives on disk under mode 400 / UID 10001 (so only
root and the container app user can read it).
### Rotate the admin token
Any time it's been shown somewhere it shouldn't, or as routine hygiene:
```bash
cd /srv/datatools-license
openssl rand -hex 32 > secrets/admin_token
chown 10001:10001 secrets/admin_token
chmod 400 secrets/admin_token
docker compose restart api # ~3 seconds; old token stops working immediately
```
### Mint a license from your laptop
```bash
# 1. Open the SSH tunnel (leave running in a background terminal)
ssh -L 8090:127.0.0.1:8090 michael@46.225.166.142 -N &
# 2. Set the auth env
export DATATOOLS_ADMIN_TOKEN="$(ssh michael@46.225.166.142 'sudo cat /srv/datatools-license/secrets/admin_token')"
export DATATOOLS_ADMIN_URL=http://127.0.0.1:8090
# 3. Mint
python3 -m src.admin_cli mint \
--name "Buyer Name" \
--email buyer@example.com \
--tier core
# 4. (optional) List or revoke
python3 -m src.admin_cli list --email buyer@example.com
python3 -m src.admin_cli revoke DT1-CORE-xxxx-yyyy --reason "refund"
```
The blob lands in the response (and in the `licenses` table). Deliver it
to the buyer however suits — copy-paste into email, attach as `.dtlic`.
### Inspect / debug
```bash
# Container status + recent logs
ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose ps && docker compose logs api --tail 30'
# Query the licenses table directly
ssh michael@46.225.166.142 'cd /srv/datatools-license && docker compose exec -T postgres \
psql -U datatools_api -d datatools_licenses -c "SELECT license_key, email, tier, source, expires_at FROM licenses ORDER BY created_at DESC LIMIT 20;"'
# Public-side health
curl https://licenses.datatools.unalogix.com/health
```
### Bring it down / back up / rebuild
```bash
cd /srv/datatools-license
# Restart just the API (e.g. after rotating a secret)
docker compose restart api
# Restart everything
docker compose restart
# Bring down (DB volume PRESERVED)
docker compose down
# Bring up
docker compose up -d
# Rebuild the image after a git pull
cd app && git pull
cd ..
docker compose build && docker compose up -d
docker compose exec api alembic upgrade head # if new migrations
```
### Backups (not yet automated)
Postgres state is the system of record for the customer list — once PR 2
auto-mints from Gumroad webhooks, losing the DB would mean losing every
buyer record. Schedule a daily dump:
```bash
# /etc/cron.daily/datatools-license-backup — see SETUP-LICENSE-SERVER.md §9
```
Until that's in place, dump manually before any risky operation:
```bash
docker compose exec -T postgres \
pg_dump -U datatools_api datatools_licenses \
| gzip > backups/db-$(date -u +%Y%m%dT%H%M%SZ).sql.gz
```
### Production signing key (not yet rotated)
The server currently signs with the in-tree dev keypair (no
`DATATOOLS_LICENSE_PRIVKEY_FILE` configured → falls back to
`src/license/_dev_keypair.py`). That matches what the desktop currently
verifies against, so existing buyers continue to work.
**Before shipping v1.0 to paying buyers**, rotate to a production keypair:
1. `python scripts/generate_keypair.py` (on a trusted machine).
2. Save the private hex to `/srv/datatools-license/secrets/license_privkey`,
chmod 400, chown 10001:10001.
3. Bake the public hex into the PyInstaller build's
`DATATOOLS_LICENSE_PUBKEY` env.
4. Wire `DATATOOLS_LICENSE_PRIVKEY_FILE` + `DATATOOLS_LICENSE_PUBKEY`
into compose.yml's `api.environment` and add `license_privkey` to
the secrets block.
5. `docker compose restart api`.
### What's deployed (PR 1) vs queued (PR 2 / 3)
| Capability | Status |
|---|---|
| Mint API + Postgres + auth | **Live** |
| `datatools-admin` CLI (manual mints) | **Live** |
| `licenses.datatools.unalogix.com/health` public | **Live** |
| Gumroad webhook receiver | **PR 2** |
| Postmark transactional email | **PR 2** |
| Buyer renewal / re-delivery portal | **PR 3** |
| Cloudflare in front (DDoS / WAF) | Deferred (DNS at supercp/cPanel) |
| Production signing keypair | Deferred (still using dev key) |
| Automated DB backups | **Pending** — see §"Backups" |
---