Skip to content

Register a machine to join the cluster (join listener only).

POST
/v1/join/registrations
Code sample: Shell / cURL
curl --request POST \
--url https://stratum.example.com/v1/join/registrations \
--header 'Content-Type: application/json' \
--data '{ "pake": { "session": "pk-4f7k2q9mabcdefgh", "ke3": "example" }, "release": "0.5.0", "selfUpgrade": true, "hostname": "example", "systemSerial": "example", "inventory": { "uefi": true, "disks": [ { "name": "nvme1n1", "path": "/dev/nvme1n1", "byId": [ "example" ], "model": "example", "serial": "example", "wwn": "example", "transport": "example", "sizeBytes": 1, "rotational": true, "removable": true, "readOnly": true, "mountpoints": [ "example" ], "holders": [ { "kind": "example", "device": "example", "detail": "example" } ], "partitions": [ { "number": 1, "name": "example", "path": "example", "sizeBytes": 1, "fsType": "example", "label": "example", "partLabel": "example" } ], "stack": [ { "name": "example", "path": "example", "type": "example" } ] } ], "nics": [ { "name": "enp87s0", "mac": "example", "permanentMac": "example", "operState": "example", "link": "example", "speedMbps": 1, "mtu": 1, "driver": "example", "addresses": [ "example" ], "wireless": true, "routes": [ "example" ] } ], "cpu": { "model": "example", "sockets": 1, "cores": 1, "threads": 1, "virtualization": "example" }, "memoryBytes": 1, "osDiskLayout": "compute-tail" } }'

. Served only on the join listener (TLS, port 9444, open while a join code is live), never on the API. No session: the proof of the code (the KE3 of a POST /v1/join/pake exchange on this connection) authenticates the call, and the TLS client certificate is the machine’s key, which every later call is pinned to.

Refused: a code not proven (401 JOIN_CODE_INVALID), an expired or revoked code (401, with the reason); a proof for no waiting exchange of this key on this connection (409 JOIN_PAKE_RESTART); a release other than the cluster’s (409 RELEASE_MISMATCH, naming the cluster’s release and its ISO path); a key whose registration was rejected (409); more than 16 registrations waiting, or too many from one address (429); an upgrade in progress (503, retry later). The same key registering again gets its registration back. Nothing secret is returned: the pairing code is derived from the key and the PAKE session.

Media typeapplication/json

A joiner’s registration. Sent over TLS 1.3 to the join listener with the joiner’s own client certificate, on the connection the joiner proved the code on (POST /v1/join/pake), and once the listener has proven it holds the code. The registration is pinned to that certificate’s key.

object
pake
required

The joiner’s proof of its join code: the OPAQUE KE3 for the exchange the listener answered. Sent on the same TLS connection as that exchange, with the same client certificate; a session is used once.

object
session
required

The exchange, from the POST /v1/join/pake answer.

string

Example

pk-4f7k2q9mabcdefgh
ke3
required

The OPAQUE KE3 message (RFC 9807), base64.

string format: byte
release
required

The release the joiner was installed from. Another release than the cluster’s is refused (RELEASE_MISMATCH), unless the joiner sent selfUpgrade and the release is older than the cluster’s and within its upgrade rules: it is then registered as needs-upgrade.

string

Example

0.5.0
selfUpgrade

The joiner can upgrade itself to the cluster’s release before it registers. Absent or false: exactly the cluster’s release or a refusal, as before.

boolean
hostname
string
systemSerial
string
inventory

A joining machine’s hardware, as it reported it at registration (the inventory). The approval’s assignment is checked against it.

object
uefi
boolean
disks
Array<object>

One whole disk of a joining machine, as its discovery reports it.

object
name
required
string

Example

nvme1n1
path
required
string

Example

/dev/nvme1n1
byId
Array<string>
model
string
serial
string
wwn
string
transport
string
sizeBytes
integer format: int64
rotational
boolean
removable
boolean
readOnly
boolean
mountpoints

