Initial backup of root workspace files

Glue files not covered by subproject repos: top-level docs, logo,
keysat-design-system, and crosscheck tests. Subproject folders are
gitignored (each has its own Gitea remote).
This commit is contained in:
Keysat
2026-06-12 17:51:40 -05:00
commit 843ff0e5d7
55 changed files with 6887 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
# Keysat master keypair — generation, storage, and import
This document covers the keypair that signs every Keysat self-license — the
"Keysat-licenses-Keysat" trust root. It is the single most security-critical
piece of cryptographic material in the entire project. Anyone with the
private key can mint Keysat licenses indefinitely. Treat it accordingly.
## What this keypair is
An Ed25519 keypair generated by Grant on his laptop. The **public** half is
embedded in the Keysat daemon source code at `licensing-service/src/license_self.rs`
in the `TRUST_ROOT_PUBKEY_PEM` constant. Any Keysat install with that source
embedded will accept license keys signed by the corresponding private half,
and reject everything else.
The **private** half is held offline and only used to mint Keysat licenses
(eventually via a "master Keysat" instance that imports the keypair).
## How it was generated
```
openssl genpkey -algorithm Ed25519 -out keysat-master-private.pem
openssl pkey -in keysat-master-private.pem -pubout -out keysat-master-public.pem
```
These are the same commands that ran. They produce two files:
- `keysat-master-private.pem` — the private key, ~120 bytes
- `keysat-master-public.pem` — the public key, ~120 bytes
## What's already done
✅ Public key embedded in `licensing-service/src/license_self.rs` as `TRUST_ROOT_PUBKEY_PEM`.
The current value is:
```
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAgsromMy4osMJplX1rY0fd4ouS6wfkm/vfeY2gXEQHkA=
-----END PUBLIC KEY-----
```
If that block ever needs to change (e.g. if the private key leaks and you have
to rotate), edit `TRUST_ROOT_PUBKEY_PEM` and ship a new package version. Note
that all previously-issued licenses become unverifiable on the new package —
plan rotations carefully.
## What you need to do
### Right now: secure the private key
The private-key file is sitting in your home folder (`~/keysat-master-private.pem`).
Do all of the following before anything else:
1. **Make a paper backup.** Open the file in TextEdit, print it on paper (one
sheet, three lines of base64). Store the printout in a fireproof safe or
safe-deposit box. Test that the printed copy is legible — small fonts and
inkjet bleed are common gotchas.
2. **Make a digital backup in something secure.** Recommended in rough order:
- 1Password / Bitwarden secure note (attach the .pem file). Synced across
your devices, encrypted at rest.
- Encrypted USB drive (VeraCrypt or APFS-encrypted disk image), kept offline.
- GPG-encrypted backup uploaded to a personal cloud bucket.
3. **Delete the plaintext file from your laptop's home folder** once backed up.
```
srm ~/keysat-master-private.pem # macOS
shred -u ~/keysat-master-private.pem # Linux
```
Don't just `rm` it — that leaves the bytes recoverable on most filesystems.
### When you're ready: stand up a master Keysat instance
The master Keysat is a separate Keysat install whose only job is to mint
licenses for the Keysat package itself. It runs on its own Start9 (or wherever)
and uses the private key you generated as its issuer key.
The keypair is stored in the daemon's SQLite database (under the `server_keys`
table), not as a file on disk. To bootstrap the master instance, use the
admin-only `POST /v1/admin/import-issuer-key` endpoint, added in v0.1.0:15
specifically for this scenario.
Steps:
1. Install the Keysat package on a fresh Start9 (or a separate StartOS account
on the same Start9, if possible). Standard install flow. The daemon will
generate a throwaway issuer keypair on first boot — that's expected; you'll
replace it in step 3.
2. Grab the admin API key for this fresh instance: StartOS dashboard → Keysat
service → **Actions → Show admin API key**. Copy the 64-hex-character key.
3. From your laptop (the one with `keysat-master-private.pem`), POST the PEM
to the master Keysat's import-issuer-key endpoint:
```
ADMIN_KEY="paste-the-admin-api-key-here"
MASTER_KEYSAT="https://your-master-keysat.example" # or LAN URL like https://your-mdns.local:port
curl -X POST \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/x-pem-file" \
--data-binary @/path/to/keysat-master-private.pem \
"$MASTER_KEYSAT/v1/admin/import-issuer-key"
```
Expected response:
```json
{
"ok": true,
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",
"restart_required": true,
"message": "Issuer key imported. Restart the Keysat service for the new key to take effect..."
}
```
The endpoint refuses if any licenses have already been issued by this
Keysat — a safety guard against accidentally invalidating customer
keys. Since this is a freshly-installed master instance, it should
succeed.
4. **Stop and start the Keysat service** from the StartOS dashboard. The
in-memory keypair gets re-loaded from the DB on startup, picking up
the imported key. Confirm in the logs that the public key now matches
your `keysat-master-public.pem`.
5. In the master Keysat's admin UI, define a product called "Keysat" with your
intended pricing (e.g. 50,000 sats standard, 250,000 sats patron).
6. Define a `default` policy on the Keysat product. Perpetual single-seat is a
reasonable default. Optionally add entitlements: `["patron"]` for a Patron
tier.
7. The master instance is now ready. Self-redeem a free license for your own
non-master Keysat installs (use `free_license` discount codes for
bootstrapping).
#### Alternative: SSH if you prefer file-system access
If you'd rather not use the import endpoint, you can SSH into the Start9 host
and edit the SQLite database directly. Less recommended (more error-prone), but
documented for completeness:
```
# Enable SSH in StartOS Settings → SSH, add your laptop's pubkey, then:
ssh start9@<your-mdns>.local
# find the keysat data volume
sudo find / -name "*.db" -path "*keysat*" 2>/dev/null
# edit server_keys row 1 with sqlite3
```
This route requires you to UPSERT the right PEM strings into the
`server_keys` table manually. Stick with the curl-based import endpoint
unless you specifically need SSH access for some other reason.
#### Why this isn't a StartOS button
Putting an "Import master signing key" button in every Keysat operator's
StartOS Actions tab would clutter the UX for the 95% of operators who don't
need it (their auto-generated issuer key is exactly what they want). The
import-issuer-key endpoint is admin-only, invisible by default, and called
exactly once during master setup.
### When 100 free first-user licenses are needed
In the master instance's admin UI:
1. Go to **Discount codes → Create new code**.
2. Set kind `free_license`, max uses 100, no expiry.
3. Pick a memorable code: `EARLY100`, `FOUNDERS100`, etc.
4. Distribute the code via your launch announcement / Twitter / Nostr / mailing
list. First 100 users redeem at `https://registry.keysat.xyz` (or wherever
the buy page is hosted by the master instance).
## Threat model and rotation plan
**If the private key leaks:**
- An attacker can mint Keysat licenses indefinitely.
- Existing customers' licenses are unaffected — the leak doesn't retroactively
invalidate signatures.
- Mitigation: rotate. Generate a fresh keypair, edit `TRUST_ROOT_PUBKEY_PEM`,
ship a new package version, re-issue licenses to existing customers, deprecate
the leaked key in your customer comms.
**If the private key is destroyed (and no backup):**
- You can't issue any new licenses ever.
- Existing customer licenses keep working.
- Mitigation: rotation as above. Painful but recoverable.
This is why the paper backup matters more than anything. Cloud sync is great
for daily use, but a paper copy in a safe is the resilience floor.
## Future: rolling rotation (v0.3+)
The current model is hard-cut: one public key, one private key, one rotation
event invalidates everything. v0.3 will land a rolling-rotation flow:
- Daemon embeds *two* trust-root pubkeys — primary and standby.
- During rotation, the daemon accepts licenses signed by either.
- After all customers have re-licensed under the new key, the old pubkey is
retired in the next package version.
This makes rotation graceful instead of catastrophic. Until that ships,
treat the private key as if losing it would mean re-licensing every customer
(because it would).
## Quick reference
| Action | Command |
|---|---|
| Generate keypair (already done) | `openssl genpkey -algorithm Ed25519 -out keysat-master-private.pem && openssl pkey -in keysat-master-private.pem -pubout -out keysat-master-public.pem` |
| Inspect public key | `openssl pkey -in keysat-master-public.pem -pubin -text -noout` |
| Inspect private key (don't normally do this) | `openssl pkey -in keysat-master-private.pem -text -noout` |
| Securely delete plaintext | macOS: `srm <path>` &middot; Linux: `shred -u <path>` |
| Verify which key is embedded in daemon | `grep -A4 TRUST_ROOT_PUBKEY_PEM licensing-service-startos/licensing-service/src/license_self.rs` |