Skip to content
C.W.K.
Lesson 02 of 05 · published

Tickets Minted Where Identity Is Proven

~14 min · authentication, bearer-tokens, hashing, renewal, sqlite

Level 0Loose File
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Fast Wires Have No WireGuard

The last lesson's proof only works on the tailnet. But Track 3 sends most of the family's bytes somewhere else: a Thunderbolt bridge between two Macs on the same desk, or the house's 10GbE and Wi-Fi. The measured ones are about 27 times (10GbE) to 83 times (Thunderbolt) faster than the tailnet, and none of them carries WireGuard. A packet arriving on a Thunderbolt address says nothing trustworthy about who sealed it, because nobody sealed it.

So the fast routes need a credential. The obvious move is a shared secret in a config file on every Mac. Delta refuses it for the reason the track is named after: a secret you copy is a secret you have to rotate everywhere, and one leaked copy speaks for the whole fleet. Instead, each serving Mac mints a credential for one calling Mac, and it does so over the one channel where it already knows who is asking.

Mint on the Tailnet, Spend on the LAN

  1. The calling Mac needs a LAN or Thunderbolt route to a peer. Before its first request there, it sends POST /v1/tickets to that peer over the tailnet.
  2. The peer's gateway authenticates the caller exactly as in lesson 1 (address, roster, label). Only then does it mint: secrets.token_urlsafe(32), 32 random bytes in URL-safe base64.
  3. The peer stores a row: the ticket's SHA-256, the caller's label, issued, expires (thirty days), last used. The ticket itself is returned once, in the response body, and never stored on the issuer.
  4. The caller keeps the ticket in its own state database, keyed by the peer's label, and presents it as Authorization: Delta <ticket> on every Thunderbolt and LAN request to that peer.

The gateway enforces the first step structurally. /v1/identity and /v1/tickets on any route but the tailnet answer 403: "identity and tickets are served over the tailnet only". A ticket cannot be minted with a ticket, and it cannot be minted on a wire where the caller is anonymous. That is invariant 3, and it is why the scheme does not collapse into "whoever reaches the port".

Why the Issuer Keeps Only a Hash

The issuer has to recognise a ticket; it does not have to know it. Verification hashes the presented value and looks the hash up. If the serving Mac's state database is ever copied (a backup, a disk image, a curious process), the copy holds thirty days of hashes that open nothing. Because the secret is 256 random bits rather than a human password, a plain SHA-256 is enough: there is no dictionary to try, so a slow password hash would add cost and no safety.

The holder's side is different by necessity. To present a ticket you must have it, so the caller stores it in plain form. That row lives under Delta's state root, ~/cwk-dbs/delta-db, and that folder is in CREDENTIAL_DIRS: the home share refuses to serve or write it at any depth, to any Mac. Delta's own credentials are never data on Delta's own channel.

Thirty Days, Renewed a Week Early

MomentWhat happens
Ticket has more than 7 days leftUsed as is, read from memory (no database read per request)
7 days or fewer leftThe holder treats it as gone and mints a new one over the tailnet
Issuer mints a new ticket for that labelExpired rows are pruned; the caller's older live ticket stays valid until it expires, so requests already in flight with it never fail
Ticket refused (401: unknown, revoked, minted by another Mac, expired)The holder drops it, mints again, and retries

Renewing early turns expiry into a non-event: the boundary is crossed on a quiet request, a week before it could bite. Renewing on 401 handles everything else, including a peer whose state was reset or an issuer that revoked the label on purpose (Tickets.revoke deletes every row for it).

Eight Workers, One New Ticket

A transfer on Thunderbolt runs eight workers. If the peer has just forgotten this Mac's ticket, all eight get 401 within the same millisecond, and a naive client mints eight tickets. Delta's PeerClient.ticket takes a per-peer lock and passes along the ticket that was refused. The first worker through the lock mints; the other seven find a held ticket that is not the one they were refused with, and use it. One round trip on the tailnet, not eight.

On the issuer, verification is memoised for sixty seconds per ticket hash, so a busy route costs one database read and one last_used write per ticket per minute rather than per request. The memo also records expiry, so a ticket that expires inside the minute is refused on time.

What Never Happens

A ticket never rides in a URL, where proxies and access logs would keep it; it travels only in the Authorization header. It never leaves the two Macs it was minted between. It is never minted off the tailnet, and nothing ever copies one from Mac to Mac. If you ever find yourself shipping a ticket file with an installer, you have rebuilt the thing this lesson exists to avoid.

Code

Delta's tickets: mint, verify, and the holder's renewal rule (trimmed excerpt)·python
# delta/identity.py (trimmed excerpt)
TICKET_SECONDS = 30 * 24 * 3600
TICKET_RENEW_SECONDS = 7 * 24 * 3600