Live mounts on the disk or anything stacked on it. A mounted disk is the system disk.

Array<string>
holders

What already holds data on the disk. Empty means blank.

Array<object>
object
kind
required
string
device
required
string
detail
string
partitions

The disk’s partitions. On an OS disk they say what stage 1 left free at its end (the compute tail, or nothing with the whole-disk layout).

Array<object>
object
number
required
integer
name
required
string
path
required
string
sizeBytes
required
integer format: int64
fsType
string
label
string
partLabel
string
stack

Every md array and device-mapper device built on the disk, deepest first.

Array<object>
object
name
required
string
path
required
string
type
required
string
nics
Array<object>

One network interface of a joining machine.

object
name
required
string

Example

enp87s0
mac
string
permanentMac
string
operState
string
link

up with a carrier, down without one, unknown.

string
speedMbps
integer format: int64
mtu
integer format: int64
driver
string
addresses
Array<string>
wireless
boolean
routes

The IPv4 destinations the main routing table sends out of this NIC (CIDRs; a host route as /32), without the default route.

Array<string>
cpu
object
model
string
sockets
integer
cores
integer
threads
integer
virtualization

vmx, svm, or empty.

string
memoryBytes
integer format: int64
osDiskLayout

How stage 1 laid out the OS disks, from the joiner’s install record: compute-tail keeps the end of each OS disk for the compute pool, whole-disk gives the disks whole to the system, so compute is a data disk (a compute pool, or Ceph on a disk marked ceph-osd). Absent on a machine with no record; the founder then reads it off the disks’ partitions.

string
Allowed values: compute-tail whole-disk

This key’s existing registration.

Media typeapplication/json

A joiner’s view of its registration.

object
id
required
string
state
required

needs-upgrade: the joiner runs an older release and is upgrading itself to the cluster’s (upgrade says to which). It is not approvable until it registers again on the cluster’s release, when it becomes pending. Only a joiner that sent selfUpgrade is ever in it.

string
Allowed values: needs-upgrade pending approved delivering installing joined failed rejected expired
pairingCode
required
string
waitReason
string
reason

Why it was rejected.

string
upgrade

The upgrade a joiner on an older release makes before it can register: from the release it was installed with to the release the cluster runs. Only a joiner that sent selfUpgrade: true is ever offered one; any other joiner on another release is refused with RELEASE_MISMATCH.

object
from
required

The release the joiner reported.

string
to
required

The release the cluster runs, which the joiner upgrades to.

string
source

Where the joiner fetches the release from (the release mirror’s URL of the release directory). Every file is checked against the release’s signed manifest. Empty when the founder names none.

string
served

The founder gives the release itself, when no release mirror can (a lab, an air gap): fetch it from GET /v1/join/registrations/{joinId}/release/{path} on the join listener, the release layout a mirror publishes (payload/manifest.json, os/blobs/..., sysext/...). source is then empty. Served only to this registration while it is needs-upgrade, read only, a few files at once (503 RELEASE_BUSY: retry); ranges are accepted so a download resumes. The joiner verifies the manifest’s signature against the release keys of its own OS and every file against the manifest, as it does from a mirror.

boolean
delivery

What an approved joiner receives when its turn comes, only over the channel pinned to its key: its node.yaml, built from the assignment, and the join bundle, with its transport leaf issued for its node id.

object
nodeId
required
string
nodeYaml
required
string
bundle
required
object
clusterId

The cluster’s id (e.g. cl-3vin6yzz). The joiner MUST adopt it as its own so every node’s Patroni scope matches — otherwise each node manages a separate single-node cluster and the replica never finds the leader (M1).

string
role
required

The role this node joins as (from the redeemed token).

string
Allowed values: control-plane hybrid hypervisor storage
nodes
required

The cluster members to peer with: every member in joining or active, this node included (admitted as joining when this bundle was issued), in join order.

