Files
keysat-root/MASTER_KEYPAIR_PROCEDURE.md
T
Keysat 843ff0e5d7 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).
2026-06-12 17:51:40 -05:00

8.9 KiB

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:

    {
      "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