Stellar Control relies on the decentralized JWT authentication of NATS. The operator of the platform signs one account per cell; each user of the account connects with a JWT, signed by a signing key of the account, which lists exactly the subjects it may publish and subscribe to. The NATS server enforces them.
Three kinds of NATS users exist:
| User | Credentials | Permissions |
|---|---|---|
| Services of the core (API, reconciler, executor…) | Given by the deployment (nats.credentials) | Those of the deployment |
| Drivers, transports, gateways, connectors | A bootstrap identity, then a JWT issued by the reconciler | Derived from their bindings |
| People following runs and alarms | Exchanged against their OIDC token | Subscriptions to run events and alarm transitions only |
Components outside the core#
A driver, transport, gateway or connector runs at a bench, a ground station or a partner: the MCS does not trust it with more than its own targets.
sequenceDiagram
participant C as Driver / gateway
participant N as NATS
participant R as Reconciler (leader)
C->>N: connect with the bootstrap credentials
C->>R: stellar.ctl.register (with a fresh nkey public key)
R-->>C: accepted
R->>R: bind the instance to links
R->>C: bindings verb
R->>C: credentials verb (user JWT for its nkey)
C->>N: reconnect with the JWT and its own seed
loop at half-life, or when its bindings change
R->>C: credentials verb (new JWT)
end- Bootstrap. The instance starts with a bootstrap identity (
STELLAR_NATS_CREDENTIALS), limited to registration and heartbeats: publishstellar.ctl.register,stellar.ctl.hb.>and_INBOX.>, subscribe to_INBOX.>andstellar.ctl.rpc.>. - Registration. It generates an nkey at start and registers with its public key. The private seed never leaves the instance.
- Binding. Once the reconciler binds it to links, it issues a user JWT for that public key,
carrying exactly the permissions of its bindings, and delivers it through the
credentialscontrol verb. The instance reconnects with the JWT and its own seed. - Renewal. The JWT lasts
reconciler.jwt_ttl(10 min by default). The reconciler issues a new one at half-life while the instance stays bound, at once when its permissions change, and again after a restart of the instance (which has a new key). - Unbinding. An instance no longer bound is not renewed: it loses its rights when its JWT expires. Immediate revocation will come with rights per target.
Permissions by kind#
Every bound instance keeps: registration, its heartbeat subject, its control verbs
(stellar.ctl.rpc.<kind>.<instance>.>), reply inboxes, and the consumption of the
TC_COMMANDS stream. Then, for each target T of its bindings:
| Kind | Publishes | Subscribes |
|---|---|---|
| Driver | The uplink subject of its gateway (or the wrap subject of its transport), stellar.tc.evt.T.>, stellar.tm.decoded.T, stellar.file.chunk.T.> | Its encode subject, stellar.tm.raw.*.T (or the unit subject of its transport), stellar.file.decode.T |
| Transport | stellar.tc.uplink.<gateway>.T, its unit subject, stellar.tc.evt.T.>, stellar.tm.decoded.T (measures of the link component), the keys of stellar_cop1 for T | stellar.tm.raw.<gateway>.T, its wrap subject |
| Gateway | stellar.tm.raw.<instance>.T, stellar.tc.evt.T.>, stellar.metrics.<instance>.T, stellar.stream.T.> | Its uplink subject |
| Connector | Registration and heartbeat, the pulls and acknowledgements of its own consumers connector_<name>_<data> | Its control verbs |
A driver also reads the object stores stellar_files (to decode transferred files) and
stellar_uploads (for its CFDP entity); a connector of files reads stellar_files. Objects of
these stores have base64-encoded names, which permissions cannot narrow to one target.
A gateway may only publish raw telemetry of its own targets under its own name, and may only uplink to the targets it is bound to: a compromised bench gateway cannot command another target.
The signing key#
The reconciler holds a signing key of the account, not its identity key:
reconciler:
signing_key_file: /run/secrets/stellar-account-signing.nk # the seed
account: ACCOUNT_PUBLIC_KEY # the account it signs for- The reconciler refuses to start with a key file readable by users other than its owner
(
chmod 600), and never logs it. - Without
signing_key_fileandaccount, no JWT is issued: fine for a development server without authentication, where every component uses the connection it has. - In a cell, the key lives in the Nomad variable
nomad/jobs/cell-<name>of the cell, never in a repository (see TLS, Encryption and Secrets).
Services of the core#
The API, reconciler, executor, compute stage, alarm service, scheduler, transfer manager and
editor connect with the credentials file of nats.credentials (JWT and seed), given by the
deployment. In a cell of stellar-admin, that is the services user of the account.
nats:
url: tls://nats.mcs.example.org:4222
credentials: /run/secrets/services.credsThe CLI commands that talk to NATS directly (stellar compile --publish, stellar sim) take
--nats-credentials or STELLAR_NATS_CREDENTIALS.
Credentials of follow-up#
People may follow runs and alarms directly on NATS, for instance from a script, rather than through the WebSockets of the API. They exchange their OIDC token for NATS credentials:
export STELLAR_TOKEN=… # an OIDC token
stellar auth nats-creds follow.creds
nats --creds follow.creds sub 'stellar.alarm.evt.>'stellar auth nats-creds <file> generates a user nkey, sends its public key to
POST /v1/auth/nats with the token, and writes the JWT and the seed as a credentials file
readable by its owner only.
- The JWT lasts
auth.nats_ttl(1 h by default) and is signed with the same account signing key, which the API reads fromreconciler.signing_key_fileandreconciler.account. - It may only subscribe to
stellar.run.evt.>,stellar.alarm.evt.>and_INBOX.>, and publish nothing. - Only a checked OIDC token is accepted (
401 auth::jwt-required); without a signing key, the API answers503 auth::no-signing-key; a key that is not a user nkey gives400 auth::invalid-key.
Development setup#
dev/nats/setup.sh generates an operator stellar-dev, an account stellar with a signing key,
and two users:
| File | Use |
|---|---|
dev/nats/secrets/creds/dev.creds | Full access: developers, tests, the services of the core |
dev/nats/secrets/creds/bootstrap.creds | The bootstrap identity of drivers, transports and gateways |
dev/nats/secrets/stellar-account-signing.nk | The seed of the signing key, for reconciler.signing_key_file |
dev/nats/secrets/resolver.conf | Operator, system account and resolver, included by the server configuration |
reconciler.account is the public key of the account stellar (it starts with A; nsc
shows it, in the nats-box image).
dev/nats/setup.sh && docker compose up -d nats
ACCOUNT=A… # public key of the account `stellar`
STELLAR__NATS__CREDENTIALS=dev/nats/secrets/creds/dev.creds \
STELLAR__RECONCILER__SIGNING_KEY_FILE=dev/nats/secrets/stellar-account-signing.nk \
STELLAR__RECONCILER__ACCOUNT=$ACCOUNT \
stellar-reconciler
STELLAR_NATS_CREDENTIALS=dev/nats/secrets/creds/bootstrap.creds my-driverSee NATS and JetStream.