Array<object>
object
nodeId
required
string
role
required
string
Allowed values: control-plane hybrid hypervisor storage
az

Availability zone; empty on single-AZ installs.

string
mgmtIp

Management-plane address.

string
networks

The node’s traffic-class bindings, keyed by class (storage, overlay). Carried in the join bundle so a joiner’s ClusterConfig matches the founder’s.

Without it a joined node has no networks entry, so ClassAddr falls through to mgmt_ip and EVERY traffic class silently collapses onto the management plane. That put Ceph’s public_network — and therefore all client I/O — on the 1G mgmt NIC while only OSD replication used the 10G storage VLAN, with the cluster reporting HEALTH_OK throughout.

object
key
additional properties

One traffic class’s binding on a node (-TOPO-1).

object
ip
required

The address this class uses on the node (a bare IP, not a CIDR).

string
interface

Host interface carrying the class, when one is declared.

string
labels

The node’s DECLARED label map (topology.rack, topology.power, and any operator-namespace key). Carried in the join bundle so a joiner’s ClusterConfig matches the founder’s.

Without it a joined node has NO declared labels at all, and every consumer degrades to “the founder has a rack, nobody else does” — silently. Rack spread sees one value and two empties, so the anti-affinity guarantee reports success and places anywhere; a CRUSH hierarchy built from labels finds one rack and is refused as a half-declared topology; a rack failure domain can never be satisfied; and a taint declared on a joiner repels nothing.

topology.az is NOT carried here — the AZ rides its own az field, and two records of one fact is the bug each cost a session.

This is the THIRD field to go missing from this schema the same way (networks in the bundle, networks in the status report). A field added to config.ClusterNode does not reach a joiner unless someone remembers this file.

object
key
additional properties
string
bootState

Per-node boot progress, probed from the node’s /startupz. ready = serving; starting = still booting (see bootPhase); unreachable = the node didn’t answer (down, early-boot before the listener, or wedged). Lets the UI tell “booting / stuck / down” apart from a vague “unknown”. Omitted when the boot-health probe isn’t wired (single-node).

string
Allowed values: ready starting unreachable
bootPhase

The in-flight startup phase when bootState=starting (e.g. migrations).

string
bootElapseMs

How long the current boot phase has run (ms); climbs while a phase is stuck.

integer format: int64
readyAfterMs

The node’s last boot duration (ms), when bootState=ready.

integer format: int64
parentDomain

The Stratum-owned domain the cluster serves its API, console and S3 endpoints under. The joiner ADOPTS the founder’s value rather than restating it in its own manifest, so the cluster cannot end up with two nodes disagreeing about its domain. Load-bearing since the parent domain lost its built-in default: a joining node’s control plane refuses to start without one, and a joiner manifest legitimately omits the field.

string
region

The cluster’s region. Adopted by the joiner for the same reason as parentDomain: it is cluster identity, and a node that decides it locally can disagree with the cluster it just joined. The region is baked into every ARN the cluster mints and into every delegated zone’s served FQDN, so two nodes disagreeing is not a cosmetic split. A joiner’s node.yaml legitimately omits it, and the built-in fallback is the test-region-1 placeholder — so before this field a joiner on a cluster with a real region silently ran in a different one. Empty on a pre- founder, where the joiner keeps its previous local behaviour.

string
dnsUpstreamServers

The resolvers the cluster’s VPC recursors forward every name they do not serve to: the founder’s node.yaml dns.upstream_servers, as its control plane loaded them (network.default_dns_servers). Adopted by the joiner for the same reason as parentDomain. Every control plane runs the recursor reconciler, which writes this list into the one shared “.” forward zone, so a node holding another list (or none) would flip that zone on every tick. An EMPTY list is a value, not an omission: the cluster resolves from the root servers, and the joiner adopts that too. Absent only from a pre- founder, where the joiner keeps the list its own manifest names.

