A shared deployment of Stellar Control (demonstrations, integration tests, development environments) hosts one cell per customer: a NATS account and a dedicated set of core services connected to it. The same cell is what an on-premises installation deploys. Cells share the NATS cluster and the Nomad cluster; nothing else.
flowchart LR
subgraph NATS["NATS cluster (operator, full resolver)"]
A["account demo"]
B["account lab"]
end
subgraph Nomad["Nomad cluster, namespace cells"]
subgraph NA["job cell-demo"]
SA["api, reconciler, executor, compute, alarms, scheduler, transfers, editor, simulators"]
end
subgraph NB["job cell-lab"]
SB["stellar-mcs (small cell)"]
end
P["Traefik"]
end
SA --- A
SB --- B
P -->|demo.mcs.example.org| SA
P -->|lab.mcs.example.org| SB
Bench["bench of lab: gateways, drivers"] -->|leaf node, account lab| BWhy accounts#
An account is the unit of isolation that NATS itself enforces: the subjects, streams, KV buckets and object stores of an account are invisible to the others. So:
- Names do not change. No tenant prefix anywhere: the NATS contract, the drivers and their permissions are the same in a cell, on premises and in development. A missed prefix cannot leak data between tenants.
- Quotas come for free. The JetStream limits of the account (storage, streams, consumers, connections) bound what a cell can use.
- Each reconciler signs for its own account. The reconciler of a cell holds a signing key of the account of its cell, and the JWTs it issues are valid in that account only.
Services per cell#
Each cell has its own services: api, reconciler, executor, compute, alarms, scheduler, transfers, editor and simulators. Sharing services between tenants would put the tenant in every snapshot, election, lease and partition: it is not planned.
- A small cell runs the services of the core in one process,
stellar-mcs(see Running the Services). - An idle cell sleeps: every service at zero instances. It wakes up with its state intact, since the state is in its account.
- Benches join the cell of their customer: their gateways, transports and drivers connect through a NATS leaf node bound to the account of the cell, over TLS or WebSocket on 443.
Security of a cell#
| Layer | Protection |
|---|---|
| Clients ↔ NATS | TLS required (tls://), CA given by the deployment; client certificates optional (mTLS) |
| Leaf nodes, cluster routes | TLS; leaf nodes also over WebSocket on 443 |
| API, web console, editor | HTTPS and WSS, terminated by the reverse proxy: one subdomain per cell, ACME certificates |
| Data at rest | JetStream encryption, one key per server; encrypted disks for the rest |
| Secrets | Operator key offline; per cell, the account signing key, OIDC secrets and the JetStream key in encrypted Nomad variables or Vault |
Encryption at rest protects disks and backups; between tenants, the accounts isolate. See TLS, Encryption and Secrets.
stellar-admin#
stellar-admin provisions the cells. It is a Python tool of the operator of the platform,
distinct from the stellar CLI and the SDKs and never published: it works only with the secrets
of the operator (operator key, system account credentials, Nomad token), which no user holds.
Setup#
export STELLAR_ADMIN_DIR=/etc/stellar-admin # secrets of the operator and platform.yaml
export NOMAD_ADDR=https://nomad.example.org:4646 NOMAD_TOKEN=…
stellar-admin operator init --name stellar-platform --resolver-dir /var/lib/nats/accountsoperator init writes, in STELLAR_ADMIN_DIR (mode 700, secrets 600): the seed of the operator
(operator.nk), the system account and a user of it (system.nk, system.creds), and
resolver.conf, to include in the configuration of the NATS servers (resolver_conf of the
nats job): the operator, the system account and a full resolver, so that accounts come and go
at runtime without a restart.
The platform itself is described by platform.yaml, in the same directory:
nats_url: tls://nats.example.org:4222 # of the tool and of the cells
nats_ca: /etc/stellar/tls/ca.pem # optional
bin_dir: /opt/stellar/bin # release binaries on the Nomad clients
config_file: /opt/stellar/stellar.yaml # global configuration of the cells
repository: /opt/stellar/config # configuration repository of the cells
jobs_dir: /opt/stellar/deployment/nomad/jobs
domain: mcs.example.org # a cell is <name>.<domain>
api_port_base: 18100 # APIs of the cells from there, two ports each
limits: {disk_storage: 2000000000, mem_storage: 268435456, streams: 64}Those cells run as processes, behind the proxy of the platform. In production they run in containers, behind Traefik (Containers):
runtime: containers
ingress: traefik # the only one of the containers
nats_url: nats://172.17.0.1:4222 # reached from the containers, and from the tool
repository: /usr/share/stellar/examples/config # the template of the repositories of the cells
cells_dir: /srv/stellar/cells # a repository per cell: <cells_dir>/<cell>/config
cells_owner: "10001:10001" # the user of the services, owner of the repositories
jobs_dir: /usr/share/stellar/jobs # in the image stellar-admin
domain: cells.example.org
image: registry.example.org/stellar-mcs
tag: 0123456789ab # immutable
traefik_certresolver: letsencrypt
traefik_wildcard: "*.cells.example.org" # one certificate for every cell
limits: {disk_storage: 2000000000, mem_storage: 268435456, streams: 64}
variables: # more variables of the job of the cells
memory: '{default = 128, mcs = 384, editor = 256, simulators = 64}'Commands#
| Command | Effect |
|---|---|
stellar-admin [--dir DIR] … | Directory of the operator (default STELLAR_ADMIN_DIR, else .) |
operator init --name NAME --resolver-dir DIR | Creates the operator, the system account and the resolver configuration |
cell create NAME [--ttl D] [--host HOST] [--small] [--count GROUP=N]… [--template T] [--owner ID --owner-name N --owner-email E] | Creates a cell; --ttl (30m, 12h, 7d) makes it a demonstration that expires; --host overrides <name>.<domain>; --small runs the core in one process; --count sets the instances of a group (editor=1); --template starts its repository from a template of provisioning.yaml; --owner… says who it is for |
cell create NAME --offer O --template T --owner ID … | Creates the cell of a user with an offer of provisioning.yaml, within its quotas; prints its status |
cell status NAME | Where the cell stands, in JSON: provisioning, starting, ready, sleeping or failed, and its steps |
cell extend NAME [--by D] | Pushes its expiry back (default: the life of its offer), within the longest life of its offer |
cell list | Lists the cells |
cell credentials NAME | Prints the bootstrap credentials of the benches of the cell |
cell secret NAME SECRET | Adds or replaces a secret of the cell, read on standard input (forge_token) |
cell sleep NAME | Every service of the cell at zero instances |
cell wake NAME | Wakes the cell up |
cell delete NAME | Deletes the cell and everything of it |
cell reap | Deletes the cells past their expiry |
cell tick | The life cycle, every few minutes: deletes the expired cells, warns before, puts the idle ones to sleep |
stellar-admin cell create demo --ttl 7d # https://demo.<domain>
stellar-admin cell create lab --count editor=1
stellar-admin cell create mini --small
stellar-admin cell credentials demo > bench.creds # STELLAR_NATS_CREDENTIALS of its benches
stellar-admin cell secret lab forge_token < token
stellar-admin cell sleep demo
stellar-admin cell wake demo
stellar-admin cell delete demoWhat cell create provisions#
- An account, signed by the operator and pushed to the resolver of the NATS servers
(
$SYS.REQ.CLAIMS.UPDATE), with the JetStreamlimitsofplatform.yamland, with--ttl, its expiry: past it, the servers refuse its connections. - A signing key of the account for the reconciler of the cell, and two users signed with
it:
services(the core services) andbootstrap(the instances of the benches: registration, heartbeats and control verbs, until the reconciler issues their own JWT). - With
cells_dir, its configuration repository<cells_dir>/<name>/config: the files of its template (repositoryofplatform.yamlby default), committed onmainas its user, given tocells_owner. Its services compile and publish it when they start, and its web editor publishes the drafts proposed there at once (directforge, see Web Editor). - Its Nomad job
cell-<name>, in the namespace of the cells (cells), with the secrets of the cell in the variablenomad/jobs/cell-<name>: Nomad gives them to that job only. Its services run as processes or in containers (theruntimeofplatform.yaml, see Nomad). - Its record in
stellar/cells/<name>and its route: with the proxy of the platform, the variablenomad/jobs/proxy/cells/<name>, read without redeploying it; with Traefik, the tags of the servicecell-<name>-api, read by its Nomad provider.
cell delete removes all of it: the services; the streams, KV buckets and object stores of the
account (renewed first when it expired, so that its data can be reached); the account
($SYS.REQ.CLAIMS.DELETE); the secrets, the service registrations of its job, its repository,
the route and the record. Nothing of the cell is left, on disk either.
Each step is noted in the record once done: a creation interrupted (the cell still
provisioning) goes on from there when asked again, and cell tick deletes one left more than
an hour.
Life of a trial cell#
An online trial gives each user a cell of their own, ready to use: provisioning.yaml, next to
platform.yaml, says what a cell gets and how long.
offers:
trial:
ttl: 7d # life of a cell
max_ttl: 30d # longest life, extensions included
idle_sleep: 2h # asleep after two hours without an access to its API
small: true # the core in one container
counts: {editor: 1} # with its web editor
limits: {disk_storage: 536870912, mem_storage: 67108864, streams: 32}
variables: {memory: '{default = 128, mcs = 384, editor = 256, simulators = 64}'}
templates_dir: /usr/share/stellar/templates # the templates of the image stellar-admin
templates: # more, or overriding one of the directory
my-bench:
path: /srv/stellar/templates/my-bench # a configuration repository
default_offer: trial
quotas: {per_owner: 1, total: 40}
warn_before: 1d # the user warned a day before the expiry
hook: {url: https://app.example.org/hooks/cells, token_env: STELLAR_ADMIN_HOOK_TOKEN}- Ready to use. The cell starts from its template: its repository committed as its user,
the configuration published before its services start, its simulators running. It is
readyonce every service is up and healthy. - Asleep when idle. Its API notes its last access (bucket
stellar_activity, keyapi, at most once a minute; its health checks excluded). Pastidle_sleepwithout one,cell tickputs the cell to sleep; waking it up is the first thing its next visit does (the onboarding application, throughwake). - Expiry.
warn_beforeits expiry, the hook receivesexpiring(once); past it, the cell is deleted and the hook receivesdeleted.extendpushes it back, withinmax_ttl: its account is signed again with the new expiry. - Events. The hook receives a JSON
POSTper event (expiring,deleted,asleep): the cell, its URL, its user (id,name,email) and its expiry, with the bearer token oftoken_env. An event not received is sent again at the next round. - Periodically.
deployment/nomad/jobs/containers/admin.nomad.hclrunscell tickevery five minutes in the imagestellar-admin, with the directory of the operator and the directory of the cells; with Nomad ACLs, bind it todeployment/nomad/policies/cells-admin.hcl.
Templates#
A template is a configuration repository that runs as it is, every target of its demonstration
simulated, with a template.yaml: its title, description and use case, and the procedure and
run request of its demonstration, proposed to the user at their first visit. The images
stellar-admin and stellar-onboarding carry them in /usr/share/stellar/templates, one
directory each, named after the template.
Where the templates come from#
deployment/docker/build.sh runs deployment/docker/templates.sh before building, which fills
deployment/docker/.templates for the Dockerfile:
- From the catalog of Stellar Systems (
STELLAR_CATALOG, thecatalog.yamlof stellar-catalog). Each use case of Stellar Control is a repository of its own,github.com/<company.github>/<repo.name>, cloned at the tagrepo.refpinned in the catalog; a branch (main) is refused, so that an image never changes with a repository. The template is named after the use case (use-cases/<id>), the name the onboarding offers for#/new?usecase=<id>. The catalog goes into the image (/usr/share/stellar/catalog/catalog.yaml,ONBOARDING_CATALOGofstellar-onboarding), and/usr/share/stellar/templates.MANIFESTrecords the repository, tag and commit of each template. - From this repository without a catalog, as before:
examples/use-cases, andexamples/configasspace-missions; no catalog in the image, the onboarding then shows the templates as they describe themselves.
STELLAR_CATALOG=../stellar-catalog/catalog.yaml TEMPLATES_TOKEN=$(gh auth token) \
STELLAR=target/release/stellar deployment/docker/build.sh| Variable | Meaning |
|---|---|
STELLAR_CATALOG | The catalog; without it, the examples of this repository |
TEMPLATES_TOKEN (or GITHUB_TOKEN) | Reads the private use-case repositories (contents: read) |
TEMPLATES_GIT_BASE | A directory of clones instead of GitHub, for a local build |
STELLAR | The stellar CLI of the same commit: each template must pass stellar check --locked; never a CLI of another version |
The workflow Publish images does the same when the repository variable STELLAR_CATALOG_REPO
(owner/name of stellar-catalog, at STELLAR_CATALOG_REF, main by default) and the secret
TEMPLATES_TOKEN are set.
A new version of a template is a tag on its repository (v0.1.1), then the same tag in
repo.ref of the catalog, then a build of the images: the cells created from then on start from
it; the cells already created keep their own repository.
The templates of the catalog (use cases of Stellar Control):
| Template | Demonstration |
|---|---|
payload-integration | The HSC-100 hyperspectral camera from its supplier's ICD: first light |
satellite-integration | A 6U CubeSat over CSP: functional acceptance, the payload chain powered alone |
test-bench-automation | A TVAC chamber cycling a unit under test: pumpdown, two thermal cycles, venting |
The examples of this repository: test-benches, remote-assets, flight-test,
research-facilities, critical-operations, space-missions.
The simulators of a cell are read from the topology of its template: each target whose default
link has the driver of a simulation of simulations/ runs that simulation, on the gateway of
the link.
The use cases the onboarding presents, and in what order, come from the catalog of Stellar
Systems when it is given (ONBOARDING_CATALOG, catalog.yaml of stellar-catalog): a template
is matched to the use case of its use_case. A link to #/new?usecase=<id> opens the wizard on
it, after the sign-in when needed; each environment then lists its first steps: the CLI from
get.stellar-systems.eu, the public repository of its template, its console, its
demonstration run from the command line.
The onboarding application uses the same steps as a Python library:
from stellar_admin.cells import Owner
from stellar_admin.provisioning import Provisioner, QuotaError
provisioner = Provisioner.open(Path("/etc/stellar-admin"))
status = await provisioner.create(Owner("u-42", "Ada Lovelace", "ada@example.org"), template="test-bench")
status = provisioner.status(status.cell) # provisioning → starting → ready
provisioner.cells_of("u-42") # the cells of a user
await provisioner.extend(status.cell) # a week more
provisioner.wake(status.cell) # on the first visit to a sleeping cell
await provisioner.delete(status.cell)create is idempotent: asked again for the same user, it returns their cell, or goes on with the
one being created; past the quotas, QuotaError.
Benches of a cell#
A bench runs its gateways, transports and drivers on site. They reach the cell through a leaf
node of the NATS server of the bench, bound to the account of the cell, and start with the
bootstrap credentials of stellar-admin cell credentials <cell> in STELLAR_NATS_CREDENTIALS.
Once bound, the reconciler of the cell gives each of them its own short-lived JWT (see
NATS Accounts and Credentials).