Stellar ControlMission control · by Stellar Systems v0.1.0

Security

Identity and Roles

OIDC tokens, declared identities and roles, what each environment accepts, and the development identity provider.

People and services act on the MCS through the API (and the web editor), which needs to know who they are: to trace who launched a run or confirmed a hazardous telecommand, and to check that a supervisor is a supervisor. Stellar Control accepts two kinds of identity:

  • an OIDC token, a JWT issued by an identity provider and checked by its signature;
  • a declared identity, a name and roles the caller states, trusted without proof.

The environment of the target decides which one is enough, so that a bench in AIT runs without any identity provider while operations in orbit require tokens.

Presenting an identity#

WayHTTPCLIWeb console
OIDC tokenAuthorization: Bearer <jwt>stellar login, or --token / STELLAR_TOKENSign-in at the provider, or the OIDC token field of the identity panel
Declared identityX-Stellar-User: <name>--as (default $USER, else operator)Operator (declared) field
Declared rolesX-Stellar-Role: operator,supervisor--role or STELLAR_ROLERoles (declared) field

When a request carries a token, the token decides: a refused token is an error, never a fallback to the declared identity. The CLI always sends X-Stellar-User, and a token too when one is given. The web console keeps these settings in the local storage of the browser.

What each environment accepts#

The rule is set by the human_orchestration of the environment (see Environments and Policies):

EnvironmentAcceptedRefused with
human_orchestration: offAnyone, anonymous included—
identity: declaredA declared identity, or a checked token401 api::anonymous without identity
identity: jwt (the default)A checked token only401 auth::jwt-required for a declared identity, 401 api::anonymous without identity

The rule applies, in the environment of the run, to its launch, to answers to its questions and decisions, and to its suspension, resumption and abortion; also to the creation of a schedule and to its validation by a supervisor.

Some requests need an identity whatever the environment: commands on alarms (acknowledge, shelve, unshelve), answers and commands on runs (400 api::anonymous without one). Pass feeders are recognised by their identity, token or declared, against passes.feeders (see Writing a Pass Feeder).

Roles#

Roles are free strings carried by the token or declared. The MCS gives meaning to two of them:

RoleWhere
operatorConfirms hazardous telecommands, first (hazardous_confirmation)
supervisorConfirms hazardous telecommands after the operator; validates the plan of a schedule (POST /v1/schedules/{id}/validate, 403 schedule::not-supervisor otherwise)

An environment lists the roles that confirm a hazardous telecommand, always starting with operator. In in_orbit, the operator and the supervisor must be two distinct authenticated people: the executor requires a different sub. See Hazardous Confirmations.

OIDC tokens#

The MCS checks tokens against the provider's key set, read from its jwks_uri or from a file.

YAML
auth:
  jwks_url: https://idp.example.org/realms/ops/protocol/openid-connect/certs
  # or jwks_file: /etc/stellar/jwks.json   # the file the jwks_uri of the provider serves
  issuer: https://idp.example.org/realms/ops
  audience: stellar-mcs
  roles_claim: realm_access.roles

With auth.jwks_url, the services read the key set at start (a provider out of reach stops them) and again every hour, so that keys rotated by the provider are taken; a failed read keeps the keys already known and is logged.

A token is accepted when:

  1. it is a JWT (three base64url parts);
  2. its signature verifies with a key of the set: RS256, RS384 or RS512 (RSA key), ES256 (EC key on P-256), ES384 (EC key on P-384, that of Logto) or EdDSA (OKP key on Ed25519); when both the token and the key carry a kid, they must match;
  3. exp is present and not past, and nbf, when present, is reached, both with 30 s of leeway;
  4. iss equals auth.issuer, when it is set;
  5. aud equals auth.audience or, as a list, contains it, when it is set;
  6. sub is present and not empty.

The roles are read at auth.roles_claim, a dotted path in the claims (roles by default, realm_access.roles for Keycloak): a list of strings, a string of roles separated by spaces (the scope of an OAuth access token), or no roles when absent. With auth.roles_prefix, only the roles with that prefix are those of the MCS, the prefix taken off: a provider shared with other applications keeps its own. With roles_claim: scope and roles_prefix: "stellar:", the scope openid stellar:operator billing:read gives the role operator.

The API, the executor and the editor load the same key set. Without auth.jwks_url or auth.jwks_file, tokens are refused (401 auth::no-provider) and only declared identities work; the API logs a warning at start.

A token for everything#

With auth.required: true, the API refuses every request without a checked token, whatever its environment: declared identities and anonymous requests get 401 auth::token-required. Only GET /v1/auth/config (how to sign in) and GET /v1/openapi.json stay open, and the files of the web console. A browser cannot give a header to a WebSocket: its token is then the token parameter of its URL. The cells of the online trial run so, each open to the members of its organization only.

Signing in the web console#

With auth.issuer and auth.client_id, the web console signs its users in by itself. It asks GET /v1/auth/config (open to anyone), which names the provider, the client of the console and, for Logto, the organization of the cell and the scopes to ask:

YAML
auth:
  issuer: https://auth.example.org/oidc
  jwks_url: https://auth.example.org/oidc/jwks
  audience: urn:logto:organization:<organization>   # the tokens of that organization only
  roles_claim: scope
  roles_prefix: "stellar:"
  required: true
  client_id: <the single-page application of the consoles>
  organization: <organization>

The console is a public client (authorization code with PKCE, S256), coming back to the root of its own address, which must be among the redirect URIs of the client. It asks offline_access, the organizations (urn:logto:scope:organizations) and stellar:operator stellar:supervisor, then exchanges its refresh token for the token of the organization (organization_id): the one the API takes, its roles the permissions of the user in that organization. That token stays in memory, renewed a minute before it expires; the refresh token stays in the tab (sessionStorage), never in the URL nor kept by the browser beyond the tab. The WebSockets carry it as their token parameter. Sign out signs out at the provider too. A token of another organization is refused by the audience (401 auth::invalid-token).

Without a provider named, the console works as before: a declared identity or a pasted token.

Signing in the CLI: stellar login#

With auth.cli_client_id too, the CLI signs in by the device authorization grant (RFC 8628), which needs no browser on the machine of the CLI:

Shell
stellar login --api https://ada-1.control.example.org
# Open https://auth.example.org/oidc/device?user_code=WXYZ-1234
# and check that it shows the code WXYZ-1234
stellar run runs/first-light.yaml --api https://ada-1.control.example.org
stellar logout --api https://ada-1.control.example.org
  • The client is a public native application with device flow (customClientMetadata.isDeviceFlow at Logto), named in auth.cli_client_id and given by GET /v1/auth/config. One serves every cell: the token of the organization keeps them apart.
  • The token. The CLI asks the same scopes as the console (offline_access, the organizations, the roles of the MCS), polls the provider while the user approves, then exchanges the refresh token for the token of the organization of the cell.
  • The session is kept per API URL in ~/.config/stellar/credentials ($XDG_CONFIG_HOME, or STELLAR_CREDENTIALS), created readable by its owner only (0600). Every command to that API uses it when no --token is given, renewed a minute before it expires with the refresh token, which the provider may rotate. A long stellar watch taken up again after a cut gets a renewed token.
  • stellar logout forgets the session of the API.

For a machine (CI), pass a token with --token or STELLAR_TOKEN.

Errors#

CodeStatusWhy
auth::invalid-token401Not a JWT, unknown key or algorithm, invalid signature, expired, not valid yet, wrong issuer or audience, no subject (the message says which)
auth::no-provider401A token, but no auth.jwks_url nor auth.jwks_file
auth::jwt-required401The environment requires a token
auth::token-required401auth.required: the API requires a token for every request
api::anonymous400 or 401No identity where one is needed

Answers checked twice#

An answer to a question of a run goes from the API to the executor with the identity, its roles and, for a checked identity, the token itself. With identity: jwt, the executor verifies the token again (signature, expiry, sub equal to the author, roles of the token) before taking the answer. An answer that does not fit (wrong role, a person who already confirmed the step, a token refused) is logged in the run as answer_refused, with the author and the reason, and the run keeps waiting.

Development identity provider#

Benches and tests often have no identity provider. The CLI makes one, with an Ed25519 key:

Shell
stellar auth dev-key dev-idp.seed --jwks jwks.json     # a new key; its JWK set for auth.jwks_file
stellar auth dev-token --key dev-idp.seed --sub alice --role operator
stellar auth dev-token --key dev-idp.seed --sub bob --role operator --role supervisor \
    --issuer https://dev.local --audience stellar-mcs --ttl 3600
  • dev-key <seed> writes a new seed (hexadecimal, readable by its owner only) and prints the JWK set (key id stellar-dev, algorithm EdDSA), or writes it to --jwks.
  • dev-token prints a token signed with the key, with sub, roles, iat, exp (now plus --ttl, 8 h by default) and, when given, iss and aud.

Development tokens carry their roles in the roles claim: keep auth.roles_claim: roles with them.

Shell
export STELLAR_TOKEN=$(stellar auth dev-token --key dev-idp.seed --sub alice --role operator)
stellar run request.yaml

The web editor#

The web editor identifies its users the same way, token first:

  • a token (Authorization: Bearer) is checked against the key set of the provider;
  • a declared identity (X-Stellar-User) is accepted unless editor.declared_identity: false, in which case it is refused with 401 auth::token-required;
  • without either, 401 auth::anonymous.

A WebSocket opened by a browser cannot carry headers: the editor then reads the identity from the token or user parameter of its query. Only the owner of a draft may change it; its commits are authored by the user.

Services and NATS#

Services do not use OIDC: the services of the core connect to NATS with credentials given by the deployment, and drivers, transports, gateways and connectors with the JWT the reconciler issues them. People who want to follow runs and alarms directly on NATS exchange their token for short-lived NATS credentials. See NATS Accounts and Credentials.

Stellar Control · v0.1.0

↑↓ to moveEnter to open