Skip to content

Benefits Agent — Setup and Operations#

The Agent is a self-hosted Windows Service that runs on each deployment target. It exposes an HTTP API that bmo calls to upload and activate packages in IIS. A built-in SPA dashboard lets operators manage installations, Windows services, and IIS app pools from a browser without needing the CLI.


Prerequisites#

  • Windows Server with IIS installed (Web Server role enabled)
  • .NET 10 Runtime (or higher)
  • The service account must be able to create TempPath and each environment's BasePath (e.g. F:\Acceptatie, F:\Productie), and to administer IIS — the agent creates and repairs the environment sites itself at startup (see What the agent does at startup); do not create them by hand
  • Inbound firewall rule on port 5100 (or your configured port), and on each environment's Binding port
  • A server certificate if you want HTTPS or mTLS

Installation#

Publish or copy the Benefits.Agent binaries to a local directory, then run the install script from that directory, in an elevated PowerShell:

cd C:\Benefits\Agent
.\Install.ps1

The script:

  1. Registers the service Benefits-Mgmt-Agent (startup type Automatic). An existing service keeps its registration and account, so the same command updates an installed agent after new binaries were copied over.
  2. Sets service recovery: restart on every failure (after 5 s, 30 s, then every minute; the count resets after a day), with failureflag 1. A restart on request from CommandCenter depends on it: the agent exits non-zero and recovery starts it again. The agent refuses a restart request while recovery is not set, so run Install.ps1 once more on agents installed before this version.
  3. With -AppConfigEndpoint, sets up shared configuration (below).
  4. Starts the service, or restarts it when it was running.
Parameter
-ServiceName Default Benefits-Mgmt-Agent. Another name is written to Agent:ServiceName too, so the agent can find its own service.
-AgentName Agent:Name; default the machine name.
-AppConfigEndpoint, -KeyVaultUri The App Configuration store and the Key Vault its references point to.
-Identity managed-identity or certificate. Without it the script asks, and suggests a managed identity on an Azure VM or an Arc-connected server.
-ManagedIdentityClientId A user-assigned identity; empty for the system-assigned one.
-TenantId, -ClientId, -CertificateSubject For certificate: the app registration and the subject of the machine certificate in LocalMachine\My.

The service runs under LocalSystem by default. If your deployment paths or SQL connections require a named account, change it in the Services MMC snap-in after installation.

Start, stop, restart:

Start-Service   Benefits-Mgmt-Agent
Stop-Service    Benefits-Mgmt-Agent
Restart-Service Benefits-Mgmt-Agent

Configuration#

The settings live in appsettings.json (and appsettings.Production.json for production overrides) next to the executable. Restart the service after a change there. With shared configuration, the settings below can also be managed centrally, and some of them apply without a restart.

Shared configuration (App Configuration)#

An agent can read its settings from the same Azure App Configuration store as CommandCenter, so they are managed in one place instead of on each server. It is optional: an agent without Configuration:AppConfig behaves exactly as before.

flowchart LR
    subgraph Server["Windows server"]
        Agent["Benefits.Agent<br/>(Windows service)"]
        Local["appsettings.json<br/>(base and fallback)"]
        Env["Service environment<br/>Configuration:* (Install.ps1)"]
    end
    AC["App Configuration<br/>platform/agent<br/>platform/agent/{name}"]
    KV["Key Vault"]
    SB["Service Bus<br/>agent.state.v1"]
    CC["CommandCenter<br/>/config/agents"]
    Local --> Agent
    Env --> Agent
    Agent -- "managed identity or certificate" --> AC
    AC -. "Key Vault references" .-> KV
    Agent -- "heartbeat + configuration state" --> SB --> CC
Hold "Alt" / "Option" to enable pan & zoom

What it reads. Label platform/agent (every agent), then platform/agent/{name} (this agent; wins), where {name} is Agent:Name in lower case. It does not read platform or unlabelled keys: those hold the gateway's and the backends' settings. Order, later wins: appsettings.json → App Configuration → environment variables → command line. A local environment variable therefore still overrides the store, and is reported as a shadowed key. Agent:Name and Configuration:* always stay local: they choose the store and the label.

Bootstrap (Configuration:*, written by Install.ps1 as environment variables of the service):

