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:
{
"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:
[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:
aura mothership token issue --for alice@acme.com
Output:
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
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:
aura mothership token issue --bulk team.csv --expires-in 72h
Where team.csv is:
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.
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:
[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:
aura mothership token revoke jt_01HYXT8K3P...
Revoke every token (issued and active) for a user:
aura mothership peer revoke alice@acme.com
Revoke a specific connected peer by ID:
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 <peer_id>.
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:
aura mothership token issue \
--for carol@contractor.com \
--role read-only \
--expires-in 5d
Send the token via Signal. Walk her through:
aura mothership join --url https://mothership.acme.internal:7777 \
--token <token>
Mid-week, confirm she is connected:
aura mothership peers
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:
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:
aura mothership audit --since 24h
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, 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.