Skip to content

Authentication

Two credentials exist, for two shapes of caller:

  • Org API tokens — the REST APIs' credential: minted on the console, acts for the organization. Everything below describes them.
  • OAuth sign-in — the MCP server's credential: you sign in as yourself in a browser, your agent acts as you, and access follows your org membership (removing a member cuts their agent's access within seconds). No token to store.

Every REST API endpoint authenticates with an org API token, sent as a Bearer header:

curl -H "Authorization: Bearer $NC_TOKEN" \
  https://nationalcompute.com/api/vm/capacity

Minting a token

Tokens are minted on the console's API Keys page — sign in at nationalcompute.com, then API → Keys in the sidebar. Any member of your organization can mint one, and the secret is shown once, at mint. A token belongs to your organization and acts for it on every API: reading and declaring capacity, setting a limit price, swapping nodes, SSH keys, the price feed, and the billing records. There is one grant, not a set of permissions — the scopes a token echoes on /api/whoami are recorded at mint and describe that one grant; no route narrows a token by them.

At mint a token is bound to every cluster or to one — that is the binding the API resolves below. Tokens are revoked on the same page, instantly. Treat them like passwords: anyone holding the token can act as your organization.

A token stops working in four ways. It expires at the expires_at set at mint, at most a year out. A member revokes it on the API Keys page. The token revokes itself with DELETE /api/org/tokens/self. The platform revokes it when the member who minted it leaves the organization, or when the organization is retired. The last case writes the revoke into the token's activity trail under the actor system:offboard, so the remaining members see why it closed. Every case answers 401 unauthorized on the next call.

Token ↔ cluster resolution

A token may be bound to one cluster or org-wide:

  • A cluster-bound token addresses its own cluster; no cluster name is needed (passing one that matches is fine — only a contradicting one refuses). A token bound to a VM cluster is refused on the Kubernetes surfaces and vice versa.
  • An org-wide token resolves automatically when your org holds exactly one cluster of the relevant kind. If it holds several, pass cluster (query parameter on GETs, body field on writes) or you get a 409 ambiguous-cluster.

Discovering what a token can address

GET /api/whoami answers the bootstrap questions with the token itself — no cluster name required:

curl -H "Authorization: Bearer $NC_TOKEN" \
  https://nationalcompute.com/api/whoami
{
  "org_id": "8f2c…",
  "org_display": "acme",
  "org_kind": "standard",
  "scopes": ["capacity:read", "capacity:write"],
  "cluster": {"name": "acme-inference", "kind": "k8s",
              "gpu_vendor": "…", "gpu_model": "…",
              "market": true, "gpus_per_node": 8,
              "shared_volume": true, "cpu_worker": true},
  "clusters": [
    {"name": "acme-inference", "kind": "k8s",
     "gpu_vendor": "…", "gpu_model": "…",
     "market": true, "gpus_per_node": 8,
     "shared_volume": true, "cpu_worker": true},
    {"name": "acme-train", "kind": "vm",
     "gpu_vendor": "…", "gpu_model": "…",
     "market": true, "gpus_per_node": 8,
     "shared_volume": false, "cpu_worker": false}
  ]
}

org_display is your organization's display name, the segment in console URLs. org_kind is standard, or public_research for an organization created at sign-in for a .edu, .mil or .gov address. cluster is the token's binding (null for an org-wide token); clusters is every cluster in your org, both kinds, name-sorted. Each entry names the GPU class the cluster trades as gpu_model (null while the platform has not set one) and its maker as gpu_vendor. An agent then knows what it prices before its first limit price or declaration. Each entry also carries the facts the console gates its pages on: market (true = the cluster trades on the market, so a limit price or a capacity declaration applies; false = a fixed cluster the platform sizes, with no limit price to set; null = not known yet, the cluster record has not landed), gpus_per_node (the node to GPU conversion), shared_volume (true = a shared filesystem is bound to the cluster; null = the storage record could not be read) and cpu_worker (the cluster carries a CPU only node). A slurm entry adds its login endpoint as ssh_host, ssh_port, ssh_user and ssh_auth; the Slurm reads page states what each carries and when they read null. Any token may call it. Agents should call this first and only ask a human to choose when several clusters of the right kind exist — and a 409 ambiguous-cluster refusal lists the candidate clusters too (error reference).

