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 or is revoked.

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 Full access / Limited choice (Limited reveals the scope picker and a resource picker for workspaces / repos) and an expiry choice. The new token is shown once in a dialog that stays open until you dismiss it, so copy it first. 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, its member roster, its pending invites, CI-secret names and its self-hosted runners
workspace:write create and modify repos and workspace content, patch org metadata, add/remove/re-role members, mint and revoke invites, set/delete CI secrets, register a self-hosted runner, deregister one (another member’s needs the admin or owner role; your own also needs user:write), connect an org’s GitHub App installation to its repos (with repo:write on the repo), and archive an org — for which your role must be owner
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, your own self-hosted runners (list / deregister)
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.

Pick a lifetime when you create the token: 7 / 30 / 90 / 180 / 365 days (default 90), a custom date up to 400 days out, or No expiration.

A token with no expiration stays valid until you revoke it. It is an explicit choice, never the default: use it only for a long-lived integration that cannot be rotated on a schedule, and keep its scopes and resources as narrow as you can. A fixed expiry is recommended — a short-lived credential is the whole point of handing one to an automated process, and a leaked never-expiring token works until someone notices. Token lists show its expiry as Never.

Create tokens from the web settings page: the token-create API needs a signed-in browser session (with sudo), so a CLI bearer token cannot mint another token. neo token create exposes the same choices (--expires <days>, clamped to 400, or --no-expiry; not both) for when that changes. The API takes expires_in_days or no_expiry: true, and rejects a request that sends both. Tokens minted by neo auth login always get the 90-day default.

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 registry — docker 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.