Control Plane
The hosted control plane handles human login, short-lived bearer credentials,
agent enrollment, and tenant CA signing. The data plane remains the per-tenant
orlop-server; after enrollment the agent reads entity data directly from that
server over mTLS.
See design-auth.md for the security model and
control-plane-runbook.md for operator tasks.
Runtime
Section titled “Runtime”cmd/orlop-control is a Go service and CLI.
CLI groups:
orlop-control migrate: apply Postgres migrations.orlop-control ca: bootstrap root and tenant CAs.orlop-control user: seed admin sessions and suspend users.
Service environment:
| Variable | Meaning |
|---|---|
PORT |
HTTP listen port, default 8080. |
DATABASE_URL |
Postgres DSN. Without it, device-flow and enroll routes are not mounted. |
ORLOP_SECRETS_DIR |
Filesystem secrets root containing CA material. Required for /agent/enroll. |
ORLOP_TRUST_DOMAIN |
SPIFFE trust domain, default orlop.example. |
ORLOP_ORG_NAME |
X.509 organization, default ORL. |
Health check:
curl -fsS https://control.orlop.example/healthzDevice Flow
Section titled “Device Flow”orlop login uses a first-party device flow shaped like RFC 8628.
Create code
Section titled “Create code”curl -fsS -X POST https://control.orlop.example/auth/device/code \ -H 'Content-Type: application/json' \ -d '{}' | jq .Response:
{ "device_code": "opaque", "user_code": "ORL-ABCD", "verification_uri": "https://control.orlop.example/device", "expires_in": 900, "interval": 5}The user opens verification_uri and enters user_code. The approval page
requires an admin session cookie, usually created from the one-shot URL printed
by orlop-control user seed.
Poll token
Section titled “Poll token”curl -fsS -X POST https://control.orlop.example/auth/device/token \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "opaque" }' | jq .Pending responses are OAuth-style errors:
{"error":"authorization_pending"}Success:
{ "access_token": "opaque", "access_expires_at": "2026-04-30T13:00:00Z", "refresh_token": "opaque", "refresh_expires_at": "2026-05-30T12:00:00Z", "control_plane_url": "https://control.orlop.example", "token_type": "Bearer", "expires_in": 3600}orlop login persists this as ~/.config/orlop/credentials.json with mode
0600.
Refresh token
Section titled “Refresh token”curl -fsS -X POST https://control.orlop.example/auth/token/refresh \ -H "Authorization: Bearer <refresh_token>" | jq .The response shape matches the successful device-token response. The Rust
client refreshes automatically when the access token is inside its safety
window. If refresh fails with 401 or 403, the client tells the user to run
orlop login again.
Agent Enrollment
Section titled “Agent Enrollment”orlop mount calls /agent/enroll with the current access token.
curl -fsS -X POST https://control.orlop.example/agent/enroll \ -H "Authorization: Bearer <access_token>" | jq .Success:
{ "client_cert_pem": "-----BEGIN CERTIFICATE-----\n...\n", "client_key_pem": "-----BEGIN PRIVATE KEY-----\n...\n", "ca_chain_pem": "-----BEGIN CERTIFICATE-----\n...\n", "server_fqdn": "tenant-acme.orlop.example", "expires_at": "2026-04-30T13:00:00Z"}The control plane:
- Authenticates the bearer access token.
- Confirms the tenant and user are active.
- Looks up or creates a
server_vmsrow via the placement scheduler (lazy allocation). - Mints a one-hour client certificate from the tenant intermediate.
- Records an
agent_enrollmentsaudit row with cert serial and expiry.
Retryable enrollment failures return 503 with Retry-After: 60, for
example when tenant CA material is unavailable or server placement is pending.
Request Flow
Section titled “Request Flow”orlop login -> POST /auth/device/code -> operator approves at /device -> POST /auth/device/token -> ~/.config/orlop/credentials.json
orlop mount -> refresh access token if needed -> POST /agent/enroll -> write cert.pem/key.pem/ca.pem under hosted.cert_dir -> GET https://<server_fqdn>/healthz with client cert -> mount remote backend over mTLS -> renew client cert before expiry while mountedLocal Development
Section titled “Local Development”Run Postgres, migrate, and start the control plane:
export DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/orlop_controlexport ORLOP_SECRETS_DIR=/tmp/orlop-control-secretsexport ORLOP_TRUST_DOMAIN=orlop.localexport ORLOP_ORG_NAME=ORL Dev
orlop-control migrate uporlop-control ca init --rootorlop-control ca init --tenant acmeorlop-controlSeed an admin session:
orlop-control user seed \ --tenant acme \ --email operator@acme.example \ --base-url http://127.0.0.1:8080For manual testing without enrollment, you can seed a server VM row ahead of time:
INSERT INTO server_vms (tenant_id, fqdn, status, provisioned_at)VALUES ('acme', 'tenant-acme.localhost', 'active', now())ON CONFLICT (tenant_id)DO UPDATE SET fqdn = EXCLUDED.fqdn, status = 'active', provisioned_at = now();Then run:
orlop login --control-plane http://127.0.0.1:8080For full-stack mTLS testing, you need an orlop-server certificate whose
name matches a server_vms.fqdn value and whose tls.client_ca_file points
to the tenant intermediate cert. (Server VM rows are created lazily on agent
enrollment; use the SQL above if pre-seeding is preferred.)