Kubernetes service account tokens

A Kubernetes cluster issues its own credentials, separate from org API tokens: ServiceAccount tokens. An org API token acts for your organization on the platform API; a ServiceAccount token acts as one account inside one cluster, with exactly the RBAC you bind to it. Use ServiceAccount tokens for automation that talks to the cluster API and for workloads that prove their identity to outside services.

Minting one

A signed-in kubectl (see Connecting) mints a token for any ServiceAccount, with the lifetime and, when a relying party asks for one, the audience of your choice:

kubectl -n <namespace> create serviceaccount <name>
kubectl -n <namespace> create token <name> --duration=24h [--audience=<audience>]

The token is printed once and is not stored anywhere; mint another when it expires. Its subject is system:serviceaccount:<namespace>:<name>. Inside a pod the same account's token is already mounted as a projected volume and refreshed by the kubelet, so a workload never mints one.

Tokens from create token expire at the duration you ask for. Deleting the ServiceAccount invalidates every token minted for it at once. A token that must not expire is a legacy kubernetes.io/service-account-token Secret you create yourself; nothing rotates it, so prefer a short duration and a re-mint.

Using one against the cluster

The cluster API accepts a ServiceAccount token as a Bearer credential from anywhere the API is reachable. Three inputs are needed, all of them in the kubeconfig you downloaded: the server URL, the cluster CA certificate, and the token. No sign in, no kubeconfig plugin:

kubectl --server=https://<api-host>:<port> --certificate-authority=ca.pem \
        --token="$TOKEN" -n <namespace> get pods

What the account may do is the RBAC you bind. A fresh ServiceAccount can do nothing; a namespaced read-only binding is the usual first grant:

kubectl -n <namespace> create rolebinding <name>-view \
  --clusterrole=view --serviceaccount=<namespace>:<name>

Anything outside the binding is refused with 403 Forbidden, and a token that is expired, revoked, or malformed is refused with 401 Unauthorized.

The cluster as an OIDC issuer

Every cluster signs its ServiceAccount tokens as an OpenID Connect issuer that outside services can verify. The issuer is

https://nationalcompute.com/oidc/k8s/<cluster-id>

where <cluster-id> is the cluster's permanent id, not its name. Read it from the cluster itself:

kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

The issuer serves the two documents an OIDC relying party fetches, anonymously and over public TLS: /.well-known/openid-configuration and /openid/v1/jwks. The key set is the cluster's public signing keys and nothing else; no cluster name, address, or membership is exposed.

A service that accepts OIDC federation (Tailscale, AWS, GCP, Vault, and the like) is configured with the issuer URL, the token's subject and, where it asks for one, an audience. Mint the token with that audience (--audience), present it, and the service verifies it against the issuer without any shared secret. For Tailscale's workload identity federation, for example, the trust credential takes a custom issuer set to the URL above, the subject system:serviceaccount:<namespace>:<name> and an empty audience; the credential it returns is short-lived and scoped to what you granted.

Signing keys rotate. A rotation reaches the public key set within about 15 minutes (currently a 10 minute refresh plus a 5 minute cache), and both the old and the new key are published while tokens signed by either are alive. Tokens minted before a cluster gained its public issuer stay valid: the cluster accepts its former in-cluster issuer alongside the public one. Nothing about kubectl access changes.

Auth errors

Status Meaning
401 missing, malformed, expired, or revoked token, or a token whose organization was retired
403 session-required: a token on a console-only action (deleting a preserved volume, changing billing settings) — every token is refused there
404 no cluster of the right kind matches the token, or another organization's {org} in the path

Errors are plain JSON — see API conventions.

What needs no auth

The discovery surfaces are deliberately unauthenticated: the OpenAPI contracts (/api/vm/openapi.json, /api/k8s/openapi.json, /api/market/openapi.json, /api/billing/openapi.json), the agent orientation pages (/llms.txt, /AGENTS.md) and these docs — so a client can learn the wire before it holds a valid token. A program reads the docs under the apex too, nationalcompute.com/docs/ + the page path (/docs/authentication/ is this page), no token needed; a token that is sent must be valid, as on every route.