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:
@@ -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>` · 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` |
|
||||
Reference in New Issue
Block a user