Array<string>
caBundle

PEM cluster CA bundle the node trusts (HA installs with an internal PKI).

string
nodeCert

PEM transport leaf issued to this node by the cluster CA (mTLS identity).

string
nodeKey

PEM private key for nodeCert. Sensitive — the node persists it mode 0600.

string
postgresPassword

The cluster’s Postgres superuser/replication password (M1). Sensitive — a joining node uses it so its Patroni replica authenticates streaming replication against the primary instead of minting a fresh password the cluster wouldn’t accept. The node persists it only as the podman secret (mode 0600). Replaced by cert-based replication auth in a later hardening.

string
adminEmail

The cluster’s bootstrap IAM admin email: the email of the admin the founder’s control plane found at start, not that node’s configured one. Adopted by the joiner for the same reason as adminPassword: the admin identity lives once in the cluster Postgres. Before this field a joiner always wrote the built-in default email, and control planes then ensured the admin by email, so on a cluster whose admin had another email its control plane created a second bootstrap admin under the default name. Since a control plane finds the admin by its stamp whatever its env names. Empty on a pre- founder, where the joiner uses that founder’s default, admin@stratum.local.

string
adminPassword

The cluster’s bootstrap IAM admin password. Sensitive — the admin identity is shared (one iam.users row + argon2 hash in the cluster Postgres), so a joining node MUST adopt the founder’s admin password rather than generate its own; otherwise each node’s controlplane.env carries a different STRATUM_IAM_ADMIN_PASSWORD and only the founder’s actually authenticates against the shared hash. The node persists it only in controlplane.env + bootstrap-credentials.txt (mode 0600). Empty on a Postgres-only replica (which runs no control plane).

string
openBaoToken

The cluster’s OpenBao token. Sensitive — a full-stack joiner runs a control plane that needs OpenBao access, but increment 1 keeps OpenBao a single instance behind the.242 VIP (raft is the separate EPIC). Rather than init its own, the joiner adopts this token and points STRATUM_OPENBAO_ADDR at the VIP. The node persists it only in controlplane.env (mode 0600). Empty on a Postgres-only replica (which runs no control plane). a scoped, periodic token (policy stratum-cp), never the root token; a founder still on root refuses the bundle until its token converge has run.

string
openBaoNodeToken

A token minted for this joiner’s host tools (policy stratum-node: the raft doctor’s peer list, removing a failed member from the OpenBao raft, the unseal key in the cluster KV). Sensitive. The node persists it only in /etc/stratum/secrets/openbao-node.token (mode 0600) and its host-helper renews it. Absent from a founder that predates.

string
dnsEncryptionKey

The cluster’s axdnsd ENCRYPTION_KEY, 64 hex characters: the AES-256 key every node’s authoritative DNS seals and opens the DNSSEC private keys and TSIG secrets in the shared database with. Sensitive. The serving control plane reads it from its own node’s axdnsd, because nothing can re-derive it: the founder derived it from the Postgres password it had at install, and a joiner that derived it from postgresPassword after a rotation got a key the cluster’s rows were never sealed with. The joiner persists it only in axdnsd.env (mode 0600). Absent from a pre- founder, where the joiner still derives the key from postgresPassword.

string
openBaoUnsealKey

The cluster’s OpenBao Shamir unseal key. Sensitive — when OpenBao runs as a 3-node integrated- storage raft every member shares the founder’s single unseal key, so a joiner needs it to unseal its local voter right after operator raft join. The joiner persists it NOT in plaintext: it TPM-seals the key to its own TPM immediately on receipt (the boot-time stratum-openbao-unseal.service then re-unseals on every reboot with no human). Transits the same already-authenticated join channel that carries openBaoToken. Empty on increment-1 single-instance clusters (the joiner used the.242 VIP, ran no local OpenBao) and on a Postgres-only replica.

string
membershipGeneration

