sam-control-plane

sam-control-plane admits nodes, mints credentials, holds the mesh policy and tracks routers. It is an HTTP server over SQLite or PostgreSQL.

sam-control-plane [flags]                     run the server
sam-control-plane admin ban   --peer <id>     ban a node directly in the database
sam-control-plane admin unban --peer <id>     lift a ban

Flags

FlagDefaultMeaning
--issuerrequiredOIDC issuer URL(s), comma-separated. The first is advertised to enrolling nodes on /info. Can also be set with SAM_OIDC_ISSUER.
--allowed-audiencessam-mesh-audienceAudiences accepted in OIDC tokens, comma-separated.
--oidc-client-idfirst audienceOAuth client ID advertised on /info, for providers where it differs from the audience.
--insecure-skip-tls-verifyfalseSkip TLS verification when fetching issuer metadata and keys. For a cluster issuer served with the cluster CA, or a local development issuer.
--bind-address0.0.0.0:8080HTTP listen address.
--db-driversqlitesqlite or postgres.
--db-dsncontrol-plane.dbDatabase DSN. A PostgreSQL DSN contains a password, so the next two options are preferred for PostgreSQL.
--db-dsn-pathFile containing the DSN. Overrides --db-dsn. Can also be set with SAM_DB_DSN.
--admin-token-pathFile containing the bearer token for /admin/* and POST /policies. Can also be set with SAM_ADMIN_TOKEN. Without a token, the admin API cannot be used.
--auto-approve-enrollmentfalseIssue credentials for valid bootstrap-token enrollments immediately instead of queueing them for approval.
--biscuit-ttl24hLifetime of each credential. If the OIDC token expires sooner, the credential expires with it.
--oidc-session-ttl2160h (90 days)How long an OIDC enrollment may keep refreshing before the identity must log in again.
--key-rotation-interval24hHow often a new signing key is generated. 0 disables rotation.
--key-grace-period1hHow long a rotated-out key stays accepted. Credentials signed by a retired key cannot be verified or refreshed. Nodes and routers must pull /keys well within this window (sam-node --control-plane-sync-interval, sam-router --keys-sync-interval).
--lease-duration15mHow long a router lease lasts without renewal.
--node-retention720h (30 days)How long the record of an enrolled node is kept after its session expires. Banned nodes are kept forever. 0 keeps every record.
--mesh-reconnect-interval30sHow often the event publisher re-reads the router leases and dials any router it is not connected to.
--log-levelinfodebug, info, warn, error. LOG_FORMAT=json selects JSON output.

HTTP API

Two encodings are in use. Routes that mesh components call use protobuf (application/x-protobuf), with messages defined in api/sam.proto. Routes for operators and the console use JSON. POST /policies accepts either.

Responses that carry credentials are sent with Cache-Control: no-store.

Unauthenticated

RoutePurpose
GET /healthzLiveness.
GET /readyzReadiness. Returns 503 when the database is unreachable.
GET /metricsPrometheus metrics.
GET /infoControlPlaneInfoResponse: the OIDC issuer, client ID and audience, the addresses of routers with live leases, and the list of banned peer IDs. This is what a node needs before it can enroll.
GET /keysThe current set of signing public keys, signed by every key in the set. A caller accepts the set only if one signature verifies under a key it already trusts.

Enrollment and refresh

Every request below carries a timestamp and a challenge_signature. The enrollee signs sam:<endpoint>:<peer_id>:<unix-millis> with its key, and the control plane accepts the signature within five minutes. This proves that the caller holds the key behind the peer ID it names.

RouteBodyPurpose
POST /registerEnrollRequest (OIDC token, public key, requested role, labels)OIDC enrollment. Returns EnrollResponse: the credential, the control plane public key, router addresses and expiry.
POST /enrollBootstrapEnrollRequest (bootstrap token, public key, requested role, labels)Bootstrap enrollment. Returns BootstrapEnrollResponse with status APPROVED and the credential, or with status PENDING. For a peer that is already approved, a new credential is minted directly.
GET /enroll/status?peer_id=headers X-Sam-Challenge-Ts, X-Sam-Challenge-SigPoll a pending enrollment. Any authentication failure answers 401, so the credential is released only to the enrollee.
POST /refreshTokenRefreshRequest, current credential as Authorization: Bearer <base64>Exchange a credential for a new one. Refuses a replayed (superseded) credential, a banned node, an expired session, and a credential signed by a retired key unless the node has autonomous_recovery.
POST /routers/leaseRouterLeaseRequest (credential, addresses, telemetry)Register or renew a router lease. Requires role("sam:role:router"). Announced addresses must end in the router’s own peer ID.
GET /policiescredential as Authorization: Bearer <base64>The mesh policy as PolicyConfigGetResponse. Also accepts the admin token.
POST /nodes/catalogNodeCatalogReport, credential as bearerA node’s report of the services it publishes, for the console. Display only. Never used for authorization.

Admin

All routes require Authorization: Bearer <admin-token> and use JSON.

RoutePurpose
GET /admin/statusEverything the console shows: routers, nodes, enrollment requests, tokens, users, and the policy as JSON.
POST /policies, PUT /policiesReplace the mesh policy. The JSON body is protojson of PolicyConfigUpdateRequest. Unknown fields are rejected, so a misspelt grant fails instead of being dropped.
POST /admin/bootstrap-tokensMint a token. Body: role (required), ttl_hours (default 24), max_usages (default 1), description, autonomous_recovery (default false). Returns 201 with id, token (shown once), role, expires_at.
DELETE /admin/bootstrap-tokens/{id}Revoke a token. Can be repeated. The token stays in the list, marked as revoked.
GET /admin/enrollmentsPending bootstrap enrollment requests.
POST /admin/enrollments/{id}/approveApprove. Spends a token usage. Checks the token again, and checks the labels against the role’s allowed_labels.
POST /admin/enrollments/{id}/rejectReject.
POST /admin/revokeBody {"peer_id": "..."}. Ban the node and, for an OIDC enrollment, the identity behind it.
POST /admin/nodes/{peer_id}/unbanLift both bans.
POST /admin/nodes/{peer_id}/autonomous-recoveryBody {"enabled": true}. Toggle the flag on an enrolled node.

User

For an OIDC user acting on their own nodes, authenticated with an ID token from the configured issuer. The console uses these routes.

RoutePurpose
GET /user/statusThe caller’s enrolled nodes and tokens.
POST /user/bootstrap-tokensMint a token owned by the caller. role defaults to sam:role:node. Banning the owner disables the tokens they minted.
POST /user/revokeBan one of the caller’s own nodes.

Notes

  • A new control plane grants nothing. With no policy in the database, enrollment succeeds only for identities bound to the requested role, and with no bindings there are none. A policy must be posted first.
  • Bootstrap tokens are spent atomically. max_usages holds under concurrent enrollments, and a pending request is resolved exactly once.
  • An approved peer that enrolls again gets a new credential without approval, as long as the token is valid, the role matches the record and the peer is not banned. This is how a bootstrap node recovers from a retired signing key.
  • Bans propagate in two ways. /info lists banned peer IDs for nodes and routers that start or restart. A signed mesh event reaches the ones that are already running.
  • Node records are garbage-collected. A node whose session has expired is deleted after --node-retention. A node without a persistent data directory enrolls a new identity on every restart, so without this the node table would only grow.

Metrics

/metrics exposes sam_control_plane_* counters and gauges: enrolled nodes by role and state, active routers, mesh-connected peers as reported by router leases, per-route request counts and latencies, and the Go runtime.