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:
174
docs/ADMIN.md
174
docs/ADMIN.md
@@ -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" |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user