def _hash(secret: str) -> str:
    return hashlib.sha256(secret.encode()).hexdigest()


class Tickets:
    VERIFIED_SECONDS = 60.0

    def mint(self, label: str) -> dict:
        secret = secrets.token_urlsafe(32)
        now = time.time()
        expires = now + TICKET_SECONDS
        self.store.execute(
            "INSERT INTO tickets_issued(hash, label, issued, expires, last_used) VALUES (?,?,?,?,NULL)",
            (_hash(secret), label, now, expires))
        # Expired tickets are pruned; a caller's older live ticket stays valid
        # until it expires, so requests in flight during a renewal never fail.
        self.store.execute("DELETE FROM tickets_issued WHERE expires < ?", (now,))
        return {"ticket": secret, "expires": expires, "label": label}

    def verify(self, header) -> str:
        if not header or not header.startswith("Delta "):
            raise Unauthorized("this route needs a Delta ticket; the caller mints one over the tailnet")
        digest = _hash(header[len("Delta "):].strip())
        row = self.store.query_one("SELECT label, expires FROM tickets_issued WHERE hash=?", (digest,))
        if not row:
            raise Unauthorized("unknown Delta ticket (revoked or minted by another Mac); "
                               "mint a new one over the tailnet")
        label, expires = row
        if expires < time.time():
            raise Unauthorized(f"the Delta ticket for {label} expired; mint a new one over the tailnet")
        return label

    def held(self, peer: str):
        row = self.store.query_one("SELECT secret, expires FROM tickets_held WHERE peer=?", (peer,))
        if not row or row[1] - time.time() < TICKET_RENEW_SECONDS:
            return None            # a week from expiry counts as gone: mint again
        return row[0]
A hashed ticket store you can run: mint, verify, renew early, revoke·python
#!/usr/bin/env python3
"""A minimal ticket issuer and holder in one SQLite file (stdlib only)."""
import hashlib
import secrets
import sqlite3
import time

LIFETIME = 30 * 24 * 3600
RENEW_BEFORE = 7 * 24 * 3600

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE issued(hash TEXT PRIMARY KEY, label TEXT, expires REAL)")
db.execute("CREATE TABLE held(peer TEXT PRIMARY KEY, secret TEXT, expires REAL)")


def digest(secret: str) -> str:
    return hashlib.sha256(secret.encode()).hexdigest()


def mint(label: str, now: float) -> dict:
    secret = secrets.token_urlsafe(32)
    db.execute("INSERT INTO issued VALUES (?,?,?)", (digest(secret), label, now + LIFETIME))
    db.execute("DELETE FROM issued WHERE expires < ?", (now,))
    return {"ticket": secret, "expires": now + LIFETIME}


def verify(header: str, now: float) -> str:
    if not header.startswith("Delta "):
        raise PermissionError("401: no ticket")
    row = db.execute("SELECT label, expires FROM issued WHERE hash=?",
                     (digest(header[6:].strip()),)).fetchone()
    if not row or row[1] < now:
        raise PermissionError("401: unknown or expired ticket")
    return row[0]


def ticket_for(peer: str, now: float) -> str:
    row = db.execute("SELECT secret, expires FROM held WHERE peer=?", (peer,)).fetchone()
    if row and row[1] - now >= RENEW_BEFORE:
        return row[0]
    doc = mint("workbench", now)       # in Delta this is POST /v1/tickets over the tailnet
    db.execute("INSERT OR REPLACE INTO held VALUES (?,?,?)", (peer, doc["ticket"], doc["expires"]))
    return doc["ticket"]


now = time.time()
first = ticket_for("peer", now)
print("label:", verify("Delta " + first, now))
print("same ticket a day later:", ticket_for("peer", now + 86400) == first)
later = ticket_for("peer", now + 24 * 86400)      # 6 days left: renewed early
print("renewed at day 24:", later != first)
print("old ticket still valid until it expires:", verify("Delta " + first, now + 25 * 86400))
print("issuer stores only hashes:", all(len(h) == 64 for (h,) in db.execute("SELECT hash FROM issued")))
db.execute("DELETE FROM issued WHERE label=?", ("workbench",))   # revoke
try:
    verify("Delta " + later, now + 25 * 86400)
except PermissionError as exc:
    print("after revoke:", exc)

External links

Exercise

Run the ticket store above, then extend it with the eight-worker case: start eight threads that each call a renew(peer, refused) function after a simulated 401 on the same ticket. Make it mint exactly one new ticket. Print how many rows the issuer holds before and after.
Hint
Guard renewal with a threading.Lock per peer. Inside the lock, if the held ticket is no longer the one this worker was refused with, return the held one instead of minting. SQLite connections are per thread, so give the demo one shared connection opened with check_same_thread=False and keep all access inside the lock.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.