Key
Configuration:AppConfig:Endpoint The store.
Configuration:KeyVault:Uri The only vault whose references resolve; any other reads as empty and is reported.
Configuration:Identity:Mode managed-identity or certificate. Empty: DefaultAzureCredential, as before.
Configuration:Identity:ManagedIdentityClientId A user-assigned managed identity.
Configuration:Identity:TenantId, ClientId, CertificateSubject A service principal that signs in with the newest valid certificate of that subject in LocalMachine\My, chosen again every hour and after a failed sign-in, so a renewed certificate needs no change. Never pinned by thumbprint.
Configuration:AppConfig:RefreshInterval How often the sentinel is checked; default 30 s.
Configuration:AppConfig:StartupTimeout How long the first load may take; default 10 s.

The identity is used for everything the agent reaches in Azure: App Configuration (App Configuration Data Reader), Key Vault (Key Vault Secrets User), Service Bus and blob storage. On an Arc-connected server the managed identity works for services running as LocalSystem (the default) or as a member of Hybrid agent extension applications.

When a change applies. The agent reloads when Settings:PlatformSentinel (label platform) moves, and sends a heartbeat right away. These settings apply without a restart:

Setting
Agent:MonitoringInterval Heartbeat interval.
Agent:ProtectedAppPools App pools the cleanup never removes.
ClientAuthentication:AllowedSubjects, TrustedIssuers Who may call the agent over HTTPS.
Serilog:MinimumLevel:Default, Serilog:MinimumLevel:Override:* Log levels.

Everything else (port, Service Bus, certificates, package store, environments) is read at start: a change is reported as restart-pending until the agent restarts.

Lists are one value. In the store, a list setting is one JSON array without a JSON content type, for example ["CN=cc-webapi.example.nl"]. It replaces the local list. Stored as separate :0, :1 keys it would merge with the local entries by index, and a list could then be widened centrally but never shortened.

When the store is away. The agent starts on its local settings after StartupTimeout, reports Unreachable, and loads the store as soon as it answers (typically within a minute). It never refuses to start over it.

When a value is wrong. A value the agent cannot use (a duration typed as 5m) is logged (EventId 2205) and reported in configuration.lastError; the heartbeat keeps its last good settings, and the agent keeps running until the value is corrected. Whatever else reads that setting fails until then (a request that checks the client-certificate lists is refused, for example). An agent that stops because a background service failed exits with code 1 (EventId 2213), so service recovery restarts it and it loads the corrected store.

