Repository navigation
console rps deplyment design
Supporting design and implementation documentation for the Console + RPS Cloud Deployment ADR (deployment#614).
- Abstract
- Console + RPS observations
- Reference flows
- Other Design Decisions
- Diagrams
- Detailed design by decision
- Review feedback
A cloud deployment of Console and RPS runs both services side by side, with Console in place of MPS. RPS stays the provisioning orchestrator, and Console supports the RPS activation flow by serving device lifecycle APIs and managing devices once provisioned. This design proposes that Console owns profiles and the only database, including device and profile passwords; RPS reads the configuration it needs at activation from Console's API, with its own REST APIs turned off; Console upserts devices and auth stays at the gateway. Vault holds only the CIRA TLS identity (MPSCerts) and Console's app encryption key, not passwords. RPS supports both deployments, MPS + RPS as it works today and Console + RPS.
Observations and what breaks when Console and RPS run together, without the decisions in this ADR.
Deployed the stack with make up, which brings up console, rps, db, vault, kong and webui. Local activation with Console works.
Remote mode, orchestrated by RPS. Every problem below is in this path.
Note: All fixes and workarounds are temporary, done only to reach the next blocker.
| # | Problem | Effect | Decision |
|---|---|---|---|
| 1 | A profile created through Console's API is in the Sample Web UI, created through RPS's API is not listed | The UI is built "enterprise", which points to "rpsServer" at Console, so the profile list only ever reads Console's tables. Kong has no route to RPS's REST API. | 1 |
| 2 | Activation from the edge node fails with "profile does not match list of available AMT profiles" | The profile row is there in the shared database's profiles table. RPS joins profiles_proxyconfigs on every profile read, and Console's migrations never create that table, so the query fails with "relation profiles_proxyconfigs does not exist". RPS swallows the error and returns no profile. | 3 |
| 3 | Problem 2 fixed by creating the two missing tables at database init, activation fails with "Unknown error has occured" | RPS reads the AMT password from Vault at profiles/, and only RPS's own create writes it. The profile was created on Console, which encrypts the password into its own table, so the read 404s and RPS logs "password cannot be blank". The device is only told "Unknown error has occured". | 1 |
| 4 | Problem 3 worked around by seeding the Vault key, activation reports success but the device is not in the Sample Web UI | Console runs with AUTH_DISABLED=false and RPS sends no credentials, so all three POSTs to /api/v1/devices on the CIRA path - registration, CIRA registration and provisioning status - are 401 and no device row is ever created. Activation itself succeeds, and the only sign on the device is one line, "CIRA: Failed to save device to mps", followed by "Device activated successfully". | 5 |
| 5 | Problem 4 fixed by turning off Console's own API JWT check, the same three POSTs now return 400 and the device is still not registered | RPS puts the profile's tags straight into the device payload. Its own schema declares tags as text[], but Console's migrations created the shared profiles.tags column as text, so RPS reads back the string "rps-owned, cira" and Console's DTO expects an array. The body fails to bind before the insert is reached. Console logs the 400 with no reason. | 1, 3 |
Baseline Cloud v2 (MPS + RPS) is unchanged; refer to the architecture reference: cloud deployment, component map, inter-service communication, database layout, service roles, and activation phases. The Console + RPS proposal diagrams under the design decisions adapt these references, with Console in place of MPS and MPS Router.
These are proposals for review, not settled decisions.
-
Migration from MPS + RPS to Console + RPS must be planned. Propose an explicit, repeatable migration for device inventory and configuration, moving device and profile passwords from Vault into Console's database and carrying
MPSCertsinto Vault unchanged, with verification and rollback. See 6. -
Support three deployment profiles with standalone defaults. Standalone Console, Cloud greenfield, and Cloud brownfield all keep passwords in Console's database, and select a Vault-backed provider for the CIRA certificate and the app encryption key. Leaving the settings unset preserves standalone behavior. See 7.
-
Console is single-instance for v3.0. Horizontal CIRA scaling and MPS Router device affinity are not replaced in this release; this is a documented regression from Cloud v2 and a follow-up requirement before scaling Console horizontally. See 8.
These diagrams adapt the architecture reference to the Console + RPS proposal, with Console in place of MPS and MPS Router. RPS reads configuration from Console's API; Vault holds only MPSCerts and the app encryption key, not passwords.
flowchart TB
Cloud["Console + RPS cloud deployment (proposal)"]
Cloud --> Console["Console"]
Cloud --> RPS["RPS"]
Cloud --> RPC["RPC-Go"]
Cloud --> SWUI["Sample Web UI"]
Cloud --> EA["Enterprise Assistant"]
SWUI --> UITAngular["UI Toolkit Angular"]
UITAngular --> UIT["UI Toolkit"]
Console --> GoWSMAN["Go-WSMAN-Messages"]
RPS --> WSMAN["WSMAN-Messages"]
RPC --> GoWSMAN
graph TB
subgraph External["External Network"]
Client[API Client / Browser]
end
subgraph Device["Client Machine (vPro/ISM)"]
RPC["RPC-Go<br/>(OS Layer)"]
AMT["Intel AMT<br/>(Firmware Layer)"]
end
subgraph Cloud["Cloud Services"]
Kong["Kong API Gateway<br/>:443 HTTPS<br/>JWT · CORS · TLS termination"]
subgraph Core["Core Services - internal network only, never published to the host"]
Console["Console, single instance<br/>:8181 REST API + WS Relay<br/>:4433 CIRA (TLS+APF)"]
RPS["RPS<br/>:8080 WebSocket (activation)<br/>config API off (RPS_MODE=console)"]
end
subgraph Backing["Backing Services (3rd Party)"]
DB["Console database :5432<br/>devices, configuration, passwords"]
Vault["Vault :8200<br/>MPSCerts · app encryption key"]
end
WebUI["Sample Web UI<br/>nginx :80"]
end
RPC -->|"LMS (APF) :16992/16993"| AMT
Client -->|HTTPS :443| Kong
RPC -->|"WSS :443 /activate"| Kong
AMT -->|"TLS :4433 (CIRA)"| Kong
Kong -->|"/api, /api/v1/authorize, /relay"| Console
Kong -->|"/activate, /deactivate, /maintenance"| RPS
Kong -->|/| WebUI
Kong -->|":4433 TCP passthrough"| Console
RPS -->|"POST /api/v1/devices, upsert, including device credentials"| Console
RPS -->|"configuration reads"| Console
Console --> DB
Console -->|"read MPSCerts and the app encryption key"| Vault
style Console fill:#1565C0,color:#fff
style RPS fill:#1976D2,color:#fff
style WebUI fill:#2196F3,color:#fff
style RPC fill:#42A5F5,color:#fff
style Kong fill:#616161,color:#fff
style DB fill:#616161,color:#fff
style Vault fill:#616161,color:#fff
style Client fill:#616161,color:#fff
style AMT fill:#616161,color:#fff
style Device fill:#EEEEEE,color:#000
style Cloud fill:#ffffff,color:#000,stroke:#1565C0
style Core fill:#E3F2FD,color:#000
style Backing fill:#FAFAFA,color:#000
style External fill:#FAFAFA,color:#000
graph LR
Client[Client / Browser] -->|":443 HTTPS"| Kong
RPC[RPC-Go] -.->|":443 WSS /activate"| Kong
AMT[Intel AMT] ==>|":4433 TLS (CIRA)"| Kong
Kong -->|":8181 HTTP auth / API / WS relay"| Console
Kong -->|":8080 WS /activate, /deactivate, /maintenance"| RPS
Kong -->|":80 HTTP"| WebUI[Sample Web UI]
Kong ==>|":4433 TCP passthrough"| Console
RPS -->|"configuration reads, device credential writes :8181"| Console
Console --> CDB[(Console database<br/>devices, configuration, passwords)]
Console -->|"read MPSCerts and the app encryption key"| Vault[Vault :8200]
style Console fill:#1565C0,color:#fff
style RPS fill:#1976D2,color:#fff
style WebUI fill:#2196F3,color:#fff
style RPC fill:#42A5F5,color:#fff
style Kong fill:#616161,color:#fff
style CDB fill:#616161,color:#fff
style Vault fill:#616161,color:#fff
style Client fill:#616161,color:#fff
style AMT fill:#616161,color:#fff
RPS reads configuration through Console's API and does not connect to a database or to Vault directly.
Standalone Console, Cloud v2 MPS + RPS, and Console + RPS are three separate deployment profiles; RPS_MODE selects the RPS deployment per decision 2.
flowchart TB
subgraph SA["Standalone Console"]
ConsoleSA[Console] --> SADB[("Console database<br/>devices, configuration,<br/>secrets encrypted in columns")]
end
subgraph V2["Cloud v2, MPS + RPS, the brownfield source"]
MPS2[MPS] --> MPSDB[("mpsdb<br/>devices")]
RPS2[RPS] --> RPSDB[("rpsdb<br/>configuration")]
MPS2 --- V2Vault[("Vault<br/>MPSCerts, devices/guid,<br/>profile secrets")]
RPS2 --- V2Vault
end
subgraph CP["Console + RPS cloud, greenfield and brownfield"]
ConsoleC[Console] --> CDB[("Console database<br/>devices, configuration,<br/>passwords encrypted in columns")]
RPSC[RPS] -. "configuration API, device credential writes" .-> ConsoleC
ConsoleC --- CVault[("Vault<br/>MPSCerts, app encryption key")]
end
V2 -. "migrate records; passwords move from Vault into the database" .-> CP
Propose migrating device inventory from mpsdb and RPS-owned configuration from rpsdb into
Console-owned storage, and migrating device and profile passwords out of Vault's devices/{guid}
and profiles/{name} into Console's database columns. Do not treat the legacy database schemas as
interchangeable or mount both services on one unconverted schema.
| Component | Proposed responsibility |
|---|---|
| Console | Device inventory, AMT operations, CIRA/APF listener, Console-owned profiles and configuration APIs, device and profile passwords |
| RPS | Activation/deactivation state machines, profile application, device credential writes through Console's API |
| Kong | External TLS/JWT boundary and WebSocket/TCP routing |
| Vault | CIRA TLS identity (MPSCerts) and Console's app encryption key |
| Sample Web UI | Console device workflows and Console-owned configuration |
graph LR
RPS["RPS<br/>activation · deactivation · maintenance<br/>profile application · device registration"]
Console["Console<br/>devices · AMT operations · CIRA/APF listener<br/>profiles and all configuration · login"]
CDB[("Console database")]
Vault[("Vault")]
RPS -->|"POST /api/v1/devices, upsert, including device credentials"| Console
RPS -->|"configuration over Console's API"| Console
Console --> CDB
Console -->|"read MPSCerts and the app encryption key"| Vault
style RPS fill:#1976D2,color:#fff
style Console fill:#1565C0,color:#fff
sequenceDiagram
participant Admin
participant UI as Sample Web UI
participant Kong
participant Console
participant DB as Console database
participant RPS
participant Vault
participant RPC as RPC-Go
participant AMT
Console->>Vault: Read existing MPSCerts and the app encryption key at startup
Vault-->>Console: Existing MPS root, server TLS material, and app encryption key
Console->>Console: Start the CIRA listener on 4433
Note over Console: Fail startup if MPSCerts is missing or unreadable, never generate a replacement
Admin->>UI: Create profile and related configuration
UI->>Kong: Authenticated Console API request
Kong->>Console: Profile/configuration write
Console->>DB: Store profile/configuration, including AMT_PASSWORD and MEBX_PASSWORD
RPC->>Kong: WSS /activate
Kong->>RPS: Activation request
RPS->>Console: Read profile/configuration over Console's API
RPS->>RPC: Send WSMAN request
RPC->>AMT: Forward via LMS/MEI
AMT-->>RPC: WSMAN response
RPC-->>RPS: Relay response
Note over RPS,AMT: CIRA configuration installs the MPS root into AMT's trusted store
RPS->>Console: Write device credentials (POST/PATCH /api/v1/devices)
Console->>DB: Store device credentials
loop Registration, CIRA registration (when enabled), and provisioning status
RPS->>Console: POST /api/v1/devices
Console->>DB: Insert or merge by GUID
end
AMT->>Kong: Outbound CIRA TLS + APF connection
Kong->>Console: TCP passthrough to CIRA listener
Console-->>AMT: Present server certificate from MPSCerts
AMT->>AMT: Validate against MPS root installed at activation
Console->>DB: Read device record and MPS_PASSWORD for APF authentication
Console-->>AMT: APF authentication succeeds
RPS remains the activation orchestrator. Console owns the device record and CIRA/APF endpoint in this proposal. RPS writes device credentials through Console's API; Console reads and writes all passwords in its own database, and uses Vault only for MPSCerts and the app encryption key.
RPS keeps activation, deactivation and maintenance. No change to the RPS state machines. In Docker, mps_server is set as RPS_MPS_SERVER=http://console:8181.
Ownership split
| Concern | Owner |
|---|---|
| Profiles, CIRA / wireless / 802.1x configs, domains, proxy configs | Console |
| Provisioning orchestration, activation and deactivation | RPS |
| Devices, AMT operations, KVM/SOL, login | Console |
RPS still exposes the same CRUD on its own API, so it must be turned off. RPS's configuration API is withdrawn in this deployment, not reproduced by Console; ISVs move to Console's API, with a field mapping for profile tags, wireless link policy, the proxy model, and domain lookup by suffix.
Proposal: add RPS_MODE, mps by default, console in this deployment. This is a change in RPS
source and ships in an RPS release. No deployment configuration substitutes for it: RPS mounts
its routes in its own code, so the setting has to exist there. The config key is mode - rc strips
the rps_ prefix off env vars, the same way RPS_MPS_SERVER sets mps_server.
RPS reads configuration from Console's API
With RPS_MODE=console, RPS's configuration CRUD routes under /api/v1 are turned off; the health
route remains. RPS selects the Console API provider and keeps no database connection of its own.
We propose retaining RPS's health route and probing Console /healthz at startup.
Reads from Console
RPS's configuration reads move to Console's API, behind the existing IDB read methods so the state machines are unchanged. Each read is tenant scoped.
| Config | RPS read site | Console endpoint |
|---|---|---|
| Profile, with AMT and MEBx settings | ProfileManager.getAmtProfile | GET /api/v1/admin/profiles/{name} |
| CIRA config | profiles.getByName, ciraConfigObject | GET /api/v1/admin/ciraconfigs/{name} |
| Wireless and 802.1x | wifiNetworkConfiguration | GET /api/v1/admin/wirelessconfigs/{name}, /api/v1/admin/ieee8021xconfigs/{name} |
| Domain, ACM provisioning cert | DomainCredentialManager.getProvisioningCert, by domain suffix | none today, gap 2 |
| Proxy configs | profiles.getByName, the proxyConfigs join | none today, gap 3 |
Four gaps in that read path:
| # | Gap | Proposal |
|---|---|---|
| 1 | RPS reads profiles/{name}, CIRAConfigs/{configName}, Wireless/{profileName} and certs/{profileName}. Console's domain cert path is certs/domains/{tenantID}/{profileName}; RPS's TLS path TLS/{name} is written by RPS. |
Console writes the profile, CIRA and wireless secrets at RPS's expected paths and aligns the domain-cert path, or RPS's read adapts to Console's. |
| 2 | RPS looks a domain up by domain suffix, not by name. Console has GetDomainByDomainSuffix in the domains usecase but no route for it | Console to expose it as a route |
| 3 | Console has no proxy config entity. Proxy is a proxydetails column on ciraconfigs, not RPS's proxyconfigs and profiles_proxyconfigs model | Console to add a proxyconfigs entity, CRUD at /api/v1/admin/proxyconfigs and a profile link table, and return proxyConfigs in the profile read. Until then RPS skips proxy configuration |
| 4 | RPS puts the profile's tags straight into the device payload, so the profile read must return tags as an array | already satisfied - dto.Profile.Tags is []string. The text column is Console's storage, not its response, so it does not surface on this path |
| Deployment | RPS_MODE | RPS config CRUD | RPS database | Configuration read from |
|---|---|---|---|---|
| MPS + RPS | mps | on | rpsdb | its own tables |
| Console + RPS | console | off (health on) | none | Console's API |
mps stays the default, so existing deployments are unchanged.
We need one Console-owned database and no RPS database connection. We propose consoledb as its
name; renaming the existing rpsdb is a separate migration operation. RPS needs no database: all
of its tables are configuration and it keeps no device or runtime state, so its connection string
is dropped. It must not read Console's database either.
Console's database holds device and profile passwords (AMT_PASSWORD, MPS_PASSWORD, MEBX_PASSWORD), as it does for standalone Console today. RPS writes and reads device credentials through Console's device API rather than Vault, removing the Vault round trip this used to require; Vault API calls are costly, and passwords are read far more often than certificates.
RPS and MPS already share one Vault; Console keeps using it, narrowed to the CIRA TLS identity
(MPSCerts) and Console's APP_ENCRYPTION_KEY. For brownfield deployments, Console must read the
existing MPSCerts in place rather than seeding it from Console's root certificate. The
certificate approach is not finalized; see
decision 3
in the ADR.
Requirements:
| # | Requirement | Why |
|---|---|---|
| 1 | Console owns the only database; consoledb is the proposed name |
RPS needs no database connection. Renaming the existing rpsdb database is a separate migration operation |
| 2 | Drop RPS_CONNECTION_STRING | RPS has no database in this deployment |
| 3 | RPS serves activation without its own database and waits for Console /healthz before starting listeners |
the configuration provider is Console's API |
| 4 | Console must normalize the Vault base path before joining secret keys | avoids double slashes on requests for MPSCerts and the app encryption key |
| 5 | Console reads the existing MPSCerts in place; it does not seed or generate a new root. Console fails to start if MPSCerts is missing or unreadable |
RPS's TLS activation path requires this secret, and reusing the existing MPS root is required for brownfield devices to reconnect |
| 6 | Console reads and writes device and profile passwords in its own database, not Vault | avoids a Vault round trip on every credential read; RPS writes device credentials through Console's device API at activation |
Console must serve the endpoints RPS calls, and persist status, activatedAt and
lastProvisionedAt in deviceInfo:
GET /api/v1/devices/{guid}?tenantId=...POST /api/v1/devicesPATCH /api/v1/devicesDELETE /api/v1/devices/{guid}?tenantId=...DELETE /api/v1/devices/refresh/{guid}?tenantId=...
The contract change is upsert: a POST for a GUID that already exists must update that row instead of failing. MPS behaves this way; Console returns 409. RPS posts the same device more than once per activation - registration and provisioning status always, plus CIRA registration with mpsusername on the CIRA path - so a 409 on any of them fails provisioning.
Proposal: on GUID conflict, POST merges the supplied fields the same way PATCH does. 201 stays "created", 200 means "merged". A caller can then overwrite mpsusername, hostname or tags on an existing device; the control against that is the trust boundary in 5.
If duplicate POSTs may run concurrently, we propose serializing merges by GUID or making the merge atomic to avoid lost field updates.
DELETE /api/v1/devices/refresh/{guid} is called after an AMT password change. Console caches a WSMAN connection per GUID, and without this route it keeps using the old password. Console reads device credentials from its own database at the point of use rather than caching its own copy, so RPS must write the rotated password through Console's device API before calling this route; refreshing first re-caches the old value.
The proposal keeps the Cloud 2.x pattern of gateway-enforced authentication, but the unauthenticated surface behind the gateway is wider than 2.x: RPS now also reads Console's configuration over that same path, not only device operations.
Proposal: a Console auth mode, auth.gatewayEnforced (AUTH_GATEWAY_ENFORCED), used with
AUTH_DISABLED=false. It changes one thing: /api is mounted without Console's JWT middleware.
Login still checks credentials and signs the JWT and session cookie Kong verifies, the relay token is
still signed per device and checked by Console, and a JWT key is still required. Console refuses to
start with both disabled and gatewayEnforced set. Tenant comes from the x-tenant-id header and is
unaffected.
| Behaviour | MPS | Console, gateway-enforced |
|---|---|---|
| Login checks credentials and signs the JWT | yes | yes |
JWT checked on /api by the service |
no | no |
JWT checked on /api by Kong |
yes | yes |
| Relay token signed and checked by the service | yes | yes |
| RPS calls the service without credentials | yes | yes |
This is a deployment requirement, not only an assumption: 8181 is only on the internal Compose network, never published to the host.
Known gaps, bounded by that requirement:
- RPS→Console is unauthenticated. Any caller on the internal network can register, merge or delete a device, or drive AMT operations, indistinguishably from RPS. Closing this needs a service token or mTLS between RPS and Console - an RPS change, deferred to a future release (proposed v3.1), out of scope for v3.0.
- The configuration reads run on the same path. Per 1 RPS reads profiles, wireless, 802.1x and domain configs over that same unauthenticated API.
Moving to Console + RPS requires a data migration, not only a Compose or routing change. MPS owns device inventory in mpsdb; RPS owns profiles and provisioning configuration in rpsdb; Vault holds device and configuration passwords today. The migration must preserve tenant ownership and references while avoiding a shared, incompatible database schema.
Open decisions: whether migration is online or requires downtime; which historical fields and records are in scope; how secrets and encrypted database fields are converted; whether the importer is one-time or rerunnable; and the supported rollback window and data-loss objective.
Proposal: provide a repeatable migration/export-import procedure with a pre-migration backup, write freeze, validation, cutover, and rollback. The migration must move passwords out of Vault into Console's database, and move the source configuration data into Console-owned storage, before Console becomes the source of truth.
| Source | Proposed target | Migration notes |
|---|---|---|
| mpsdb.devices | Console device inventory | Preserve GUID, tenant, tags, device metadata, activation/provisioning status, and CIRA username. Do not copy live socket state or MPS instance affinity; connections must be re-established. |
| rpsdb.profiles and profile links | Console profiles and associations | Convert fields and nullability to Console's model; preserve profile names, tenant IDs, tags, CIRA/wireless/802.1x references, and activation settings. |
| rpsdb.ciraconfigs, domains, wirelessconfigs, ieee8021xconfigs, proxy configuration | Corresponding Console configuration | Reconcile unsupported proxy data and domain certificate path differences before cutover; preserve relationships and tenant scope. |
| Shared Vault | Existing Vault, adopted in place | Preserve MPSCerts unchanged for brownfield; adopt or provision APP_ENCRYPTION_KEY there too. Migrate device and profile passwords out of Vault's devices/{guid} and profiles/{name} into Console's database columns, without writing plaintext secrets to export files. |
Proposed migration sequence:
- Inventory source databases, Vault paths, service versions, tenant mappings, and unsupported configuration; take restorable database and Vault backups.
- Freeze configuration and device-inventory writes, then export records and transform them into Console's schema using a repeatable importer or supported Console APIs. Do not point both services at the legacy shared schema.
- Preserve
MPSCertsin Vault unchanged, and adopt or provision APP_ENCRYPTION_KEY there; migrate device and profile passwords from Vault into Console's database, verifying by read checks without logging their values. Do not rotate or regenerate the MPS root during brownfield cutover. - Validate record counts, GUID uniqueness, tenant assignments, profile references, device metadata, certificate-chain continuity, and representative credential lookups before switching traffic.
- Switch the gateway/UI and RPS configuration provider during a planned cutover. Keep legacy databases and services read-only until the new deployment is accepted.
- Define rollback to the pre-cutover services and data snapshots, including how to handle writes made after cutover so rollback does not lose or split-brain data.
Console supports three deployment profiles. Passwords always live in Console's database; the CIRA certificate source and the app-encryption-key source are the startup selections, and their defaults preserve the existing standalone installation.
| Profile | Database | Passwords | CIRA certificate source | App encryption key | RPS |
|---|---|---|---|---|---|
| Standalone Console | Console-owned, including embedded | Database-encrypted columns | Local files; CIRA disabled by default | Local configuration | None |
| Cloud greenfield | One Console-owned database | Database-encrypted columns |
MPSCerts created once by deployment bootstrap, then read by Console |
Vault, provisioned once | Activation orchestrator |
| Cloud brownfield | Console-owned database populated from MPS/RPS data | Database-encrypted columns, migrated from Vault | Existing MPSCerts adopted in place and read by Console |
Vault, provisioned once | Activation orchestrator |
Proposal: unset provider settings mean standalone behavior. Cloud deployments must explicitly
select Vault-backed CIRA certificates and a Vault-backed app encryption key; passwords stay in the
database in every profile. Console must validate the selected provider at startup; in Vault
certificate mode, missing or unreadable MPSCerts is fatal. It must not fall back to local
certificate files or a newly generated identity.
Greenfield and brownfield use the same runtime profile after startup. They differ only in the deployment bootstrap: greenfield provisions the CIRA identity once before Console starts; brownfield retains the existing MPS identity and its existing Vault paths, and migrates its Vault passwords into Console's database.
Decision for v3.0: Console is deployed as a single instance. The CIRA listener keeps active
connections in process, and this proposal does not replace MPS Router's mpsinstance-based device
affinity. Horizontal Console scaling is unsupported in this release; devices must reconnect after
an instance restart. This is a regression from Cloud v2's horizontally routed MPS tier and must be
resolved before Console is scaled horizontally. The migrated mpsinstance value is not a routing
mechanism in Console mode.
Architecture-level feedback from reviewing this proposal against the Cloud v2 architecture reference and the current Console and RPS sources.
Scope reviewed: the end-state where Console owns every REST API, including the configuration APIs RPS serves in v2, and Console's database owns all the data behind them. RPS is then an activation, deactivation and maintenance orchestrator with no REST surface and no database.
The direction proposed is that Console adapts: it works with the single database, the shared Vault and the missing APIs, while the standalone on-premises deployment continues to work unchanged. The differences this rests on are recorded in differences from the current implementation.
Code references are permalinks pinned to Console 46b1d54 and RPS b797117, both on main, so the line numbers stay valid as the branches move.
Console has to serve three deployments from one codebase. Most of the feedback above applies to the two cloud profiles. The standalone profile is a constraint on the solution, not a target of it.
| Profile | Database | Secret store | CIRA listener | RPS | Status |
|---|---|---|---|---|---|
| Standalone Console (enterprise, on-premises) | Console's own, including embedded | Encrypted columns under APP_ENCRYPTION_KEY
|
Disabled; Console reaches AMT directly on the LAN | None | Ships today. Must keep working unchanged |
| Console + RPS cloud, greenfield | One Console database | Shared Vault | Enabled; MPSCerts created once at first deployment |
Orchestrates activation | Target of this ADR |
| Console + RPS cloud, brownfield | One Console database, migrated from mpsdb and rpsdb
|
Shared Vault, already populated | Enabled; MPSCerts adopted in place, never generated |
Orchestrates activation | Target of this ADR, decision 6 |
Proposal: Console adopts the single database and shared Vault for the cloud profiles, and adds the missing APIs, behind two selectable providers whose defaults reproduce standalone behaviour exactly.
| Abstraction | Default (standalone) | Cloud profiles |
|---|---|---|
| Secret provider |
database - encrypted columns, as today |
vault - device and profile secrets in the shared Vault at the paths RPS reads and writes |
| CIRA certificate source |
file - local certificate and key, CIRA off by default |
vault - MPSCerts read in place |
This is not a new idea for Console. The choice between Vault and the database is already in the code, just made in the wrong place:
-
Domains. Fetches the certificate from Vault when a certificate store is configured and the database column is empty, and carries on with whatever the database held when it is not (
domains/usecase.goL140-L152). -
Profile export. Tries to decrypt the domain's provisioning certificate password, and treats a failed decryption as "this one came from Vault as plaintext" (
profiles/usecase.goL231-L238).
Each read site decides for itself, so the answer can differ from one to the next, and nothing states what the deployment as a whole is doing.
Proposal: make it one choice, read once at startup, and apply it everywhere. Individual read sites stop deciding.
Three rules keep the standalone deployment safe:
-
Standalone is the default. A deployment that sets nothing new behaves exactly as it does today: secrets encrypted into Console's database (
devices/usecase.goL114-L128), certificates read from local files (app.goL220-L223). Nobody has to change a standalone install. - Never mix the two. A deployment reads its secrets from Vault or from the database, never from both. The either/or fallbacks in the code today are there to help migration; they are not a way to run.
-
Fail loudly, not quietly. In Vault mode, Console does not quietly fall back to the database if Vault is unreachable, and does not quietly generate a certificate if
MPSCertsis missing. It refuses to start.
Rule 3 matters most at cutover. Without it, one wrong Vault setting gives a Console that looks healthy, turns away every device that tries to authenticate, and offers a certificate no device recognises. The deployment appears to work and manages nothing. With rule 3, it stops at startup and says why.
Once running, the two cloud profiles are the same deployment. They differ in what already exists when Console first starts, which is what decision 6 has to cover.
| Concern | Greenfield | Brownfield |
|---|---|---|
| Device inventory (DB) | Empty; populated as devices activate | Migrated from mpsdb.devices, preserving GUID, tenant, tags, CIRA username and provisioning status |
| Configuration (DB) | Created through Console's APIs | Migrated from rpsdb profiles, CIRA, domains, wireless and 802.1x into Console's model |
MPSCerts (Vault) |
Created once at deployment and written to Vault | Adopted in place, never regenerated. See certificate reuse |
devices/{guid} secrets (Vault) |
Written by RPS as each device activates | Already present; Console begins reading them |
| Profile secrets (Vault) | Written by Console at the paths RPS reads | Already present for migrated profiles; verify by read check before cutover |
| CIRA connections (runtime, neither) | None yet | Break at cutover and re-establish against Console, provided the certificate precondition holds |
Nothing in the brownfield column requires a code path the greenfield profile does not also use. The migration is a data and ordering problem, not a second implementation.
Secrets stay where they are throughout. Because both cloud profiles read Vault, decision 6's "keep device credentials at devices/{guid}" is accurate as written: the migration moves device records, not device secrets, and nothing has to decrypt and re-encrypt a credential on the way.
The baseline the proposal rests on: what v2 does today, and where Console differs. Split by whether the proposal answers the difference or leaves it open.
Addressed by this proposal
| # | Concern | Cloud v2 (MPS + RPS) | Console today | How it is addressed |
|---|---|---|---|---|
| 1 | Device and profile secrets | Vault only, at devices/{guid} and profiles/{name}
|
Encrypted into Console's own database columns; Vault used narrowly, mainly for domain certificates | The secret provider in deployment profiles: Vault for the cloud profiles, the database for standalone. Decision 3 requirement 6 and the decision 1 gap 1 still name Vault paths Console does not read or write, so both need rewording |
| 2 | CIRA APF authentication | MPS verifies the device password against Vault devices/{guid}/MPS_PASSWORD
|
Console compares MPSUsername and MPSPassword read from its device record (cira/handler.go L107-L118) |
Console reads MPS_PASSWORD from devices/{guid} in the cloud profiles, so the password does not travel in the device API body and exists in one copy. Open item 3 |
| 3 | CIRA server TLS identity | MPS reads MPSCerts from Vault; self-signed fallback when absent |
Local certificate and key files, with common_name defaulting to a detected IP address |
MPSCerts read in place, with no generation and no fallback; see certificate reuse. Replaces decision 3 requirement 5 |
| 4 | Configuration API surface | RPS serves profiles, domains, CIRA, wireless, 802.1x and proxy CRUD | Console serves all of these except proxy, and has no domain-lookup-by-suffix route | Console adds the proxy entity and CRUD, and the domain-lookup-by-suffix route; see API additions |
Gaps this proposal does not yet answer
| # | Concern | Cloud v2 (MPS + RPS) | Console today | What is still undecided | What is expected |
|---|---|---|---|---|---|
| 5 | Horizontal scaling of the CIRA tier | MPS Router resolves mpsinstance from the database and TCP-proxies to the owning MPS |
Single in-process CIRA listener; the mpsinstance column exists with no routing tier behind it |
Whether v3.0 is single-instance Console, or device affinity gets a replacement design. Either is defensible; saying nothing reads as an oversight rather than a decision, and v2 shipped horizontal MPS scaling as a feature | A stated position in this ADR. If single-instance: say so, record it as a regression from v2, and either drop mpsinstance or document it as unused. If scaled: a device-affinity design, covering how Console claims a device on CIRA connect, releases it on shutdown, and how a request reaches the instance holding the connection |
| 6 | Unauthenticated internal surface | Device operations behind the gateway | Same model proposed, plus the configuration reads | Decision 5 claims parity with the 2.x trust model. It is wider: provisioning certificates, profile secrets and CIRA credentials now cross the same unauthenticated path. Needs restating, with what crosses it enumerated | Decision 5 reworded to claim a wider surface than 2.x rather than parity, listing what crosses it; the "8181 is never published to the host" invariant stated as a deployment requirement rather than an assumption; and the service token or mTLS between RPS and Console recorded as deferred, with the release it is expected in |
Selecting the Vault provider is not enough on its own. Console and RPS have to use the same paths, and today they do not.
| # | Change | Why |
|---|---|---|
| 1 | Console writes profile, CIRA and wireless secrets at the paths RPS reads | RPS's read sites stay unchanged; see the decision 1 gap 1 |
| 2 | Align the domain certificate path, or adapt RPS's read of it | Console writes certs/domains/{tenantID}/{profileName}; RPS reads certs/{profileName}. ACM activation fails while these disagree |
| 3 | Normalise the Vault base path before joining keys | Already decision 3 requirement 4; avoids double slashes on the shared secret/data/ path |
Decision 3 requirement 5 proposes seeding MPSCerts from Console's root certificate. For existing deployments this is reversed, and it is the single item that decides whether migrated devices reconnect at all. The MPS root is load-bearing in two places on the firmware side:
-
CIRA trust. RPS installed the MPS root into AMT's trusted root store with
AddTrustedRootCertificateat activation. Console's CIRA listener must present a leaf chaining to that same root, or the TLS handshake fails permanently. AMT does not retry its way out of a trust failure. -
The E2E TLS device leaf. Each device certificate at
devices/{guid}/TLS_ISSUED_CERTIFICATEwas signed by the MPS root private key, and AMT holds the matching private key internally. It cannot be reissued without re-runningGenerateKeyPairandAddCertificateon the device.
Proposal: replace requirement 5 with an in-place read. Console reads the existing MPS root and server TLS material from MPSCerts unchanged; it neither generates a root nor relocates the existing one. A Console deployment that cannot read MPSCerts must fail to start rather than fall back to generating a self-signed set, because that fallback turns a Vault misconfiguration at cutover into a silent loss of every migrated device.
The missing APIs are Console's to add. Each is additive, so none of them changes standalone behaviour: a standalone deployment gains endpoints and an entity it need not use.
| # | Addition | Why | Standalone impact |
|---|---|---|---|
| 1 |
proxyconfigs entity, CRUD at /api/v1/admin/proxyconfigs, a profile link table, and proxyConfigs returned in the profile read |
Console has no proxy entity; proxy is a proxydetails column on ciraconfigs. RPS's model has five fields and its profile read joins a link table. Without this, rpsdb proxy configuration has no migration destination and migrated profiles silently lose the capability |
New, unused unless configured |
| 2 | Domain lookup by domain suffix | RPS resolves the ACM provisioning certificate by DNS suffix, not by name. Every ACM activation depends on it. The usecase method exists; only the route is missing | New route, additive |
| 3 | Upsert on POST /api/v1/devices
|
RPS posts the same GUID more than once per activation; a 409 on any of them fails provisioning. Already decision 4 | Changes a 409 to a merge; no standalone caller relies on the conflict |
| 4 | DELETE /api/v1/devices/refresh/{guid} |
Console caches a WSMAN connection per GUID and would otherwise keep using the old password after a rotation. Already decision 4 | New route, additive |
Two questions to settle alongside them:
-
Do Console's APIs match RPS's? ISVs build against the RPS REST API today, and the toolkit documentation tells them to. If the request and response shapes differ, this is a breaking change and has to be called one. RPS serves proxy CRUD that Console has no entity for (
proxy/index.tsL18-L22), and Console has the domain-by-suffix lookup as a usecase with no route behind it (domains/usecase.goL98). -
Who updates Console's copy of the AMT password? RPS changes the password during maintenance and writes it to
devices/{guid}in Vault (changePassword.tsL102-L107), while Console keeps its own encrypted copy (devices/usecase.goL114-L128). The refresh route in decision 4 does not exist in Console yet; once added it would only drop the cached connection, so a stale stored copy is read again.
Proposal: adapt Console in both cases, and leave RPS's behaviour as it is.
| # | Adaptation in Console | Why this way round |
|---|---|---|
| 1 | Keep Console's API as the published contract, and declare RPS's configuration API withdrawn in the cloud profiles rather than reproduced. Publish a field mapping for the known differences: profile tags, wireless link policy, the proxy model, and domain lookup by suffix | RPS's API and its schema are internal and unversioned. Making Console match them field for field rebuilds exactly the coupling that decision 1 rejects option 2 for. A mapping ISVs can read beats a compatibility layer nobody can version. Some of the mapping is already free: Console's profile DTO returns tags as an array (profile.go L37) |
| 2 | In Vault mode, Console does not keep its own copy of device credentials. It reads the AMT and MPS passwords from devices/{guid} at the point of use, so the refresh route only has to drop the cached connection |
RPS already writes the rotated password to devices/{guid} at the end of maintenance, so nothing new is asked of RPS. The stale-copy problem disappears rather than being synchronised, and the same change closes the two-copies difference in row 2 above |
One ordering requirement follows from adaptation 2: RPS must write the new password to Vault before it calls the refresh route. Refreshing first makes Console re-read and re-cache the old value.
The review above is written as feedback. These are the edits to the decisions themselves, where the current text states something the code contradicts or a position it does not yet take. Rows 1 to 6 are the decision text; rows 7 to 12 are the diagrams.
| # | Change | Where | Status |
|---|---|---|---|
| 1 | Promote the deployment profiles and the two selectable providers to a numbered decision, with standalone as the default | new decision, extends 3 | Fixed |
| 2 | Rewrite requirement 5: Console reads the existing MPSCerts in place, rather than seeding it from Console's root certificate. Add the fail-fast rule, with no file-path or self-signed fallback |
3 | Fixed |
| 3 | Reword requirement 6 and the option 1 gap 1 so they stop naming Vault paths Console neither reads nor writes, and state the paths Console will use | 3, 1 | Fixed |
| 4 | Replace the parity claim with a statement that the surface is wider than 2.x, listing what crosses it, and record the "8181 is never published to the host" invariant as a deployment requirement | 5 | Fixed |
| 5 | Add the scaling position: single-instance Console for v3.0, or a device-affinity design replacing the MPS Router | new decision | Fixed |
| 6 | Record that RPS's configuration API is withdrawn in the cloud profiles, with the field mapping ISVs need | 1 | Fixed |
| 7 | Correct the roles diagram. Option 2's configuration read is drawn as an arrow into Console, but under option 2 RPS reads Console's database directly. As drawn, the two options are indistinguishable | microservice roles | Fixed |
| 8 | Add the CIRA TLS handshake to the activation flow: Console loading MPSCerts, presenting the server certificate, and AMT validating it against the MPS root installed at activation. No diagram shows the certificate path, which is what brownfield reconnection depends on |
activation flow, certificate reuse | Fixed |
| 9 | Label the Vault edges. The component map and inter-service diagrams both draw RPS --> Vault and Console --> Vault unlabelled, so the devices/{guid} write-then-read handoff that decision 3 turns on is invisible |
component map, inter-service communication | Fixed |
| 10 | Draw the device registration POST repeating in the activation flow. It appears once, which hides the duplicate registration that the upsert requirement in decision 4 exists to handle | activation flow | Fixed |
| 11 | Draw the v2 layout alongside the proposal in the database layouts diagram, or retitle it. The heading is plural and the lead-in compares two deployment modes, but only the proposal is drawn, and with the deployment profiles there are now three | database layouts | Fixed |
| 12 | Mark what the component map leaves implicit: that Console's CIRA listener is a single instance with no routing tier, and that 8181 is internal-only. Both are load-bearing and neither is visible |
component map | Fixed |
Two verification tasks come with them, neither of which is an edit:
- Confirm device GUID uniqueness is global rather than per-tenant, before migration merges tenants into one database.
- Add standalone-profile regression coverage to CI, so adopting the shared Vault and single database does not regress the on-premises deployment.
Replacements for the four diagrams rows 7 to 12 refer to. Each carries the same information as the original plus the missing parts.
Red marks what is new or changed relative to the diagram it replaces. Everything in the original styling is unchanged. The red is a review aid; drop it once the diagrams are adopted.
Microservice roles - fixes row 7, where option 2's read is drawn into Console instead of the database, and labels the Vault edges from row 9.
graph LR
RPS["RPS<br/>activation · deactivation · maintenance<br/>profile application · device registration"]
Console["Console<br/>devices · AMT operations · CIRA/APF listener<br/>profiles and all configuration · login"]
CDB[("Console database")]
Vault[("Vault")]
RPS -->|"POST /api/v1/devices, upsert"| Console
RPS -. "option 1, configuration over Console's API" .-> Console
RPS -. "option 2, configuration direct from the database" .-> CDB
Console --> CDB
RPS -->|"write devices/{guid}, read profile secrets"| Vault
Console -->|"read MPSCerts and devices/{guid}, write profile secrets"| Vault
style RPS fill:#1976D2,color:#fff
style Console fill:#1565C0,color:#fff
%% new nodes
style CDB fill:#C62828,color:#fff
style Vault fill:#C62828,color:#fff
%% new edges: option 2 to the database, and the Vault reads and writes
linkStyle 2,3,4,5 stroke:#C62828,stroke-width:2px
Component map - adds the single-instance CIRA listener and the internal-only boundary from row 12, and the labelled Vault edges from row 9.
graph TB
subgraph External["External Network"]
Client[API Client / Browser]
end
subgraph Device["Client Machine (vPro/ISM)"]
RPC["RPC-Go<br/>(OS Layer)"]
AMT["Intel AMT<br/>(Firmware Layer)"]
end
subgraph Cloud["Cloud Services"]
Kong["Kong API Gateway<br/>:443 HTTPS<br/>JWT · CORS · TLS termination"]
subgraph Core["Core Services - internal network only, never published to the host"]
Console["Console, single instance<br/>:8181 REST API + WS Relay<br/>:4433 CIRA (TLS+APF)<br/>connections held in process"]
RPS["RPS<br/>:8080 WebSocket (activation)<br/>config API off (RPS_MODE=console)"]
end
subgraph Backing["Backing Services (3rd Party)"]
DB["Console database :5432"]
Vault["Vault :8200<br/>MPSCerts · devices/guid · profile secrets"]
end
WebUI["Sample Web UI<br/>nginx :80"]
end
RPC -->|"LMS (APF) :16992/16993"| AMT
Client -->|HTTPS :443| Kong
RPC -->|"WSS :443 /activate"| Kong
AMT -->|"TLS :4433 (CIRA)"| Kong
Kong -->|"/api, /api/v1/authorize, /relay"| Console
Kong -->|"/activate, /deactivate, /maintenance"| RPS
Kong -->|/| WebUI
Kong -->|":4433 TCP passthrough"| Console
RPS -->|"POST /api/v1/devices, upsert"| Console
RPS -. "option 1, configuration API" .-> Console
RPS -. "option 2, direct DB and schema compatibility required" .-> DB
Console --> DB
Console -->|"read MPSCerts for the CIRA listener"| Vault
Console -->|"read devices/{guid}, write profile secrets"| Vault
RPS -->|"write devices/{guid}, read profile secrets"| Vault
style Console fill:#1565C0,color:#fff,stroke:#C62828,stroke-width:3px
style RPS fill:#1976D2,color:#fff
style WebUI fill:#2196F3,color:#fff
style RPC fill:#42A5F5,color:#fff
style Kong fill:#616161,color:#fff
style DB fill:#616161,color:#fff
style Vault fill:#616161,color:#fff,stroke:#C62828,stroke-width:3px
style Client fill:#616161,color:#fff
style AMT fill:#616161,color:#fff
style Device fill:#EEEEEE,color:#000
style Cloud fill:#ffffff,color:#000,stroke:#1565C0
style Core fill:#E3F2FD,color:#000,stroke:#C62828,stroke-width:3px
style Backing fill:#FAFAFA,color:#000
style External fill:#FAFAFA,color:#000
%% new edges: the three labelled Vault paths
linkStyle 12,13,14 stroke:#C62828,stroke-width:2px
Red outlines mark the three changed labels: the Core subgraph gains the internal-only boundary, Console gains the single-instance note, and Vault names what it holds.
Activation flow - adds the certificate path from row 8 and the repeated registration from row 10.
sequenceDiagram
autonumber
participant Admin
participant Kong
participant Console
participant DB as Console database
participant RPS
participant Vault
participant RPC as RPC-Go
participant AMT
rect rgb(255, 235, 238)
Note over Console,Vault: New. Startup. Console refuses to start if MPSCerts is missing.
Console->>Vault: Read MPSCerts, root and server certificate
Vault-->>Console: Certificate material, read in place
Console->>Console: Start the CIRA listener on 4433
end
Admin->>Kong: Create profile and related configuration
Kong->>Console: Authenticated Console API request
Console->>DB: Store profile/configuration
Console->>Vault: Store profile secrets at the paths RPS reads
RPC->>Kong: WSS /activate
Kong->>RPS: Activation request
RPS->>Console: Read profile/configuration (option 1 API, option 2 database)
RPS->>Vault: Read profile secrets
loop WSMAN round-trips
RPS->>RPC: WSMAN request
RPC->>AMT: Forward via LMS/MEI
AMT-->>RPC: WSMAN response
RPC-->>RPS: Relay response
end
rect rgb(255, 235, 238)
Note over RPS,AMT: New. CIRA configuration installs the MPS root into AMT's trusted store.
end
RPS->>Vault: Write device credentials at devices/{guid}
rect rgb(255, 235, 238)
Note over RPS,DB: New. Drawn once before, which hid the upsert requirement.
loop Registration, CIRA registration and provisioning status
RPS->>Console: POST /api/v1/devices
Console->>DB: Insert, or merge when the GUID already exists
end
end
rect rgb(255, 235, 238)
Note over Console,AMT: New. The certificate path brownfield reconnection depends on.
AMT->>Kong: TLS ClientHello on 4433
Kong->>Console: TCP passthrough to the CIRA listener
Console-->>AMT: Server certificate from MPSCerts
AMT->>AMT: Validate against the MPS root installed at activation
AMT->>Console: APF USERAUTH_REQUEST
end
Console->>DB: Read device record for MPSUsername
Console->>Vault: Read MPS_PASSWORD at devices/{guid}
Console-->>AMT: APF USERAUTH_SUCCESS
Red-tinted blocks are new. The unshaded steps are carried over from the existing activation flow.
Database layouts - fixes row 11 by drawing all three, including the v2 layout the brownfield migration reads from.
flowchart TB
subgraph SA["Standalone Console"]
ConsoleSA[Console] --> SADB[("Console database<br/>devices, configuration,<br/>secrets encrypted in columns")]
end
subgraph V2["Cloud v2, MPS + RPS, the brownfield source"]
MPS2[MPS] --> MPSDB[("mpsdb<br/>devices")]
RPS2[RPS] --> RPSDB[("rpsdb<br/>configuration")]
MPS2 --- V2Vault[("Vault<br/>MPSCerts, devices/guid,<br/>profile secrets")]
RPS2 --- V2Vault
end
subgraph CP["Console + RPS cloud, greenfield and brownfield"]
ConsoleC[Console] --> CDB[("Console database<br/>devices and configuration")]
RPSC[RPS] -. "option 1, configuration API" .-> ConsoleC
RPSC -. "option 2, direct DB" .-> CDB
ConsoleC --- CVault[("Vault<br/>MPSCerts, devices/guid,<br/>profile secrets")]
RPSC --- CVault
end
V2 -. "migrate records, secrets stay in Vault" .-> CP
%% new: the standalone and v2 layouts, and the migration edge
style SA fill:#FFEBEE,color:#000,stroke:#C62828,stroke-width:3px
style V2 fill:#FFEBEE,color:#000,stroke:#C62828,stroke-width:3px
linkStyle 10 stroke:#C62828,stroke-width:2px
The existing diagram drew only the Console + RPS layout. The two red subgraphs and the migration edge are new.
The recommendation of option 1 over option 2 in decision 1 is unaffected by the above, and observation 2 in the problem table remains the strongest argument for it: a missing relation in RPS's unversioned schema surfaced to the device as profile-not-found.