Skip to content

Authentication and tokens

An access token lets a script, agent, or CI consumer act against neosource over the API, git-over-HTTPS, or the container registry without a browser session. Every token is fine-grained and expiring by default: it carries a set of scopes and an optional resource restriction, and it can only ever do less than the account that created it.

A token is an attenuated bearer credential. Authorization for a token-backed request is the intersection of the two:

effective = account-permissions ∩ token-grant

The account’s normal access — workspace role, repo visibility, and per-repo grants — sets the ceiling; the token’s grant narrows it. A token can never widen its owner’s access, and a leaked token exposes only the slice of authority you granted it, only until it expires.

Tokens are stored hashed at rest. The plaintext you copy once at creation has the shape prefix_payload_checksum (a CRC32 check segment, GitHub-style, so secret scanners can validate a leaked candidate offline). The prefix tells you where a token came from:

prefix kind
nsp_ personal access token — owned by your account
nso_ workspace / org access token — owned by a service account
nsc_ CI job token — minted automatically per job, not by you

Create tokens in the web UI at /settings/developer, which offers a scope picker, a resource picker (workspaces / repos), and an expiry choice. The same page manages your SSH keys.

Creating or rotating a token is a sensitive action, so it is sudo-gated (see Rotating & revoking) — and a token can never mint another token.

A scope is a colon-separated permission. Scopes do not nest, except through an explicit *. A held scope matches a requested permission only when every segment is equal and both have the same number of segments — or when the held scope reaches a *, which matches everything below it.

The broad repo: scopes are the one deliberate exception to that rule:

The v1 grantable catalog. “Covers” means exactly what the scope authorizes — nothing more, and no parent/child inheritance between rows:

scope covers
repo:read read repo metadata over the API, and git clone / git fetch
repo:write write repo content over the API, and git push / git fetch. Not repo:read
repo:admin repo settings, grants, webhooks, and git push / git fetch. Not read or write
repo:* all of the above
repo:registry:read pull images from a repo’s container registry namespace
repo:registry:write push images to it. Not delete, and not git push
repo:registry:delete delete manifests and tags. Not implied by repo:registry:write
workspace:read list a workspace’s repos, read its settings and CI-secret names
workspace:write create and modify repos and workspace content, set/delete CI secrets, register a self-hosted runner
workspace:registry:read pull any of the workspace’s images
workspace:registry:write push any of them. Again not delete, and not git push
workspace:* full workspace administration
team:read / team:write read / manage teams and their rosters
user:read / user:write profile, SSH-key list/manage, notifications
user:* all user-owned surfaces
* everything the owner can do

Grant the narrowest set that gets the job done. An agent that only pushes to one repo wants repo:write restricted to that repo (see below) — not repo:*.

site:admin also exists in the grammar, but it is deliberately absent from the scope picker — the site-admin control plane lives on its own origin and is not reachable with a token minted here.

Leaving the resource picker empty makes the token unrestricted — it can reach any workspace or repo its owner can, within the granted scopes. Selecting specific workspaces or repos locks the token to them: a request against anything outside the list is denied.

Both gates apply, scope first and resource lock second. A repo-locked token is refused a repo outside its lock even when that repo is public, so the lock is a real boundary rather than a filter over what the account can already see.

Every token expires. The choices are 30 / 90 / 180 / 365 days (default 90), with a hard cap of 400 days — there is deliberately no “never”. A short-lived credential is the whole point of handing one to an automated process.

Rotate issues a new secret for the same token — the token id, scopes, resources, and label stay put, but the old secret dies immediately. Rotation cannot be performed by the token itself, so a leaked token can’t quietly extend its own life. Update the consumer with the new secret.

Rotation and creation are sensitive actions and normally require a fresh (sudo) session; an account that has no step-up credential set up yet (no password, enabled TOTP, or passkey) is allowed through on its first credential instead.

Revoke kills a token outright. Revocation and rotation take effect within seconds across all nodes.

Organizations need keys that aren’t tied to any one person’s account — CI for an org repo, a shared deploy bot, an import job. A workspace token is owned by a service account: an identity that has no password and can never log in, holding a membership in the workspace at a role no higher than the creator’s and a token resource-locked to that workspace.

Create one under Organization settings → Tokens. The minted secret carries the nso_ prefix. Because the service account is a normal workspace member under the hood, the token flows through the same authz, membership, and audit machinery as a human’s access — it just can’t exceed the workspace, its role, or its scopes.

Tokens with the nsc_ prefix are minted automatically for each workflow job and scoped to that job’s repo for its lifetime. They are a separate mechanism from personal and workspace tokens — you don’t create or manage them, and this page’s create/rotate flows don’t apply to them.

  • Getting started — using a token to push over HTTPS, and adding an SSH key.
  • Container registrydocker login, and which registry scope a publish credential should hold.
  • The neosource access model (workspace role → repo visibility → per-repo grants) — how account permissions are resolved before the token intersection applies.