# Give a script or agent access

> Add access that signs in with a private key, use its key file from the CLI, scripts and your own code, and rotate or revoke it

> Nevia is in beta and open to selected teams. Public launch in Dec 2026. The CLI installs with `curl -fsSL https://get.nevia.cloud | sh`, and the MCP server is at https://api.nevia.cloud/mcp. A person signs in once in the browser.
> Setup, sign-in and requesting access: [SKILL.md](https://nevia.cloud/SKILL.md). When Nevia fits and when it doesn't: [why.md](https://nevia.cloud/why.md).

> For the complete documentation index, see [llms.txt](https://nevia.cloud/llms.txt). This page's HTML version: https://nevia.cloud/docs/workspaces/private-key-auth

A script, CI job or agent acts in a workspace as its own principal. It holds the role `editor` or
`viewer` and signs in with a private key instead of a login. The key stays on the system that holds the
key file and never travels.

You need the [CLI installed and signed in](https://nevia.cloud/docs/get-started/quickstart.md) and the workspace's `admin` role.

## Add access that signs in with a private key

Add a short description so the access list shows what the key is for:

```bash
nevia access add --private-key --description "deploys from CI"
```

The key file is saved as `~/.config/nevia/keys/<key>.json`, next to the CLI's own login, readable only by
you. To save it elsewhere, pass a folder or file to `--out`, or pass `--out -` to print it:

```bash
nevia access add --private-key --description "deploys from CI" --out ./secrets
```

Nevia keeps no copy of the private key, so this file is the only one. Store it where your automation reads
secrets, such as a CI secret, and delete the local copy. If you lose it, issue a new key with `keys rotate`.

A private key defaults to `editor`, which can create and manage computers and databases. A login added without
a role starts as a `viewer`. Pass `--role viewer` for read-only access. A private key is never an admin; see
[Workspace permissions](https://nevia.cloud/docs/reference/workspace-permissions.md).

The console manages the same access. Open the workspace's **Settings**, where one **Access** table lists logins
and private keys. **Add access** creates a private key and downloads its key file.

## Use it from the CLI

Point `NEVIA_KEY_FILE` at the file, and every command runs as that principal:

```bash
export NEVIA_KEY_FILE=~/.config/nevia/keys/<key>.json
nevia whoami
nevia ls
```

`whoami` shows the principal, `(private key)`, and the one workspace it belongs to. The CLI signs in with the key
and holds a short-lived access token in memory for the process. It signs in again when the token expires. Nothing
is written to disk.

A key file outranks a saved `nevia login`. Unset `NEVIA_KEY_FILE` to act as yourself again. To use the key file
without setting the variable each time, save its path:

```bash
nevia auth use-key-file ~/.config/nevia/keys/<key>.json --local    # this project: writes key_file into .nevia.toml
nevia auth use-key-file ~/.config/nevia/keys/<key>.json            # everywhere: writes it into your config
nevia auth use-key-file --unset                                     # back to your own login
```

Only the path is saved. The key stays in its file. `nevia whoami` and `nevia config` show which identity is in
effect, and `NEVIA_KEY_FILE` still wins in the current shell.

## Use it from scripts and your own code

For curl or another HTTP client, have the CLI produce the token:

```bash
export NEVIA_KEY_FILE=~/.config/nevia/keys/<key>.json
TOKEN=$(nevia auth access-token)
curl https://api.nevia.cloud/v1/me -H "Authorization: Bearer $TOKEN"
```

The token lasts 15 minutes. Run the command again for a new one. Routes are in the
[API reference](https://nevia.cloud/docs/reference/api.md).

Without the CLI, your code repeats what the CLI does: sign a short JWT with the key file's private key and post it
to the file's `token_url` for an access token. The file holds everything this takes: `subject`, `key_id`,
`audience`, `token_url` and `private_key`, plus `principal`, the ID that `nevia access` uses for the key's
principal. In Node, using only the standard library:

```js
import { readFileSync } from "node:fs";
import { randomUUID, sign } from "node:crypto";

const key = JSON.parse(readFileSync(process.env.NEVIA_KEY_FILE, "utf8"));
const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");

const now = Math.floor(Date.now() / 1000);
const header = encode({ alg: "RS256", typ: "JWT", kid: key.key_id });
const payload = encode({
  iss: key.subject,
  sub: key.subject,
  aud: key.audience,
  iat: now,
  exp: now + 60,
  jti: randomUUID(),
});

const input = `${header}.${payload}`;
const signature = sign("RSA-SHA256", Buffer.from(input), key.private_key).toString("base64url");

const response = await fetch(key.token_url, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ assertion: `${input}.${signature}` }),
});

const { access_token } = await response.json();
```

Send `access_token` as the Bearer token on every call. Any JWT library can do the signing in a few lines. Sign
in again when a request returns `401`.

Reuse the token for its 15 minutes instead of exchanging before every call. The token route allows 30 exchanges
a minute per client address and returns `429` with a `Retry-After` header beyond that. A refused exchange is a
plain `401` with no reason. Either the key file is wrong, or its key was revoked, expired or replaced.

## List, change and remove access

Logins and private keys share one list, which also shows how each signs in:

```bash
nevia access ls
nevia access set <principal> --role viewer
nevia access rm <principal>
```

Removing private-key access deletes the principal and its key, and its access tokens stop working at once. A
role change applies from the principal's next request.

## Rotate the key

A principal has one key at a time, addressed through the principal, so there is no key id to type. Rotating
issues the next key and revokes the current one in the same step, so there is never a moment with two keys or
with none:

```bash
nevia access keys rotate <principal>
```

Keys expire 90 days after creation unless you pass `--expires-in`, which accepts `15m` to `90d`. `nevia access ls`
shows each principal's key state in the STATUS column, `active`, `expired` or `revoked`, and
`nevia access get <principal>` adds the expiry.

Rotating or revoking takes effect at once. The old key can no longer sign in, and every access token the principal
holds stops working on its next request. With the new key file, the CLI signs in again on its own, and your own
code should sign in again on a `401`. To cut off access without replacing the key, revoke it. The principal keeps
its role but cannot sign in until the next `keys rotate`:

```bash
nevia access keys rm <principal>
```

## Bring your own key

If keys must be generated on your own systems, pass the public half and keep the private key:

```bash
openssl genrsa -out ci.pem 2048 && openssl rsa -in ci.pem -pubout -out ci.pub
nevia access add --private-key --own-key ci.pub      # the .pub, never ci.pem: the CLI and the API refuse a private key here
```

The resulting key file has no private key. Add yours as `private_key` before pointing `NEVIA_KEY_FILE` at it.
Nevia accepts RSA keys of 2048 bits or more.
