console rps deplyment design - device-management-toolkit/deployment GitHub Wiki

Console + RPS Deployment Design

Supporting design and implementation documentation for the Console + RPS Cloud Deployment ADR (deployment#614).

Contents

Abstract

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.

Console + RPS observations

Observations and what breaks when Console and RPS run together, without the decisions in this ADR.

Local activation

Deployed the stack with make up, which brings up console, rps, db, vault, kong and webui. Local activation with Console works.

Remote activation

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

Reference flows

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.

Other Design Decisions

These are proposals for review, not settled decisions.

  1. 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 MPSCerts into Vault unchanged, with verification and rollback. See 6.

  2. 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.

  3. 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.

Diagrams

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.

Cloud deployment

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
Loading

Component map

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
Loading

Inter-service communication

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
Loading

RPS reads configuration through Console's API and does not connect to a database or to Vault directly.

Database layouts

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
Loading

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.

Microservice roles and responsibilities

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
Loading

Activation flow

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
Loading

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.

Detailed design by decision

1 Console owns profiles, RPS orchestrates provisioning

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

2 RPS supports both deployments

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.

3 One database, shared Vault

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

4 Device API compatibility

Console must serve the endpoints RPS calls, and persist status, activatedAt and lastProvisionedAt in deviceInfo:

  • GET /api/v1/devices/{guid}?tenantId=...
  • POST /api/v1/devices
  • PATCH /api/v1/devices
  • DELETE /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.

5 Trust boundary

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.

6 Migration from MPS + RPS

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:

  1. Inventory source databases, Vault paths, service versions, tenant mappings, and unsupported configuration; take restorable database and Vault backups.
  2. 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.
  3. Preserve MPSCerts in 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.
  4. Validate record counts, GUID uniqueness, tenant assignments, profile references, device metadata, certificate-chain continuity, and representative credential lookups before switching traffic.
  5. 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.
  6. 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.

7 Deployment profiles and provider selection

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.

8 Console scaling and device affinity

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.

Review feedback

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.

Deployment profiles

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.go L140-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.go L231-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:

  1. Standalone is the default. A deployment that sets nothing new behaves exactly as it does today: secrets encrypted into Console's database (devices/usecase.go L114-L128), certificates read from local files (app.go L220-L223). Nobody has to change a standalone install.
  2. 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.
  3. 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 MPSCerts is 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.

Greenfield and brownfield differ only at first start

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.

Differences from the current implementation

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

Vault paths Console and RPS must agree on

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

Certificate reuse is the brownfield precondition

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:

  1. CIRA trust. RPS installed the MPS root into AMT's trusted root store with AddTrustedRootCertificate at 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.
  2. The E2E TLS device leaf. Each device certificate at devices/{guid}/TLS_ISSUED_CERTIFICATE was signed by the MPS root private key, and AMT holds the matching private key internally. It cannot be reissued without re-running GenerateKeyPair and AddCertificate on 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.

API additions accepted as Console scope

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.ts L18-L22), and Console has the domain-by-suffix lookup as a usecase with no route behind it (domains/usecase.go L98).
  • Who updates Console's copy of the AMT password? RPS changes the password during maintenance and writes it to devices/{guid} in Vault (changePassword.ts L102-L107), while Console keeps its own encrypted copy (devices/usecase.go L114-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.

Changes the ADR body needs

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.

Proposed diagrams

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
Loading

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
Loading

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
Loading

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
Loading

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.

⚠️ **GitHub.com Fallback** ⚠️