Stellar ControlMission control · by Stellar Systems v0.1.0

Security

NATS Accounts and Credentials

Decentralized JWT authentication: bootstrap identities, JWTs issued by the reconciler with the permissions of each binding, and credentials of follow-up for people.

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:

UserCredentialsPermissions
Services of the core (API, reconciler, executor…)Given by the deployment (nats.credentials)Those of the deployment
Drivers, transports, gateways, connectorsA bootstrap identity, then a JWT issued by the reconcilerDerived from their bindings
People following runs and alarmsExchanged against their OIDC tokenSubscriptions 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
  1. Bootstrap. The instance starts with a bootstrap identity (STELLAR_NATS_CREDENTIALS), limited to registration and heartbeats: publish stellar.ctl.register, stellar.ctl.hb.> and _INBOX.>, subscribe to _INBOX.> and stellar.ctl.rpc.>.
  2. Registration. It generates an nkey at start and registers with its public key. The private seed never leaves the instance.
  3. 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 credentials control verb. The instance reconnects with the JWT and its own seed.
  4. 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).
  5. 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:

KindPublishesSubscribes
DriverThe 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
Transportstellar.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 Tstellar.tm.raw.<gateway>.T, its wrap subject
Gatewaystellar.tm.raw.<instance>.T, stellar.tc.evt.T.>, stellar.metrics.<instance>.T, stellar.stream.T.>Its uplink subject
ConnectorRegistration 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:

YAML
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_file and account, 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.

YAML
nats:
  url: tls://nats.mcs.example.org:4222
  credentials: /run/secrets/services.creds

The 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:

Shell
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 from reconciler.signing_key_file and reconciler.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 answers 503 auth::no-signing-key; a key that is not a user nkey gives 400 auth::invalid-key.

Development setup#

dev/nats/setup.sh generates an operator stellar-dev, an account stellar with a signing key, and two users:

FileUse
dev/nats/secrets/creds/dev.credsFull access: developers, tests, the services of the core
dev/nats/secrets/creds/bootstrap.credsThe bootstrap identity of drivers, transports and gateways
dev/nats/secrets/stellar-account-signing.nkThe seed of the signing key, for reconciler.signing_key_file
dev/nats/secrets/resolver.confOperator, 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).

Shell
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-driver

See NATS and JetStream.

Stellar Control · v0.1.0

↑↓ to moveEnter to open