The membership generation this node was admitted at. Its join is done when every member in joining or active reports at least this generation as applied (GET /v1/admin/cluster/members).

integer format: int64
vips

The cluster’s keepalived service VIPs (HA installs,). firstboot renders the joiner’s keepalived from these so the API + Postgres VIPs match the rest of the cluster.

object
api

Control-plane API VIP.

string
postgres

Postgres VIP (follows the Patroni primary, M1.2 /).

string
openbao

OpenBao VIP (WS-B).

string
ovnNb

OVN northbound DB VIP (WS-C).

string
ovnSb

OVN southbound DB VIP (WS-C).

string
vmselect

Vmselect read VIP for the VictoriaMetrics cluster. The controlplane’s PromQL read path follows it to a healthy vmselect.

string
providerGw

Routed-host floating-IP next hop: the LAN router routes the floating ranges to it, and it follows the node whose chassis holds the VPC gateways. Absent on a bridged cluster.

string

Example

{
"state": "needs-upgrade",
"upgrade": {
"from": "0.5.0",
"to": "0.6.0",
"source": "https://mirror.example/releases/stratum/0.6.0/bundle"
},
"delivery": {
"bundle": {
"clusterId": "cl-3vin6yzz",
"role": "control-plane",
"nodes": [
{
"nodeId": "n-abc12def",
"role": "control-plane",
"networks": {
"additionalProperty": {
"ip": "192.0.2.111",
"interface": "vlan90"
}
},
"bootState": "ready",
"bootPhase": "migrations"
}
],
"parentDomain": "stratum.example.com",
"region": "lab-1",
"dnsUpstreamServers": [
"192.0.2.53"
],
"adminEmail": "admin@cloud.internal"
}
}
}

Registered; waiting for approval.

Media typeapplication/json

A joiner’s view of its registration.

object
id
required
string
state
required

needs-upgrade: the joiner runs an older release and is upgrading itself to the cluster’s (upgrade says to which). It is not approvable until it registers again on the cluster’s release, when it becomes pending. Only a joiner that sent selfUpgrade is ever in it.

string
Allowed values: needs-upgrade pending approved delivering installing joined failed rejected expired
pairingCode
required
string
waitReason
string
reason

Why it was rejected.

string
upgrade

The upgrade a joiner on an older release makes before it can register: from the release it was installed with to the release the cluster runs. Only a joiner that sent selfUpgrade: true is ever offered one; any other joiner on another release is refused with RELEASE_MISMATCH.

object
from
required

The release the joiner reported.

string
to
required

The release the cluster runs, which the joiner upgrades to.

string
source

Where the joiner fetches the release from (the release mirror’s URL of the release directory). Every file is checked against the release’s signed manifest. Empty when the founder names none.

string
served

The founder gives the release itself, when no release mirror can (a lab, an air gap): fetch it from GET /v1/join/registrations/{joinId}/release/{path} on the join listener, the release layout a mirror publishes (payload/manifest.json, os/blobs/..., sysext/...). source is then empty. Served only to this registration while it is needs-upgrade, read only, a few files at once (503 RELEASE_BUSY: retry); ranges are accepted so a download resumes. The joiner verifies the manifest’s signature against the release keys of its own OS and every file against the manifest, as it does from a mirror.

boolean
delivery

What an approved joiner receives when its turn comes, only over the channel pinned to its key: its node.yaml, built from the assignment, and the join bundle, with its transport leaf issued for its node id.

object
nodeId
required
string
nodeYaml
required
string
bundle
required
object
clusterId

The cluster’s id (e.g. cl-3vin6yzz). The joiner MUST adopt it as its own so every node’s Patroni scope matches — otherwise each node manages a separate single-node cluster and the replica never finds the leader (M1).

string
role
required

The role this node joins as (from the redeemed token).

string
Allowed values: control-plane hybrid hypervisor storage
nodes
required

