# Tokens and secrets

> Who and what can act on a morca stack: UI logins and API tokens, worker tokens scoped to a pool, the per-machine boot token of an autoscaled pool, the per-run dista cluster token, pipeline secrets that specs name but never carry, SOPS on deploy, and where the Hetzner, AWS and registry credentials live.

[morca](https://morca.dev/)

[](https://morca.dev/search "Search")

Every credential in morca has one job and a short reach. Postgres holds only hashes of tokens and the names of secrets, a worker never holds database credentials, and a pipeline's spec names its secrets without carrying them.

## Overview

| Credential | Form | Held by | Lasts | Ended by |
| --- | --- | --- | --- | --- |
| UI session | orca\_session cookie, 32 random bytes | a user's browser | ORCA\_UI\_SESSION\_HOURS (12) | logout, password change, user removed |
| API token | orca\_ and 64 hex digits | orca login on a laptop | ORCA\_API\_TOKEN\_DAYS (30) | orca logout, orca user revoke |
| Worker token | orca\_ and 64 hex digits | one remote worker | \--days, at most ORCA\_WORKER\_TOKEN\_DAYS (365) | orca worker-token revoke |
| Boot token | a worker token | one autoscaled machine | 30 days | revoked when the machine is removed |
| Cluster token | DISTA\_TOKEN | one run start's dista processes | that run start | ends with the run start |

The server keeps only the SHA-256 of a session, an API token or a worker token, so a copy of the database doesn't let anyone in. The `orca_` prefix lets a secret scanner recognise a token that leaked.

## UI logins

Anyone who can reach the UI can read it. Its actions (start, backfill, resume, rerun, cancel, rebuild) need a logged-in user as soon as any user exists, or with `ORCA_UI_LOGIN=required` before one does, so a stack with users can't be left open by forgetting the variable.

```
orca user add alice          # reads the password from standard input
orca user tokens alice       # her API tokens
orca user revoke alice 3     # or @all
orca user remove alice       # and ends her sessions
```

- **Passwords** are bcrypt hashes made by Postgres's pgcrypto, so neither morca nor its logs see one after it is checked.
- **Sessions** are an `HttpOnly`, `SameSite=Strict` cookie, `Secure` with `ORCA_UI_SECURE_COOKIES=1` behind HTTPS.
- **Failed logins** are audited and counted: after 5 for one name, or 20 from one address, in 15 minutes, `/login` and `/api/login` answer 429 without checking the password. Behind your own proxy, `ORCA_UI_TRUST_PROXY=1` takes the address from `X-Forwarded-For`.
- **API tokens.** `orca login <url>` gets a token for the `orca` command on another machine, kept there mode 600, and sent as `Authorization: Bearer`. Changing a user's password or removing the user revokes all of her tokens.

Every action a user takes is a row of `orca.audit` ([The UI](https://morca.dev/docs/ui#login)).

## Worker tokens

```
orca worker-token create --pool bgent --label bgent   # prints the token, once
orca worker-token list
orca worker-token revoke 3
```

A worker token is scoped to one pool. It can claim that pool's runs and make the state calls of the runs it claimed, and nothing else; a user's token gets nothing from the worker API. There is no read-only or read-write variant. A revoked token stays listed as revoked, and the worker using it is refused and exits. Each claim is audited as `worker:<label>`.

A run on a worker gets the worker's token as `ORCA_STATE_TOKEN`, with `ORCA_STATE_URL`, in place of a database. A worker with `ORCA_DATABASE_URL` set refuses to start, and it needs only outbound HTTPS (`--remote` takes `http` only to its own machine), so Postgres stays reachable only on the server ([Remote workers](https://morca.dev/docs/workers#setup)).

## Boot tokens of autoscaled machines

A machine of a [pool morca grows](https://morca.dev/docs/pools#autoscaling) gets a worker token of its own, made for it and revoked when it is removed. Its startup script holds that token and no other secret. With it, the machine asks the UI's `/api/worker/boot` over TLS for the image, the registry login to pull it, and the bucket's variables. `/api/worker/boot` answers only a token the autoscaler made for a machine that is live: one from `orca worker-token create` gets a 403\.

The bucket's variables come from the UI's own environment, so on Hetzner a pool machine gets the same S3 keys as the server. On EC2, give the instances a profile whose role reaches the bucket (`ORCA_POOL_EC2_PROFILE`) instead. The machine keeps them in a file only root can read, and deletes the registry login's file as soon as it has read it.

## Cluster tokens

From 0.24.0, a run spread over several machines of a pool ([workers = N](https://morca.dev/docs/pools#one-run-many-machines)) gets a token for each start. The worker that starts the run makes it and gives it, as `DISTA_TOKEN`, to the run's dista head, its driver and its own dista worker, and reports it to the UI. A member gets it only in the answer to its claim, over the worker API's TLS. It is never in a log or a page.

dista checks it on every connection to a head or a worker's object server: a missing or wrong token closes the connection unanswered, and the comparison takes the same time wherever the bytes differ. So nothing else on the network can join the run, submit to it or fetch its results. It authenticates and doesn't encrypt, so keep the pool's machines on a private network, as an autoscaled Hetzner pool does ([dista's DESIGN.md](https://github.com/morca-dev/dista/blob/main/DESIGN.md)).

## Pipeline secrets

```
[pipelines.gharchive]
secrets = { GITHUB_TOKEN = "orca/github-token" }
```

A pipeline's `secrets` map an environment variable to a secret's ID, which starts with `orca/`. Postgres and the run's spec hold only the IDs, and a run's page shows its secrets by name only. orca.toml refuses an `env` name that looks like a secret (`*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*DATABASE_URL*`), so values don't end up there.

- **On EC2**, each instance reads them from Secrets Manager at start with its role, which may read only `orca/`.
- **With `ORCA_SECRET_FETCH`**, the scheduler runs that command with the ID and uses what it prints. On the Hetzner server it is `sh /etc/orca/secret-fetch`, which prints `/etc/orca/secrets/<id>` (mode 600, root only) and must refuse `..`.
- **On a remote worker**, the worker's own `ORCA_SECRET_FETCH` reads them there. The spec it claims names the secrets and never carries their values.

Each run gets `ORCA_SECRETS`, the names of its secrets, and morca masks their values in every log it writes.

## Secrets on deploy

On the Hetzner stack, `hetzner.env` holds no secret itself: it names the secret files. A file named `*.sops` or `*.sops.<ext>` is decrypted with `sops -d` on its way to the server and never written to a file on the laptop, so the laptop keeps one age key instead of plain tokens. `SECRETS_FILE` names one SOPS file with every secret, decrypted once per deploy, so a Secure Enclave key asks for Touch ID once. The server gets plain files under `/etc/orca`, mode 600\. Every deploy brings them in line and prints the names of what changed, never a value ([Settings and secrets](https://morca.dev/docs/deploy#settings), [hetzner.md](https://github.com/morca-dev/morca/blob/main/docs/hetzner.md#secrets-on-your-laptop-sops)).

## Cloud credentials

| Credential | Who uses it | Where it lives |
| --- | --- | --- |
| Hetzner API token (read-write) | the scheduler, to make and remove pool machines | ORCA\_HCLOUD\_TOKEN, or the secret ORCA\_HCLOUD\_TOKEN\_SECRET names; on the stack, HCLOUD\_POOL\_TOKEN\_FILE sends it to /etc/orca/secrets/orca/hcloud-token |
| AWS credentials | the scheduler, for EC2 pools and the EC2 runner | AWS\_REGION and credentials in its environment, or an instance role |
| Instance profile | EC2 pool machines and run instances, to reach the bucket | ORCA\_POOL\_EC2\_PROFILE, else ORCA\_EC2\_PROFILE |
| Registry login | the scheduler (to read the image's architectures) and booting machines (to pull it) | ORCA\_REGISTRY\_USER and the secret ORCA\_REGISTRY\_TOKEN\_SECRET names (orca/ghcr-token on the stack) |
| S3 keys | the server, workers, and autoscaled Hetzner machines | the stack's s3.env; a worker's worker.env |
| ntfy topic and token | the scheduler, for alerts | ORCA\_NOTIFY\_SECRET, ORCA\_NOTIFY\_TOKEN\_SECRET |

The Hetzner token is sent only as a bearer token and never put in an error, and the registry login is never in one either. The firewall each Hetzner pool machine gets lets in ssh from `ADMIN_IP` only ([hetzner.md](https://github.com/morca-dev/morca/blob/main/docs/hetzner.md#worker-pools-that-grow-themselves)).

[morca](https://morca.dev/)

Data pipelines in Mojo. Early, and built in the open.

### Project

[GitHub organisation](https://github.com/morca-dev) [Issues](https://github.com/morca-dev/morca/issues) [morca](https://github.com/morca-dev/morca) [dista](https://github.com/morca-dev/dista) [census](https://github.com/morca-dev/census)

### Built on

[magmalake.dev](https://magmalake.dev) [The Iceberg, Parquet and codec libraries](https://magmalake.dev/tins) [mojoshelf.org](https://mojoshelf.org) [The Mojo language](https://www.modular.com/mojo)

### Standards

[Apache Iceberg](https://iceberg.apache.org) [Apache Parquet](https://parquet.apache.org) [PostgreSQL](https://www.postgresql.org)

Copyright 2026 Marius Seritan. Every repository is licensed [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).

Apache Iceberg and Parquet are trademarks of the Apache Software Foundation. Mojo is a trademark of Modular Inc. This project is not affiliated with or endorsed by the ASF or Modular.
