ADR-0003: Worker protocol and authentication
- Status: Accepted (milestone 1). mTLS, OIDC/SSO and scoped RBAC are Proposed.
- Date: 2026-10-29
Context
Workers may sit behind NAT on other networks, so they must be able to reach the control plane without the control plane reaching them. Remote execution is a security boundary. A credential that lets someone submit jobs must not also let them impersonate a worker, and the reverse also holds.
Decision
- Transport: HTTP/1.1 + JSON, stdlib only (
api/server.py,api/client.py). Workers poll: they send heartbeats (default every 2 s) andacquirecalls. All connections are outbound from the worker. - Protocol version: every authenticated request carries
X-GpuLayer-Protocol: 1, and a mismatch returnsprotocol_mismatch. - Credentials (
api/auth.py). They are never interchangeable, and only SHA-256 hashes are stored (server-credentials.json, mode 0600):- client token (
glc_…): jobs, workers list/revoke, artifacts. - join token (
glj_…):POST /v1/workers/registeronly. - worker session token: minted per worker at registration and stored hashed in the
workerstable. It is valid only for that worker's heartbeat, acquire, task start/logs/finish/rendezvous and deregister. Task ownership is enforced inControlPlane._owned_task. - instance token: single-use and bound to one provisioned instance (ADR-0010).
Workers that claim
origin=providermust present it; a static join token is rejected for them.
- client token (
- Token comparison uses
hmac.compare_digestover hashes. - TLS:
ControlPlaneServerrefuses to bind a non-loopback address without a cert/key, unless--allow-insecure-remoteis given. TLS 1.2 is the minimum. Clients accept--ca-file. - Leases: each assignment carries
lease_expires_at. Heartbeats renew the leases of the worker's active tasks. Expiry marks the tasklostand the workeroffline(ADR-0011). Heartbeat responses carrycancel_task_ids(cancellations, lost tasks) andrevoked. - Revocation (
gpulayer worker revoke) flipsrevoked. Subsequent calls fail with 403, and active tasks becomelost.
Alternatives considered
- gRPC bidirectional streams: lower latency push, but a heavier dependency and harder debugging. This can be revisited for log streaming at scale.
- WebSocket push: same trade-off. Polling was chosen for simplicity and NAT-friendliness.
- mTLS per worker: stronger identity, but needs a PKI and enrollment flow. Proposed as a hardening step.
- A single shared API key: rejected, because it collapses the client/worker boundary.
Consequences and trade-offs
- Cancellation and assignment latency are bounded by the poll and heartbeat interval (seconds).
- Polling load grows linearly with the number of workers.
- Known gaps:
- The join token is static and shared by every non-provider worker. Anyone who has it can register a worker and receive jobs. There is no per-worker enrollment or approval step yet.
GET /v1/artifacts/<sha>is allowed for any authenticated worker, not only one whose task references that artifact. Possession of a digest acts as a capability.- There is no rate limiting or request throttling. Body size caps exist (16 MiB JSON, 2 GiB artifacts).
- There is no audit log beyond lifecycle events, and tokens have no expiry or rotation
API (
server init --rotatereplaces all of them). - One client role: any client token can cancel any job and revoke any worker.
Security and operational implications
- Tokens are printed once at
server init. Request logging never includes headers. - Plaintext HTTP is only permitted on loopback by default.
Follow-up
- Per-worker enrollment (one-time join codes, admin approval), token expiry and rotation.
- Scope artifact reads to the artifacts referenced by the worker's assigned tasks.
- Client roles and RBAC (submit, read, admin), rate limiting and audit events.
- Evaluate mTLS and OIDC for team and hosted deployments.