The cluster members to peer with: every member in joining or active, this node included (admitted as joining when this bundle was issued), in join order.

Array<object>
object
nodeId
required
string
role
required
string
Allowed values: control-plane hybrid hypervisor storage
az

Availability zone; empty on single-AZ installs.

string
mgmtIp

Management-plane address.

string
networks

The node’s traffic-class bindings, keyed by class (storage, overlay). Carried in the join bundle so a joiner’s ClusterConfig matches the founder’s.

Without it a joined node has no networks entry, so ClassAddr falls through to mgmt_ip and EVERY traffic class silently collapses onto the management plane. That put Ceph’s public_network — and therefore all client I/O — on the 1G mgmt NIC while only OSD replication used the 10G storage VLAN, with the cluster reporting HEALTH_OK throughout.

object
key
additional properties

One traffic class’s binding on a node (-TOPO-1).

object
ip
required

The address this class uses on the node (a bare IP, not a CIDR).

string
interface

Host interface carrying the class, when one is declared.

string
labels

The node’s DECLARED label map (topology.rack, topology.power, and any operator-namespace key). Carried in the join bundle so a joiner’s ClusterConfig matches the founder’s.

Without it a joined node has NO declared labels at all, and every consumer degrades to “the founder has a rack, nobody else does” — silently. Rack spread sees one value and two empties, so the anti-affinity guarantee reports success and places anywhere; a CRUSH hierarchy built from labels finds one rack and is refused as a half-declared topology; a rack failure domain can never be satisfied; and a taint declared on a joiner repels nothing.

topology.az is NOT carried here — the AZ rides its own az field, and two records of one fact is the bug each cost a session.

This is the THIRD field to go missing from this schema the same way (networks in the bundle, networks in the status report). A field added to config.ClusterNode does not reach a joiner unless someone remembers this file.

object
key
additional properties
string
bootState

Per-node boot progress, probed from the node’s /startupz. ready = serving; starting = still booting (see bootPhase); unreachable = the node didn’t answer (down, early-boot before the listener, or wedged). Lets the UI tell “booting / stuck / down” apart from a vague “unknown”. Omitted when the boot-health probe isn’t wired (single-node).

string
Allowed values: ready starting unreachable
bootPhase

The in-flight startup phase when bootState=starting (e.g. migrations).

string
bootElapseMs

How long the current boot phase has run (ms); climbs while a phase is stuck.

integer format: int64
readyAfterMs

The node’s last boot duration (ms), when bootState=ready.

integer format: int64
parentDomain

The Stratum-owned domain the cluster serves its API, console and S3 endpoints under. The joiner ADOPTS the founder’s value rather than restating it in its own manifest, so the cluster cannot end up with two nodes disagreeing about its domain. Load-bearing since the parent domain lost its built-in default: a joining node’s control plane refuses to start without one, and a joiner manifest legitimately omits the field.

string
region

The cluster’s region. Adopted by the joiner for the same reason as parentDomain: it is cluster identity, and a node that decides it locally can disagree with the cluster it just joined. The region is baked into every ARN the cluster mints and into every delegated zone’s served FQDN, so two nodes disagreeing is not a cosmetic split. A joiner’s node.yaml legitimately omits it, and the built-in fallback is the test-region-1 placeholder — so before this field a joiner on a cluster with a real region silently ran in a different one. Empty on a pre- founder, where the joiner keeps its previous local behaviour.

string
dnsUpstreamServers

The resolvers the cluster’s VPC recursors forward every name they do not serve to: the founder’s node.yaml dns.upstream_servers, as its control plane loaded them (network.default_dns_servers). Adopted by the joiner for the same reason as parentDomain. Every control plane runs the recursor reconciler, which writes this list into the one shared “.” forward zone, so a node holding another list (or none) would flip that zone on every tick. An EMPTY list is a value, not an omission: the cluster resolves from the root servers, and the joiner adopts that too. Absent only from a pre- founder, where the joiner keeps the list its own manifest names.

