Skip to main content

Security

The Hamtrax CLI is the only public Hamtrax API today, so its security model gets first-class treatment. This page covers what we store, what we log, how to rotate keys cleanly, what to do if a key leaks, and the GitHub Secret Scanning Partner Program registration that auto-blocks accidental commits.

Key storage on the server

API keys are stored as SHA-256 hashes in Firestore, never as plaintext. SHA-256 (rather than bcrypt or Argon2) is the right choice here for two reasons:

  1. The keys are opaque high-entropy tokens with ~120 bits of entropy in the secret portion -- not user-chosen passwords. Slow-hashing buys nothing against tokens that aren't dictionary-attackable.
  2. Auth happens on every CLI request. We need O(1) lookup; bcrypt would add 50-150 ms of CPU per request and force a different storage architecture.

What's stored per key (collection apiKeys, server-only access):

FieldPurpose
keyIdRandom 16-byte hex doc id. Not the secret.
userIdOwner's Firebase Auth uid.
hashedKeysha256(plaintext) hex -- the only copy of any part of the secret.
keyPrefixFirst 12 chars of plaintext (htx_live_xxx). Used for UI display.
tierbasic or elevated.
nameUser label, ≤80 chars.
createdAtServer timestamp.
lastUsedAtServer timestamp, fire-and-forget update on every authenticated call.
lastUsedIpsha256(ip + salt) hex -- never the raw IP.
lastUsedUserAgentFirst 200 chars of the request UA.
revokedAtServer timestamp when revoked, or null.
revokedReason'user' | 'github_secret_scanning_alert' | null (null when not revoked).

Plaintext is shown exactly once at creation in the web app's reveal modal. There is no recovery flow. If you lose a key, revoke it and create a new one.

Plaintext format

htx_live_[a-z2-7]{24}
  • htx_live_ -- fixed prefix, registered with the GitHub Secret Scanning Partner Program (status: pending registration -- see below).
  • [a-z2-7]{24} -- base32-lowercase secret, ~120 bits of entropy.
  • Total length: 33 characters.

Example: htx_live_a4b6c5d7e2f3g4h5i6j7k2l3.

What gets logged

Every authenticated CLI request triggers a fire-and-forget update to the key's audit fields. We deliberately log the minimum:

  • lastUsedAt -- when the key was last successfully used.
  • lastUsedIp -- a SHA-256 hash of the client IP plus a server-side salt (CLI_IP_HASH_SALT). The raw IP is never stored. Two requests from the same IP produce the same hash, so you can correlate suspicious activity without holding raw PII.
  • lastUsedUserAgent -- truncated to 200 characters.

The update is non-blocking -- if the audit write fails, the request still succeeds. The user-facing CLI never logs plaintext keys at any verbosity level (no console.log, no Sentry breadcrumbs, no --verbose exposure).

Key rotation

The MVP rotation flow is delete-and-create:

  1. Create a new key in the web app at the same tier as the old one.
  2. Configure the CLI with the new key (hamtrax auth set-key …).
  3. Verify with hamtrax whoami that you're using the new key.
  4. Revoke the old key in the web app.
# Concrete sequence
hamtrax auth set-key htx_live_<new>
hamtrax whoami --json | jq .callsign # confirm
# Now revoke the old key in the web app's HamtraxCli tool.

Rotation has zero downtime as long as you swap keys before revoking the old one. There's no "expires-at" field today -- keys are valid until explicitly revoked. A rotateApiKey() callable that swaps the hash on the same doc (preserving audit history) is on the roadmap for v1.1.

Leaked-key flow

Suspect a key has leaked? Revoke it immediately. Two equivalent paths:

From the CLI (fastest if your terminal is the leak)

hamtrax auth panic-revoke

This calls the same revoke endpoint the web app uses, against the currently configured key (env var or keychain). The server sets revokedAt = now, revokedReason = 'user'. Subsequent requests with that key return 401 key_revoked and the CLI exits with code 3.

From the web app (works on mobile)

  1. Sign in to hamtrax.com.
  2. Open the Hamtrax CLI tool from the sidebar.
  3. Find the key by its prefix or name.
  4. Click Revoke, confirm by re-typing the key name.

Revocation is instant -- the next API call rejected within seconds. The doc is never deleted, only marked revoked, so you keep the audit trail of when and why.

If you can prove the key was scraped from a public location (GitHub commit, Slack channel, etc.), file a Hamtrax support ticket with the leak source so we can scan our own logs for cross-account abuse.

GitHub Secret Scanning Partner Program

We have submitted the regex htx_live_[a-z2-7]{24} to GitHub's Secret Scanning Partner Program (status: pending registration, target: pre-launch).

Once active, this gives us two things:

  1. Push protection -- pushes containing a matching string are blocked at git-push time on user repos that opt in. The user has to acknowledge an override prompt to proceed.
  2. Existing-leak alerts -- GitHub scans existing public commits and notifies us about matches so we can revoke proactively, before the leaker even notices.

We will publish a status update on this docs page when registration completes.

Threat model summary

What the design defends against:

AttackMitigation
Database read leaks plaintext keysPlaintext is never stored; only sha256(plaintext).
Brute-forcing a key by trying random values120 bits of entropy in the secret + cliUnauthPerIp rate limit (20/min).
Hash-grinding the key validation endpointcliUnauthPerIp bucket runs BEFORE the sha256 + Firestore lookup.
Replaying a key in a browser via XHRCORS is explicitly disabled (cors: false) on the CLI API.
Privilege escalation from basic to elevatedTier is fixed at key creation and checked on every elevated route.
Existence-leak via 403 vs 404Missing-or-not-yours both return 404 with identical bodies.
Logging plaintext at verbose levelThe CLI never reads or prints the configured plaintext at any verbosity.
Stealing IP-correlation data from the audit tablelastUsedIp is hashed with a server-side salt; raw IP never stored.

What this design does not defend against:

  • An attacker who has shell access on your laptop and can read your keychain. Use hamtrax auth panic-revoke immediately if that happens.
  • Active attacks against your network path (compromised CA, mitm). Standard TLS hygiene applies.
  • Social-engineering the user into pasting their key into a malicious tool. Always verify what's running before you auth set-key.

Hardening checklist

For users running automated workflows:

  • Set HAMTRAX_API_KEY from your secret manager, not from a file checked into version control.
  • Use a dedicated basic-tier key per environment (laptop, Pi, CI). Scoped revocation is much safer than rotating one shared key.
  • Set HAMTRAX_NO_KEYRING=1 in headless contexts so the CLI doesn't probe a non-existent Secret Service daemon.
  • Add *.config/hamtrax/config.json to your dotfiles .gitignore.
  • Run hamtrax auth panic-revoke if a laptop is lost or a CI runner is compromised.
  • Review lastUsedAt on your keys monthly; revoke anything you no longer recognize.

Telemetry

The CLI sends no telemetry. The only network traffic from the CLI is the explicit API call you triggered. There are no analytics, no error reporters, no version-check pings.