API Authentication
How to obtain a credential, present it, keep it alive, and what governs whether a request is allowed. This page covers the user-facing API; agent submission to the collector uses a different mechanism, described in Token Authentication.
The model
CBOM Secure delegates identity to Keycloak and holds the resulting session on the server. A client never handles a Keycloak token directly: it receives an opaque session identifier and presents that.
| Property | Value |
|---|---|
| Scheme | Authorization: Bearer {session_id} |
| Credential | An opaque session identifier, not a JWT |
| Resolution | In-process against the session manager — no identity-provider round trip per request |
| Lifetime | Configurable; 900 seconds by default |
| Storage | Server-side |
Because the credential is opaque and resolved locally, per-request latency does not depend on the identity provider, and a client cannot read or forge claims from it.
Obtaining a session
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/login | Authenticate and receive a session_id |
| POST | /auth/refresh | Rotate the session — issues a new session_id and invalidates the old one |
| POST | /auth/auto-renew | Extend the current session in place — same session_id |
| GET | /auth/validate | Test whether the current session is still valid |
| GET | /auth/logout | Invalidate the session and sign out of the identity provider |
Sign in:
curl -sS -X POST https://cbom.example.com/cbomsrv/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"svc-reporting","password":"..."}'
{
"status": 0,
"message": "OK",
"data": { "session_id": "b6f1…", "expires_in": 900 }
}
Call an endpoint:
curl -sS https://cbom.example.com/cbomsrv/api/v1/hosts/ \
-H 'Authorization: Bearer b6f1…'
Keeping a session alive
Two distinct operations, and the difference matters to a client that caches the identifier:
| Operation | Issues a new identifier | Use when |
|---|---|---|
/auth/refresh | Yes — the old one is invalidated immediately | Rotating on a schedule, which is what the console does |
/auth/auto-renew | No — the same identifier gains a later expiry | Extending without disturbing anything holding the identifier |
The console rotates every four minutes against a 900-second lifetime, so a session is renewed several times before it could expire. A script should renew at roughly half the configured lifetime.
Handle a 401 from an ordinary endpoint by refreshing once before concluding the session is dead. The body carries expired: true when a valid credential has merely timed out. A session past its expiry is retained for a grace period while a refresh token is present, so a client that was briefly offline can recover rather than being forced to sign in again.
A network error is not an expiry. A request that receives no response at all says nothing about the session. Treat a transport failure as a transport failure and retry; discarding the session on a connection error causes spurious sign-outs.
Two more behaviours worth building for:
A single failed renewal must not cancel the renewal schedule. The session may still be valid and the next attempt may well succeed.
If two renewals race, one wins and the other receives a 401 from
/auth/refreshitself. Before discarding the session, check whether the stored identifier has already changed — a concurrent refresh may have succeeded.
Single sign-on
Interactive sign-in can be federated to an external identity provider; Microsoft Entra ID is supported as a configured provider. The browser flow is standard OIDC: the console redirects to the provider, the provider returns to the platform, and the platform issues a session identifier exactly as it does for a password sign-in. From the API's point of view nothing differs — the credential is the same opaque identifier.
Multi-factor authentication, password policy and lockout thresholds are enforced at the identity provider, not in CBOM Secure. The platform holds no user passwords: only a subject reference, the user record and the role assignment.
Service integrations that cannot complete a browser flow should use a dedicated account with a password sign-in and the narrowest role that covers their calls.
Authorisation
Authentication establishes who is calling; authorisation decides whether that caller may make this call. The two are separate, and both are enforced on the server.
Roles and scopes
A role is a named set of scopes — single permissions such as policy:delete or host:read. The product defines scopes in these groups:
| Group | Covers |
|---|---|
| User Management | Reading, creating, editing and deleting users |
| Roles & Permissions | Reading and changing the role-to-permission map |
| UI Preferences | Per-user display settings |
| System Configuration | Reading, editing and activating configuration; service restart |
| Discovery — Hosts | Host inventory and CSV import |
| Discovery — Cloud Servers | Cloud discovery targets and drafts |
| Discovery — Task Templates | Templates and sensor schemas |
| Discovery — Tasks | Task definition and execution |
| Discovery — Off-grid | Bundle generation and result import |
| Discovery — Integrations | Integration records, synchronisation, duplicate resolution |
| Discovery — Tags / Tag Scopes | Tag definitions and named tag expressions |
| Cryptographic Materials | Inventory and history reads |
| Policy — User Policies | Policy authoring |
| Policy — Standard Policies | The shipped baselines |
| Dashboards/Layouts | Layouts, templates and export |
| Notifications | Rules, destinations, templates, cases |
| Deployment | Deployment history |
| Audit Trail | Reading and exporting the audit trail |
| AI Services | Access to the assistant |
| Source Code Visualizer | Access to the visualizer's data |
The full catalogue, with the display name and description of every scope, is available from GET /admin/permissions/catalogue, and the Roles & Permissions page edits the map.
How the check runs
The permission map is the API's own control, not a hint to the interface. Changing a role's permissions changes what the API accepts for holders of that role — a permission removed from a role is refused for that role at the API, not merely hidden in the console.
| Response | Meaning |
|---|---|
| 401 | No session, or the session has expired |
| 403 | Authenticated, but no role held by this session carries the required scope |
Three behaviours are deliberate:
A failed permission lookup allows the request and records that it did. "This caller lacks the permission" and "we could not determine what this caller has" are different answers. Denying on a transient database fault would lock out every user including the administrator who would fix it, so the second case is recorded as degraded rather than treated as a denial.
The administrator floor cannot be removed. The permission map is replaced wholesale by
PUT /admin/permissions. Without a floor, an administrator could remove their own permission to manage permissions and lock everyone out of the page that would undo it. The scopespermission:read,permission:manageanduser:readare always retained for the administrator role.Authorisation mode is not editable from the console. It governs the check that protects the System Configuration page, and a page that authorisation protects must not be able to switch authorisation off.
Reference reads
A small number of endpoints are readable by any signed-in user because they are reference data the console needs to render at all — the role list, the branding and display settings, the sensor schema reference. These carry no inventory content.
Rate limiting and request size
Rate limits and body-size limits are applied at the reverse proxy rather than in the application, so they are part of the deployment's own configuration:
| Path class | Limit |
|---|---|
| Authentication endpoints | Per-client request rate limit |
| Ingestion | Rate limited; body size unbounded by default, since result batches are large |
| API generally | Request body capped |
| Long-running operations | Proxy read timeout raised to accommodate them |
Exact values are deployment-specific. Check the reverse proxy configuration for the deployment rather than assuming the defaults.
Practical guidance for integrations
Use a dedicated service account with the narrowest role that covers the calls it makes, rather than an administrator account.
Renew at roughly half the configured session lifetime; do not wait for a 401.
Treat
statusin the body and the HTTP code as a pair.Send no credential in a URL or query string.
Expect masked values on credential fields, and do not write a mask back as if it were a value.
Paginate. Every list endpoint enforces a maximum page size, so an integration that assumes it received everything in one call will silently process a subset.