Array<string>
caBundle

PEM cluster CA bundle the node trusts (HA installs with an internal PKI).

string
nodeCert

PEM transport leaf issued to this node by the cluster CA (mTLS identity).

string
nodeKey

PEM private key for nodeCert. Sensitive — the node persists it mode 0600.

string
postgresPassword

The cluster’s Postgres superuser/replication password (M1). Sensitive — a joining node uses it so its Patroni replica authenticates streaming replication against the primary instead of minting a fresh password the cluster wouldn’t accept. The node persists it only as the podman secret (mode 0600). Replaced by cert-based replication auth in a later hardening.

string
adminEmail

The cluster’s bootstrap IAM admin email: the email of the admin the founder’s control plane found at start, not that node’s configured one. Adopted by the joiner for the same reason as adminPassword: the admin identity lives once in the cluster Postgres. Before this field a joiner always wrote the built-in default email, and control planes then ensured the admin by email, so on a cluster whose admin had another email its control plane created a second bootstrap admin under the default name. Since a control plane finds the admin by its stamp whatever its env names. Empty on a pre- founder, where the joiner uses that founder’s default, admin@stratum.local.

string
adminPassword

The cluster’s bootstrap IAM admin password. Sensitive — the admin identity is shared (one iam.users row + argon2 hash in the cluster Postgres), so a joining node MUST adopt the founder’s admin password rather than generate its own; otherwise each node’s controlplane.env carries a different STRATUM_IAM_ADMIN_PASSWORD and only the founder’s actually authenticates against the shared hash. The node persists it only in controlplane.env + bootstrap-credentials.txt (mode 0600). Empty on a Postgres-only replica (which runs no control plane).

string
openBaoToken

The cluster’s OpenBao token. Sensitive — a full-stack joiner runs a control plane that needs OpenBao access, but increment 1 keeps OpenBao a single instance behind the.242 VIP (raft is the separate EPIC). Rather than init its own, the joiner adopts this token and points STRATUM_OPENBAO_ADDR at the VIP. The node persists it only in controlplane.env (mode 0600). Empty on a Postgres-only replica (which runs no control plane). a scoped, periodic token (policy stratum-cp), never the root token; a founder still on root refuses the bundle until its token converge has run.

string
openBaoNodeToken

A token minted for this joiner’s host tools (policy stratum-node: the raft doctor’s peer list, removing a failed member from the OpenBao raft, the unseal key in the cluster KV). Sensitive. The node persists it only in /etc/stratum/secrets/openbao-node.token (mode 0600) and its host-helper renews it. Absent from a founder that predates.

string
dnsEncryptionKey

The cluster’s axdnsd ENCRYPTION_KEY, 64 hex characters: the AES-256 key every node’s authoritative DNS seals and opens the DNSSEC private keys and TSIG secrets in the shared database with. Sensitive. The serving control plane reads it from its own node’s axdnsd, because nothing can re-derive it: the founder derived it from the Postgres password it had at install, and a joiner that derived it from postgresPassword after a rotation got a key the cluster’s rows were never sealed with. The joiner persists it only in axdnsd.env (mode 0600). Absent from a pre- founder, where the joiner still derives the key from postgresPassword.

string
openBaoUnsealKey

The cluster’s OpenBao Shamir unseal key. Sensitive — when OpenBao runs as a 3-node integrated- storage raft every member shares the founder’s single unseal key, so a joiner needs it to unseal its local voter right after operator raft join. The joiner persists it NOT in plaintext: it TPM-seals the key to its own TPM immediately on receipt (the boot-time stratum-openbao-unseal.service then re-unseals on every reboot with no human). Transits the same already-authenticated join channel that carries openBaoToken. Empty on increment-1 single-instance clusters (the joiner used the.242 VIP, ran no local OpenBao) and on a Postgres-only replica.

string
membershipGeneration

