Athena

Format, Storage, and Lifecycle

The plaintext credential shape, the stored metadata, and what changes as a key ages.

Athena separates the credential you hand to a caller from the fields it stores in Postgres. The authoritative model lives in ApiKeyRecord, get_api_key_by_public_id, and create_api_key.

Plaintext key format

New keys are issued as:

ath_{public_id}.{secret}

The current implementation in src/api/admin/mod.rs generates:

  • public_id: the first 16 hex characters of a UUID v4 string
  • secret: two UUID v4 strings concatenated together

That means a newly created key looks like a short lookup prefix plus a long secret, for example:

ath_abcd1234efab5678.59f0d9f7d5e44f86a0d6d488c2b8d0a94b6b3b4b4b4f4a3b86d651a1f0f048c

The full plaintext key is returned once during POST /admin/api-keys and is not retrievable later. Athena stores the lookup and verification material, not the secret itself.

What Athena stores

The main persisted fields are:

FieldPurpose
idExternal UUID returned by admin APIs
public_idShort lookup identifier embedded in the plaintext key
client_nameOptional binding to one logical X-Athena-Client
key_saltPer-key salt used during verification
key_hashSHA-256 digest of key_salt:secret
is_activeSoft lifecycle toggle
expires_atHard expiration cutoff
last_used_atUpdated asynchronously after auth attempts
virgin_mode and related fieldsLock-in and IP learning state

Athena also stores right grants in api_key_right_grants, reusable right definitions in api_key_rights, and optional IP policy rows in api_key_ip_whitelist, api_key_ip_blacklist, and api_key_ip_seen.

Validation lifecycle

At runtime, authorize_gateway_request does not compare the caller's secret to stored plaintext. It:

  1. Extracts the public_id and secret from X-Athena-Key.
  2. Loads the stored row with get_api_key_by_public_id.
  3. Recomputes the expected digest from key_salt and the presented secret.
  4. Rejects or continues based on active state, expiration, client binding, rights, and IP policy.

Lifecycle states

Athena treats a key as valid only when all of these conditions pass:

  • The key has the right ath_{public}.{secret} shape.
  • public_id resolves to a stored row.
  • The recomputed hash matches key_hash.
  • is_active is true.
  • expires_at is unset or in the future.
  • If client_name is set, it matches the request's X-Athena-Client.
  • The key has all required rights.
  • The IP policy allows the caller.

The most common state changes are:

ChangeResult
PATCH /admin/api-keys/{id} with is_active: falseRuntime auth starts returning Inactive API key
Expiration time passesRuntime auth starts returning Expired API key
DELETE /admin/api-keys/{id}Lookup by public_id stops finding the record
Successful authlast_used_at is touched asynchronously

Create response shape

curl -X POST "http://localhost:4052/admin/api-keys" \
  -H "X-Athena-Admin-Key: $ATHENA_KEY_12" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "analytics-worker",
    "client_name": "analytics",
    "rights": ["gateway.query"]
  }'

Typical response fields:

{
  "status": "success",
  "message": "Created API key",
  "data": {
    "api_key": "ath_abcd1234efab5678.secret",
    "record": {
      "id": "uuid",
      "public_id": "abcd1234efab5678",
      "client_name": "analytics",
      "is_active": true,
      "rights": ["gateway.query"]
    }
  }
}

Constraints to remember

  • POST /admin/api-keys requires the static admin secret, not a gateway key.
  • Keys are currently looked up by public_id, so exposing the prefix is fine; the secret still must match key_hash.
  • Right names must already exist in api_key_rights, or creation/update fails.
  • Runtime gateway auth currently reads X-Athena-Key via extract_gateway_api_key.