Authentication¶
API calls use an API key:
Authorization: Bearer gsj_live_<prefix>_<secret><checksum>
Keys created before #125 have the form gsj_<prefix>_<secret> and keep working.
Key owners¶
Every key belongs to one owner (owner_type). What a key can do is the intersection of its scopes and the
owner's current permissions, so a key never gives more than its owner has.
owner_type |
Reaches | Who creates it |
|---|---|---|
project |
one project; reports created with the key land in it | project owner, organization owner/admin |
user |
what the user can access in one organization, right now (a viewer's key is read-only) | the user (not a viewer) |
org |
all projects of the organization | organization owner/admin |
A user key stops working as soon as its owner leaves the organization (it is revoked with reason owner_left).
Keys are managed only with a user session or JWT. An API key cannot create, rotate or revoke keys, and a
platform operator cannot read secrets or create keys on behalf of users.
Create, list, rotate, revoke¶
POST /v1/keys {"name": "ci", "owner_type": "org", "scopes": ["judge", "reports:read"]}
POST /v1/projects/{prj_…}/keys {"name": "ci"} # project key
GET /v1/keys # keys you may see in the current organization (metadata only)
GET /v1/projects/{prj_…}/keys
POST /v1/keys/{key_…}/rotate {"grace_hours": 24} # new key; old one works for the grace period
DELETE /v1/keys/{key_…} # revoke now (also ends a grace period)
DELETE /v1/projects/{prj_…}/keys/{key_…}
- The response to create or rotate contains
keyonce. The server stores only a keyed hash (HMAC-SHA256), so nobody can show the secret again. - Optional fields when you create a key:
scopes,rate_limit(requests per minute),allowed_ips,expires_atorexpires_in_days,never_expires. - Lists show
display(gsj_live_<prefix>_…),scopes,expires_at,allowed_ips,last_used_at,last_used_ip,revoked_at,revoke_reason(manual,rotated,leaked,owner_left) androtated_from. Whenrevoked_atis in the future, the key is in its rotation grace period. - Scopes:
judge(/v1/judge,/v1/judge/batch),reports:read(/v1/reports, exports),jobs(/v1/jobs),projects:read(GET /v1/projects),feedback:write. Default:judge+reports:read.
Expiry and rotation¶
- New keys expire after 90 days by default; you can choose up to 1 year. Only organization keys created
by an organization owner/admin may use
"never_expires": true. - Rotation creates a new key with the same name, scopes, limit, allowlist and owner. The old key keeps working
for the grace period (default 7 days,
grace_hours0-720). After that it returns401 invalid_api_key. - An expired key returns
401 api_key_expired.
IP allowlist¶
allowed_ips is a list of IPv4/IPv6 addresses or CIDR ranges (["203.0.113.0/24", "2001:db8::/32"], max 50).
An empty list allows any address. A request from outside the list returns 403 ip_not_allowed.
Behind a reverse proxy the server reads X-Forwarded-For only when the connection comes from an address in
GSPAMJUDGE_TRUSTED_PROXIES (self-hosting). Otherwise it uses the connection address.
Leaked keys¶
If a key was published (a public repository, a paste or logs), report it:
POST /v1/keys/leaks {"keys": ["gsj_live_…"], "source_url": "https://…"} # no authentication
The server revokes each matching active key immediately (revoke_reason: leaked), notifies the organization
owners by e-mail and writes an audit-log entry. The response is always 202 {"received": N}, so it does not
reveal whether a key existed. Organization owners are also notified when a new key is created.
Key format for secret scanners¶
New keys match the following pattern. It is meant for GitHub secret scanning (partner program, registration pending), gitleaks, trufflehog and similar tools:
\bgsj_live_[0-9a-f]{12}_[0-9A-Za-z]{43}[0-9A-Za-z]{6}\b
The last 6 characters are a CRC32 checksum of the rest of the key in base62 (0-9A-Za-z, left-padded with 0).
A scanner can use it to drop random strings that only look like keys, without calling the API.
Errors and limits¶
See Errors. Missing credentials: 401 unauthorized. Unknown, revoked or wrong key:
401 invalid_api_key. Expired key: 401 api_key_expired. Address outside the allowlist: 403 ip_not_allowed.
Missing scope: 403 insufficient_scope. Rate limit: 429 rate_limited with Retry-After and RateLimit-* headers.
The default limit is 60 requests per minute per key, and rate_limit overrides it for one key. With several server
processes the limit is a fixed one-minute window shared in Redis, so up to twice the limit can pass around a
minute boundary. If the limiter cannot be reached, the API answers 503 rate_limiter_unavailable.
Keep keys secret¶
- Keep keys server-side. Never put them in browser code, URLs or logs. Use a separate key per integration, with the smallest scopes and an IP allowlist where you can.
- Server logs never contain keys. Anything that matches
gsj_…, theAuthorizationheader value and secret query parameters are replaced with[redacted]. - Send your own
X-Request-IDto correlate logs. Every response echoes it.