The membership generation this node was admitted at. Its join is done when every member in joining or active reports at least this generation as applied (GET /v1/admin/cluster/members).

integer format: int64
vips

The cluster’s keepalived service VIPs (HA installs,). firstboot renders the joiner’s keepalived from these so the API + Postgres VIPs match the rest of the cluster.

object
api

Control-plane API VIP.

string
postgres

Postgres VIP (follows the Patroni primary, M1.2 /).

string
openbao

OpenBao VIP (WS-B).

string
ovnNb

OVN northbound DB VIP (WS-C).

string
ovnSb

OVN southbound DB VIP (WS-C).

string
vmselect

Vmselect read VIP for the VictoriaMetrics cluster. The controlplane’s PromQL read path follows it to a healthy vmselect.

string
providerGw

Routed-host floating-IP next hop: the LAN router routes the floating ranges to it, and it follows the node whose chassis holds the VPC gateways. Absent on a bridged cluster.

string

Example

{
"state": "needs-upgrade",
"upgrade": {
"from": "0.5.0",
"to": "0.6.0",
"source": "https://mirror.example/releases/stratum/0.6.0/bundle"
},
"delivery": {
"bundle": {
"clusterId": "cl-3vin6yzz",
"role": "control-plane",
"nodes": [
{
"nodeId": "n-abc12def",
"role": "control-plane",
"networks": {
"additionalProperty": {
"ip": "192.0.2.111",
"interface": "vlan90"
}
},
"bootState": "ready",
"bootPhase": "migrations"
}
],
"parentDomain": "stratum.example.com",
"region": "lab-1",
"dnsUpstreamServers": [
"192.0.2.53"
],
"adminEmail": "admin@cloud.internal"
}
}
}

Bad request.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}

Missing or invalid token.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}
WWW-Authenticate
string

Conflicting state.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}

Request rate limit exceeded. The API-boundary throttle caps request throughput per principal and per project; code is RATE_LIMITED (distinct from the quota engine’s QUOTA_EXCEEDED). The Retry-After header carries the seconds to wait. Applies to any authenticated endpoint — it is enforced by middleware, not per-path.

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}
Retry-After
integer

Seconds to wait before retrying.

Upstream temporarily unavailable (driver / scheduler / dependency).

Media typeapplication/problem+json
object
type
required
string format: uri
title
required
string
status
required
integer
>= 400 <= 599
detail
string
instance
string format: uri-reference
requestId
required
string
code
required
string
issues

The findings of a rejected IAM policy document (custom and resource policies).

Array<object>

One finding of a rejected IAM policy document. A rejection lists every finding, so the author fixes the document in one round trip.

object
code
required

The finding, e.g. INVALID_DOCUMENT, UNKNOWN_ACTION, ACTION_NOT_APPLICABLE.

string
statement
required

Index of the statement at fault; -1 for the document as a whole.

integer
>= -1
detail
required
string
action

The offending action, where one is at fault (ACTION_NOT_APPLICABLE).

string
errors

Field errors, for codes that carry them: each problem at its field (see FieldError). Documented on the responses that set it, e.g. approveNodeJoin’s INVALID_INPUT and JOIN_APPROVAL_REFUSED. Absent otherwise.

Array<object>

One field’s problem, the stage-2 installer API’s shape. field is the field’s path, message what is wrong with it.

object
field
required

The field’s path, e.g. links[0].interface, disks[1].confirm, mgmt.address or role; empty for the whole request.

string
message
required
string
details

Structured facts about the refusal, for codes that carry them. The shape is fixed per code and documented on the response that answers it, e.g. IMAGE_POOL_MISMATCH (see ImagePoolMismatchDetails) and IMAGE_IN_USE. Absent for codes that carry none.

object
key
additional properties
any

Example

{
"issues": [
{
"code": "ACTION_NOT_APPLICABLE",
"action": "compute:CreateVpc"
}
]
}