# Join Token Security _Short-lived, signed, revocable. How peers prove they belong._ ## Overview Every peer that joins a Mothership presents a join token. The token is a JSON Web Token (JWT) signed by the Mothership's private key. It is the only thing a peer needs to establish trust — no shared passwords, no pre-installed certificates, no out-of-band key exchange. Join tokens are the sharpest edge of Mothership security. Get them wrong and a compromised token equals a compromised team. Get them right and you have an audit-ready, rotation-friendly authentication story that is actually better than most enterprise VCS platforms. This page covers issuance, expiration, rotation, revocation, and the common failure modes. ## Token Anatomy A join token is a standard JWT with three parts: header, payload, signature. Decoded, the payload looks approximately like this: ```text { "iss": "mship_7fcd21a9", // Mothership host ID "sub": "alice@acme.com", // intended holder "aud": "aura-peer", // audience "iat": 1713700000, // issued at "exp": 1713786400, // expires at "jti": "jt_01HYXT8...", // unique token ID "role": "member", // or "admin", "read-only" "one_shot": true, // consumed on first use? "tls_fingerprint": "ab:cd:ef:..." // pinned server fingerprint } ``` The signature is produced with the Mothership's private key. The Mothership never ships the private key anywhere — it only ships the public key, which peers use to verify the signature. The `tls_fingerprint` field is important. It binds the token to the specific TLS certificate the issuing Mothership is using. A stolen token presented to a rogue Mothership with a different certificate will fail verification. This is belt-and-suspenders against man-in-the-middle. ## Signing Algorithm Mothership uses **EdDSA (Ed25519)** by default. Ed25519 is fast, produces small signatures (64 bytes), and is not subject to the RSA/ECDSA implementation pitfalls that have bitten other JWT implementations over the years. The `alg` header is always explicitly checked — the infamous `alg: none` attack on JWT parsers is rejected at the first byte. If you need FIPS-compliant signing, switch to ECDSA P-256: ```toml [jwt] algorithm = "ES256" ``` Other algorithms are either rejected (HS256, because shared secrets don't scale) or behind an explicit flag (RS256 for legacy integration). ## Issuing a Token The simplest path: ```bash aura mothership token issue --for alice@acme.com ``` Output: ```text token: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9... jti: jt_01HYXT8K3P... role: member expires: 2026-04-22 10:00:00 UTC (in 24h) one_shot: true fingerprint: ab:cd:ef:01:23:... ``` The token is printed once. It is not stored in retrievable form on the Mothership. If you lose it, you re-issue. ### Token options ```bash aura mothership token issue \ --for bob@acme.com \ --role admin \ --expires-in 1h \ --one-shot ``` | Flag | Default | Notes | |---|---|---| | `--for` | required | Subject claim. Used for audit logs. | | `--role` | `member` | `member`, `admin`, `read-only`. | | `--expires-in` | `24h` | Accepts `5m`, `1h`, `7d`, etc. Capped by `jwt.max_expiry` in config. | | `--one-shot` | `true` | Token consumed on first use. Disable only when provisioning a fleet. | | `--uses` | `1` | How many times the token can be used. Ignored if `--one-shot` is true. | ### Bulk issuance For onboarding a whole team at once: ```bash aura mothership token issue --bulk team.csv --expires-in 72h ``` Where `team.csv` is: ```text email,role alice@acme.com,admin bob@acme.com,member carol@acme.com,read-only ``` The output is a CSV of emails and tokens. Distribute each row to the respective person over a secure channel (signal, encrypted email, 1Password shared vault). **Do not post them in Slack.** > **Security callout.** Treat join tokens like temporary root credentials for your Mothership. A valid token is sufficient to register a new peer, which means read access to everything replicated to that peer. Use the shortest expiry you can tolerate, and prefer `--one-shot`. ## Expiration Every token has an `exp` claim. After the expiry timestamp, the Mothership rejects the token during handshake with `token expired`. Default expiry is 24 hours. The rationale: even if the token leaks, the attack window is bounded to one day. For most teams, twenty-four hours is more than enough time for someone to accept an invite. If you are onboarding a contractor over the weekend, 72 hours is reasonable. Beyond 7 days, reconsider — a week-old unused token in a Slack DM is a liability. Let it expire; issue a new one. Expiry applies only to the **join** handshake. Once a peer has joined, it is issued a long-lived refresh credential (rotated automatically). Expired join tokens do not kick existing peers. ## Rotation Two kinds of rotation matter: ### 1. Rotating the signing key This invalidates every unjoined token and forces all peers to re-handshake on next reconnect. Use it if you believe the private key may have been compromised. ```bash aura mothership rotate-key ``` Aura keeps the previous public key for a grace period (default 24 hours) so peers mid-handshake can complete. You can tighten or widen this: ```toml [jwt] key_rotation_grace = "1h" ``` ### 2. Rotating peer refresh credentials Happens automatically. Every peer re-keys its refresh credential periodically (default weekly). This is invisible to users and limits blast radius if a peer's disk is stolen — a week-old refresh credential is useless. ## Revocation Rotation is a blast-everything switch. Revocation is surgical. Revoke a specific token by its `jti`: ```bash aura mothership token revoke jt_01HYXT8K3P... ``` Revoke every token (issued and active) for a user: ```bash aura mothership peer revoke alice@acme.com ``` Revoke a specific connected peer by ID: ```bash aura mothership peer revoke peer_0a1b2c3d ``` Revocations are persisted to a revocation list that is consulted on every handshake and refresh. The list is replicated to connected peers, so even if your Mothership goes down briefly, revocations issued before it went down are still enforced. > **Gotcha.** A revoked peer that is currently connected will drop on its next heartbeat (default 30s), not instantly. If you need instant disconnect, follow revocation with `aura mothership peer kick `. ## Example Flow: Onboarding a Contractor Concrete example. Carol, a contractor, starts Monday and leaves Friday. **Monday morning**, issue her a 5-day token with read-only role: ```bash aura mothership token issue \ --for carol@contractor.com \ --role read-only \ --expires-in 5d ``` Send the token via Signal. Walk her through: ```bash aura mothership join --url https://mothership.acme.internal:7777 \ --token ``` **Mid-week**, confirm she is connected: ```bash aura mothership peers ``` ```text peer_id subject role last_seen peer_0a1b2c3d alice@acme.com admin 12s ago peer_1b2c3d4e bob@acme.com member 4s ago peer_2c3d4e5f carol@contractor.com read-only 31s ago ``` **Friday evening**, revoke her access: ```bash aura mothership peer revoke carol@contractor.com ``` She is immediately unable to pull new changes. Her local copy still exists — that's how Git works and Mothership doesn't reach into her disk — but she can no longer sync or push. ## Audit Log Every token issuance, use, revocation, and failed handshake is logged: ```bash aura mothership audit --since 24h ``` ```text 2026-04-21 09:00:12 token_issued jt_01HYXT8K3P by admin@acme.com for carol@contractor.com 2026-04-21 09:04:33 token_used jt_01HYXT8K3P peer_2c3d4e5f from 10.0.1.42 2026-04-21 14:22:09 handshake_fail jt_expired from 52.14.x.x 2026-04-21 17:59:01 token_revoked peer_2c3d4e5f by admin@acme.com ``` This log is replicated like any other log, so losing the Mothership host does not lose the audit trail. Compliance teams have asked for this; it is worth knowing it exists. ## Common Failure Modes | Symptom | Likely cause | Fix | |---|---|---| | `token expired` | Expiry elapsed | Re-issue | | `invalid signature` | Wrong Mothership, or key rotated | Use token from current Mothership | | `token already used` | One-shot token reused | Re-issue | | `tls fingerprint mismatch` | Mothership cert changed, or MITM | Verify Mothership cert; if legit, re-issue | | `revoked` | Token explicitly revoked | Re-issue | | `role not permitted` | Role downgraded | Adjust role and re-issue | ## Operational Practices We Recommend A small set of habits that distinguish teams that treat tokens seriously from teams that get burned. **Distribute out-of-band, never in code or ticket systems.** Signal, a password manager shared vault, or in-person QR codes. Not Slack DMs, not email, not a comment on a ticket. **Prefer short expiry.** Twenty-four hours is the default for a reason. If a token sits in an inbox unused, it's a liability. Make people ask for a fresh one; it's cheap. **Use role-appropriate tokens.** A read-only contractor does not need a `member` token. Mothership can't un-read what a member read, but it can limit what a read-only role sees going forward. **Audit monthly.** Run `aura mothership audit --since 30d | grep token_issued` and sanity-check who got tokens and whether their access is still warranted. Revoke stale peers. **Rotate the signing key yearly, or after incidents.** A yearly rotation forces every peer to re-verify through the grace window and shakes out any stale state. Do it in a quiet week. **Back up the signing key.** Losing the JWT signing key does not lock you out of your own Mothership — Mothership can always issue tokens because it holds the key — but if you lose the key and the Mothership host simultaneously (fire, theft), restoring the Mothership requires both backed-up data and the backed-up key. Back them up together. **Never commit tokens to source control.** Sounds obvious. Scan for it anyway. JWTs are easy to grep for because they start with `eyJ`. ## Integration With Identity Providers For teams that already run SSO, Mothership can delegate identity checks to your IdP in addition to its own JWT validation. Covered in more detail in [TLS and JWT](/tls-and-jwt), but worth mentioning here: a join token plus a fresh OIDC assertion from your IdP gives you an auditable, revocable, time-bounded flow where a disabled Okta account means an unusable join token. For regulated environments, this is usually what compliance wants to see. ## Next Steps - [TLS certificate management](/tls-and-jwt) - [Team topologies and trust boundaries](/team-topology) - [Troubleshooting handshake failures](/mothership-troubleshooting)