What it reports. The heartbeat and GET /api/agent/info carry configuration: the source (LocalOnly, AppConfiguration or Unreachable), the revision (the sentinel's value) and when it was applied, the revision it started with, restart-pending and shadowed key names, unresolved Key Vault references, and whether it can restart on request (restartSupported, with the reason when not). Key names only, never a value.

Restart on request#

POST /api/agent/restart (HTTP) and the Service Bus command Restart restart the agent without anyone signing in to the server. In CommandCenter this is the Restart button on the Configuration tab of /config/agents, where the agent settings are edited too (Agents):

sequenceDiagram
    participant CC as CommandCenter
    participant A as Agent
    participant SCM as Service Control Manager
    CC->>A: Restart
    alt no service recovery, or a deployment running or queued
        A-->>CC: 409 with the reason
    else
        A-->>CC: 202 Accepted
        Note over A: the message is completed first,<br/>so it is not delivered again
        A->>A: graceful stop (workers, web server)
        A->>SCM: exits with code 1
        SCM->>A: recovery starts the service
        A-->>CC: heartbeat at start: new revision, nothing restart-pending
    end
Hold "Alt" / "Option" to enable pan & zoom

A restart is refused (409) when the agent does not run as its Windows service (Agent:ServiceName, default Benefits-Mgmt-Agent), when that service's recovery does not restart it on every failure (run Install.ps1), or while a deployment is running or queued. A Service Bus Restart that is delivered a second time, or was sent more than five minutes before the agent started, is ignored, so a redelivered message cannot restart it twice.

Agent section#

"Agent": {
  "Port": 5100,
  "TempPath": "F:\\Temp",
  "MaxRequestBodySizeBytes": 524288000,
  "MonitoringInterval": "00:05:00",
  "Name": "",
  "WriteDeploymentRecords": true,
  "ProtectedAppPools": []
}
Setting Default Notes
Port 5100 Kestrel listen port
TempPath F:\Temp Scratch space for uploaded zips. Must be writable by the service account.
MaxRequestBodySizeBytes 524288000 (500 MB) Max upload size for --via http deploys
MonitoringInterval 00:05:00 (5 min) How often StateMonitorService scans installations
Name MachineName Agent identifier reported to the Orchestrator. Override when multiple agents share a hostname.
WriteDeploymentRecords true Persist deployment audit records to the database
ProtectedAppPools [] IIS app pools that management API calls are blocked from touching

Environments#

One entry per deployment environment. The key (Acceptatie, Productie) must match the environment name used in bmo deploy.

"Agent": {
  "Environments": {
    "Productie": {
      "BasePath": "F:\\Productie",
      "Binding": { "Host": "*", "Port": 5002, "UseHttps": false },
      "CommandCenterUrl": "http://vmbenefitsccp1:8000",
      "ConnectionStrings": {
        "BenefitManager": "Data Source=localhost;Initial Catalog=BenefitManager;Integrated Security=True",
        "CommandCenter": "Data Source=localhost;Initial Catalog=ReverseProxy;Integrated Security=True"
      },
      "NamingStrategy": "PerMajorMinor",
      "AppPoolPrefix": null
    }
  }
}
Setting Default Notes
BasePath — Root folder for this environment's deployments; also the IIS site's physical path
Binding — (required) The IIS site's HTTP binding: Host is * (any host name) or one host name, Port 1–65535. UseHttps must be false — HTTPS bindings are added by an operator, the agent does not manage certificates for sites. A binding that another site already uses is refused, never taken over
CommandCenterUrl — Internal URL of the CommandCenter reverse-proxy for this environment
ConnectionStrings.BenefitManager — SQL connection for BenefitManager data
ConnectionStrings.CommandCenter — SQL connection for the reverse-proxy database
NamingStrategy PerMajorMinor IIS App Pool naming convention. See pipeline-migration-bmo-package-push.md for the full strategy table.
AppPoolPrefix null (uses env name) Override the prefix portion of the generated pool name

Agent:PrepareEnvironmentsOnStartup (default true) switches the startup preparation below off where IIS is managed by something else; the agent then only reports what it finds.

What the agent does at startup#

Deployments land as IIS applications under a site named after the environment (Acceptatie, Productie), so that site has to exist before the first deploy. A new or existing agent makes sure of that itself, once, when the service starts — an operator no longer creates the sites by hand:

  1. TempPath and every environment's BasePath are created if missing.
  2. For each environment, the IIS site named after it is created if missing — with the Binding, BasePath as physical path and a root application pool of the same name (ApplicationPoolIdentity) — or repaired if it exists but its physical path, root pool or binding differ from the configuration. Repairs are logged as warnings (EventId 2043) so a change to IIS is never silent. A binding that would collide with another site fails that environment instead of stealing it.
  3. Every application pool on the site is granted read-and-execute on the path it serves (traverse-only on BasePath for applications elsewhere). Existing rules are kept; an explicit deny fails the environment.

Each environment is prepared independently. A failure does not stop the agent and does not affect the other environments; it is recorded and shows up in three places:

  • /health (and therefore /api/agent/info and the heartbeat) reports Degraded with the failed environments and the reason — CommandCenter shows it on /config/agents. Degraded, not Unhealthy: an unhealthy host refuses every deployment, a broken Acceptatie site is no reason to stop deploying to Productie.
  • A deployment to the failed environment is refused up front (HTTP 503 on /api/deploy, Failed on the Service Bus reply) with the recorded reason.
  • The log: Prepared IIS environment (2040) per environment, Could not prepare (2041) with the exception, Created IIS site (2042), Repaired IIS site (2043), Granted … read access (2044), Could not create Agent:TempPath (2045).

Fix the cause — configuration, IIS permissions, a port in use — and restart the service; preparation runs at startup only, there is no endpoint to re-run it.

Authentication#

How registration, the two channels and certificate trust fit together — with context, data-flow and user-journey diagrams — is on Agents: registration, transport and authentication.

The Agent's HTTP API accepts mTLS client certificates and, when configured, Entra ID tokens. The API key is deprecated and off by default. Service Bus remains the preferred channel; everything below concerns HTTP only.

mTLS (the caller is CommandCenter)

The agent trusts an issuer and a subject, never one certificate, so CommandCenter's certificate renews (Key Vault, auto-enrollment, a replaced file) without anyone touching the server:

"ServerCertificate": {
  "Source": "Store",
  "StoreLocation": "LocalMachine",
  "StoreName": "My",
  "Subject": "tas-bm02.bcs.nl"
},
"ClientAuthentication": {
  "AllowedSubjects": ["CN=commandcenter-webapi, O=BCS"],
  "TrustedIssuers": ["BCS Issuing CA"],
  "TrustedRootFiles": [],
  "RevocationMode": "NoCheck"
}

With the BCS PKI ([Root CA] → [Intermediate] → [short-lived client certificate], the Root and the Intermediates installed on every machine):

  • TrustedRootFiles stays empty: the Root is in the machine store, and the chain is built from the machine's Intermediate Certification Authorities store.
  • TrustedIssuers names the client intermediate (e.g. BCS Client Issuing CA), not the Root. The PKI has several intermediate types under one Root; trusting the Root alone would let a certificate from the server intermediate authenticate as a client.
  • RevocationMode is NoCheck, because the BCS CAs publish no CRL or OCSP yet: the short lifetime of the client certificates is the control. Switch to Online once revocation is published (#311; for example smallstep/step-ca, or a service around Key Vault) — Online against a CA without reachable revocation data refuses every certificate as "revocation status unknown". The code default is Online, so a CA that does publish revocation is checked unless configured otherwise.
  • CommandCenter sends its intermediate in the handshake (its PFX carries the chain), so an agent without the intermediate installed still validates it.

  • AllowedSubjects — a distinguished name (compared whole) or a plain name (compared with the common name and DNS names). Required: with no subjects and no thumbprints, no certificate is accepted. (Before, an empty list accepted any certificate, self-signed ones included.)

  • TrustedIssuers — the issuing CA, by distinguished or common name, that must appear above the certificate in its chain: the public CA's issuing certificate, or the internal BCS CA. Chosen per environment.
  • TrustedRootFiles — root certificates (PEM/DER) to trust instead of the machine's store, for an internal CA that is not installed there.
  • AllowedThumbprints — optional extra pin on exact certificates; a renewal changes the thumbprint.
  • RevocationMode — Online (the code default), Offline or NoCheck (where the CA publishes no reachable revocation data; shipped as NoCheck for the BCS PKI, see below).
  • Self-signed client certificates are never accepted.

The server certificate is chosen at every TLS handshake, re-read hourly: with Subject, the newest valid certificate of that subject (with its private key) in the store, so a renewed one is used without a restart. Thumbprint (one exact certificate) and KeyVault (KeyVaultUrl, CertificateName) still work. With a server certificate the agent listens on HTTPS on Agent.Port; mTLS needs https:// in CommandCenter's agent address.

Source value Required fields
Store (default) StoreLocation, StoreName, and Subject (preferred) or Thumbprint
KeyVault KeyVaultUrl, CertificateName

On the CommandCenter side the certificate is Agents:ClientCertificate in WebApi (Source KeyVault / Store / File), refreshed hourly and chosen per connection.

API key (deprecated)

"Authentication": {
  "ApiKey": ""
}

The scheme is registered only when ApiKey is set, and the agent logs a deprecation warning (EventId 2031). It remains for BMO's own CommandCenter, which still calls agents with it, until that is retired. One shared secret cannot roll over without touching every server. The key that was committed in appsettings.Production.json (123-abcd) was removed; treat it as known and replace it wherever it is still in use.

Entra ID (JWT)

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "your-tenant-id",
  "ClientId": "your-client-id"
}

The JWT scheme is only registered when ClientId is non-empty. Leave all AzureAd fields empty to disable it.

Package Store (blob, for --via blob deploys)#

"PackageStore": {
  "AccountUri": "https://<storageaccount>.blob.core.windows.net/",
  "ContainerName": "deployments"
}

When AccountUri is set, the Agent downloads packages from blob storage for --via blob deploys. If the URL contains a query string it is treated as a SAS URL; otherwise DefaultAzureCredential (managed identity) is used. Leave AccountUri empty for HTTP-only mode.

Service Bus transport (optional)#

"Agent": {
  "ServiceBus": {
    "Namespace": "my-namespace.servicebus.windows.net",
    "StateTopic": "agent.state.v1",
    "SnapshotContainerUrl": "https://...",
    "CommandsTopic": "deploy.commands.v1",
    "RepliesTopic": "deploy.replies.v1",
    "AgentCommandsTopic": "agent.commands.v1",
    "AgentRepliesTopic": "agent.replies.v1"
  }
}

Leave Namespace empty (the default) to use HTTP-only transport. When set, the Agent:

  • Starts a ServiceBusDeployWorker that consumes deploy commands from CommandsTopic and publishes deploy replies to RepliesTopic.
  • Starts an AgentCommandsConsumer that consumes operator commands (service control, IIS, uninstall, certificate operations, status queries) from AgentCommandsTopic and publishes replies to AgentRepliesTopic.

The agent creates a durable per-agent subscription agent-{Name} on AgentCommandsTopic with a SQL filter targetServer = '{Name}', so only commands addressed to this agent are delivered.

In SB mode the agent host does not need an inbound firewall rule from the Command Center. Only outbound HTTPS (port 443) to <your-namespace>.servicebus.windows.net is required. The HTTP server still runs for the local management web UI and for direct deployments that don't use the SB path.

The Orchestrator must be configured with a matching ServiceBus.Namespace.

Production log path override#

appsettings.Production.json overrides the log path and adds a Windows EventLog sink:

"Serilog": {
  "WriteTo": [
    { "Name": "Console" },
    {
      "Name": "File",
      "Args": {
        "path": "D:\\Logs\\Benefits.Agent\\agent-.log",
        "rollingInterval": "Day",
        "retainedFileCountLimit": 30
      }
    },
    {
      "Name": "EventLog",
      "Args": { "source": "Benefits.Agent", "logName": "Application" }
    }
  ]
},
"Agent": {
  "TempPath": "D:\\BenefitManager\\Temp"
}

Deploy both files side-by-side. The Production file overrides only the keys it declares; everything else comes from the base appsettings.json.


First-run smoke test#

  1. Start-Service Benefits-Mgmt-Agent
  2. Open http://<host>:5100/health — expect Healthy (plain text, no credentials needed).
  3. Open http://<host>:5100/ in a browser — the management dashboard should load.
  4. Call GET http://<host>:5100/api/installations with X-Api-Key: <key> — on a fresh box it returns [].

Web dashboard#

Open http://<host>:5100/ in a browser. The sidebar has five tabs:

Dashboard#

Lists all known installations. Each row shows environment, version, service health, and IIS pool health. The page auto-refreshes every 5 seconds.

Per-installation actions:

  • Start / Stop / Restart — controls the Windows Service for that version.
  • Recycle / Start / Stop — controls the IIS App Pool.
  • Remove — uninstalls the service, removes the IIS application, deletes the version folder, and unregisters the entry from installations.json. This is irreversible.

Deploy#

Step-by-step wizard to trigger a new deployment from the browser. Equivalent to running bmo deploy --via http from the Orchestrator.

Audit#

Read-only log of past deployment and management actions recorded on this agent.

Security#

Certificate management — view, upload, download, and delete certificates managed by the Agent.

Settings#

Under development.


Operations#

Logs#

Environment Path
Default C:\Logs\Benefits.Agent\agent-<date>.log
Production D:\Logs\Benefits.Agent\agent-<date>.log + Windows Application EventLog

Logs rotate daily. The production config retains 30 days of rolling files.

Installation registry#

The Agent tracks known installations in installations.json next to the executable. On startup, if the file is missing or empty, StateMonitorService performs a best-effort filesystem crawl and rebuilds it automatically.

On each monitoring tick (default every 5 minutes) the service:

  1. Checks the health of every registered installation (service status, IIS pool status).
  2. Logs a warning for any component in Stopped or Error health.
  3. Removes registry entries whose folder is physically missing (ghost entries).
  4. Publishes an AgentStateSnapshot for the Service Bus integration (no-op when Service Bus is not configured).

Health endpoint#

GET /health — no credentials required. Returns Healthy, Degraded, or Unhealthy as plain text. Healthy and Degraded answer 200; only Unhealthy answers 503. Authenticated callers receive JSON with per-check detail.

Health decides one thing: an agent whose host is measured Unhealthy (under 5% free memory or disk) refuses to deploy BenefitManager versions — POST /api/deploy and /api/deploy/reference answer 503 with the reason, and a deployment over Service Bus is replied to as failed. Everything else (status, installations, service control, IIS, certificates, uninstall) keeps working, because that is how an operator frees the machine. Degraded (under 10%) changes nothing. A health check that could not run at all (WMI unavailable) is logged and does not block deployments.

API endpoints reference#

All /api/* routes require authorization.

Method Path Description
GET /health Health status. Anonymous: plain text. Authenticated: JSON.
GET /api/agent/info Who this agent is: name (Agent:Name, default the machine name), hostname, health, its IIS site bindings, and its configuration state (shared configuration). CommandCenter reads it when an HTTP agent is added or verified on /config/agents.
POST /api/agent/restart Restart on request: 202, then the agent stops and service recovery starts it again; 409 with the reason when it cannot.
GET /api/installations List all known installations on this agent.
POST /api/deploy Upload zip (multipart/form-data). Returns 202 Accepted with a statusUrl.
POST /api/deploy/reference Deploy by blob reference. Returns 202 Accepted.
GET /api/deploy/status/{id} Poll job status by GUID from the accept response.
POST /api/remove Uninstall service + IIS app + delete folder + unregister.
POST /api/services/{env}/{version}/action Start, Stop, or Restart the Windows Service for a version.
POST /api/iis/pool/{poolName}/action Recycle, Start, or Stop an IIS App Pool.
POST /api/iis/pools/cleanup Delete IIS app pools no longer in the registry.

Troubleshooting#

Symptom Likely cause Fix
Service fails to start Port 5100 already in use Change Agent.Port in appsettings.json
401/403 from CommandCenter Client certificate not accepted Check ClientAuthentication.AllowedSubjects / TrustedIssuers against WebApi's certificate, that the agent's address is https://, and the agent log for the rejection reason
401 Unauthorized with an API key The key scheme is off by default Set Authentication.ApiKey only while a caller still needs it; prefer mTLS
Server cert not loaded Wrong subject/thumbprint or store, or no private key Check with Get-ChildItem Cert:\LocalMachine\My; with Subject, a valid certificate of that name with a private key must be present
Blob deploy fails with 403 Managed identity not granted Assign Storage Blob Data Reader to the service account on the storage account, or switch to a SAS URL
Ghost installations in dashboard Files deleted manually without using Remove They clear automatically on the next monitoring tick (within 5 minutes)
Upload returns 413 Package larger than MaxRequestBodySizeBytes Increase the setting or use --via blob transport
Entra ID auth not working AzureAd.ClientId is empty Fill in TenantId and ClientId; the JWT scheme activates only when ClientId is set
Restart refused: "Service recovery is not configured" Installed before Install.ps1 set recovery Run Install.ps1 again (it keeps the existing service)
Configuration source Unreachable The store cannot be reached or the identity has no access Check the endpoint, the network path to *.azconfig.io, and App Configuration Data Reader for the agent identity; the reason is in configuration.lastError and the agent log (EventId 2200)
A central change has no effect A local setting overrides it The key is listed as shadowed; remove it from appsettings.json or the service environment
configuration.lastError: "A changed setting could not be applied" A value in App Configuration does not fit its setting Correct it; the agent log (EventId 2205) names the key

Operator checklist#

  • [ ] Install.ps1 run; service appears in Services as Benefits Management Agent, startup type Automatic, and sc.exe qfailure Benefits-Mgmt-Agent shows three RESTART actions
  • [ ] For shared configuration: Install.ps1 -AppConfigEndpoint … -Identity … run, the identity has App Configuration Data Reader (and Key Vault Secrets User), and GET /api/agent/info shows configuration.source AppConfiguration
  • [ ] Agent.Port, TempPath, and all Environments entries updated in appsettings.json
  • [ ] Authentication.ApiKey left empty unless a caller still needs it (deprecated)
  • [ ] appsettings.Production.json deployed alongside appsettings.json on production servers
  • [ ] For HTTPS/mTLS: ServerCertificate.Subject set and a certificate of that subject, with its private key, in the Windows cert store
  • [ ] For mTLS client auth: ClientAuthentication.AllowedSubjects and TrustedIssuers set to CommandCenter's certificate subject and issuer
  • [ ] For blob deploys: PackageStore.AccountUri set and managed identity or SAS configured
  • [ ] GET /health returns Healthy
  • [ ] Dashboard loads at http://<host>:<port>/
  • [ ] GET /api/installations succeeds with CommandCenter's client certificate
  • [ ] First deployment from bmo or the web Deploy wizard succeeds