Nobody has started this yet — be first.
Business impact
Every write endpoint under /v1/jobs -- create, delete, enable/disable, reschedule, trigger -- is currently reachable by anyone who can reach the port, with zero authentication. For a local demo that's fine; for anything deployed beyond a fully trusted private network, it means anyone who can route to the service can create arbitrary jobs that POST to arbitrary URLs with arbitrary payloads (a server-side request forgery primitive), delete or disable existing jobs, or trigger jobs on demand. This is the kind of gap that's easy to overlook precisely because the demo/local experience works perfectly without it -- the risk only appears once someone actually deploys this somewhere reachable by more than themselves.
Problem
internal/api/server.go has the scaffolding for this already: a Server.apiKey field, a Server.SetAPIKey setter, and a requireAPIKey middleware already wired into the /v1/jobs route group -- but the middleware itself is a no-op that calls next.ServeHTTP unconditionally. Every request passes through regardless of s.apiKey or any Authorization header.
Current behavior
GET /v1/jobs with no Authorization header returns HTTP 200 even when an API key has been configured, instead of 401.
Expected behavior
When s.apiKey is non-empty, requests to /v1/jobs* must present "Authorization: Bearer <the configured key>" or be rejected with 401 Unauthorized. When s.apiKey is empty (the default), the API should remain fully open, preserving the current zero-setup demo experience.
Steps to reproduce
Why this matters
This needs to be an opt-in gate, not a hard requirement, because nothing in cmd/chronos/main.go currently calls SetAPIKey -- making the check unconditionally required would break the existing demo flow and every other ticket's test that hits the HTTP API without a key. The middleware needs to check s.apiKey != "" before enforcing anything.
Suggested approach
Look at how s.instanceID or other Server fields are read inside a handler for the pattern; requireAPIKey needs to inspect r.Header.Get("Authorization"), compare it against an expected "Bearer " + s.apiKey value, and call next.ServeHTTP only on a match (or unconditionally when s.apiKey == "").
Acceptance criteria
Verification
TEST_DATABASE_URL="postgresql://chronos:chronos@localhost:5432/chronos?sslmode=disable" go test ./tickets/tests/... -run TestTicket19 -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/api-key-authFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/api-key-authSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inhttp://localhost:8081app-1http://localhost:8082app-2http://localhost:8090curl localhost:8081/healthzcurl localhost:8081/readyzWork the tickets in tickets/docs/ (ordered easy → hard); each names one Go test in tickets/tests/. Most tickets need a real Postgres (tickets 4, 11, and 18 are pure-function tests and do not):
export TEST_DATABASE_URL="postgresql://chronos:chronos@localhost:5432/chronos?sslmode=disable"
go test ./tickets/tests/... -run TestTicket08 -v # a single ticket
./tickets/run_tickets.sh # all 20, clean pass/fail summary
Use -race when working on tickets 15, 16, or 17 (the concurrency ones) -- run_tickets.sh already does this for every ticket. Tickets 15 and 16 are adjacent bugs in the same function (pkg/leader/leader.go's lock-release path); fix ticket 16 first, since ticket 15's test keeps failing for ticket 16's reason until that one is fixed too.
Level 2
Implement a feature
Extend the system within its own patterns.