---
module: devices.config-audit
title: Config Audit
tab: Internal
order: 20
audience: internal
---

> **Internal · ops/support view.** Grounded in `hiveops-devices` source (2026-07-01). Config Audit is the read-only change log of every save to an institution's **agent properties** (the per-institution `hiveops_config` blob edited on the Agent Properties screen).

## What it actually is

- A history table in the `hiveops-devices` backend. **One row per save** to an institution's agent properties.
- The row stores a full `previous_config` / `new_config` JSON snapshot; the field-level before/after diff shown in the UI is computed **on read**, not stored.
- **Read-only.** Viewing the audit never changes any config. Nothing here pushes config to devices — that is a separate CONFIG_UPDATE fleet action.

## Who can do what (roles)

| Action | Endpoint | Auth |
|--------|----------|------|
| **View** the audit log | `GET /api/institution-keys/config-audit` | Any authenticated user (no `@PreAuthorize`) — results scoped by role, see below |
| **Create** an audit row (side effect of saving agent props) | `PUT /api/institution-keys/{key}/agent-properties` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |

- There is **no admin-only action inside Config Audit itself** — it is purely a viewer. The only way to add a row is for an `MSP_ADMIN`/`BCOS_ADMIN` to save agent properties.
- `changedBy` is captured from the Spring `SecurityContext` principal name at save time (falls back to literal `"system"` if no auth present).

## Scoping (who sees which rows)

Scope comes from `InstitutionContext.resolveScope()` (institution keys are carried in the JWT):

- **BCOS_ADMIN / ADMIN / QDS_ADMIN / USER** → unrestricted. The `institutionKey` query param is honored (blank = all institutions).
- **CUSTOMER / MSP_ADMIN** → scoped. The backend **ignores the query param** and forces the log to the caller's **first** scoped institution key only.

## First things to check when it misbehaves

1. **"Log is empty / No config changes recorded yet."** Expected until someone actually saves agent properties for that institution. Confirm a save has happened: check `institution_agent_properties.updated_at` for the key. No save → no audit row, by design.
2. **"An MSP_ADMIN can only see one institution's history."** This is current behavior, not a glitch — scoped callers are pinned to `scopedKeys.get(0)` and the institution dropdown filter is effectively a no-op for them. See uncertainties in the Architect tab. Reproduce/confirm by hitting the endpoint as an `MSP_ADMIN` with a multi-institution JWT.
3. **"Changed By shows `system`."** Save was made without an authenticated principal in context (e.g. an internal/service call path). Confirm the caller's JWT is present and being parsed by the auth filter.
4. **"Diff shows the whole config as changed on the first entry."** Correct — the first save has `previous_config = null`, so every field is rendered as an **Initial save** (badge `Initial save`, all fields `from = null`).
5. **Filter dropdown empty / institutions missing.** The dropdown is populated by `GET /api/institution-keys` (delegates to `/accessible`). If it's empty for a scoped user, their JWT institution-key claim is missing/empty — an auth/token issue, not a Config Audit issue.
6. **403 on save (no new audit rows appearing).** The saver lacks `MSP_ADMIN`/`BCOS_ADMIN`, or `verifyKeyAccess` rejected them (scoped user editing an institution outside their scope → `403 Access denied`).
7. **500 / nothing loads.** Backend down or DB unreachable. Service = `hiveops-devices` (port **8096**, DB `hiveiq_devices`). Check the container and Postgres connectivity, then Loki logs for `InstitutionKeyService`.

## Common failure modes → fixes

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| No rows for an institution | Nobody saved agent props yet | Save agent properties once (as admin); a row appears |
| Scoped user can't filter to another institution | By-design pinning to first scoped key | Use an unrestricted admin account to view other institutions |
| `Changed By = system` | Save with no auth principal | Ensure request carries a valid JWT |
| Save succeeds but no audit row | Serialization error thrown before audit save | Check `InstitutionKeyService` logs for `Failed to serialize agent properties` |
| Diff looks wrong / partial | `computeDiff` swallows parse errors (logs warn, returns partial) | Inspect `previous_config`/`new_config` TEXT columns directly in `institution_config_audit` |

## Direct DB inspection (when the UI won't tell you)

Table: `institution_config_audit` in **`hiveiq_devices`** (separate DB VM, reach via ProxyJump — see platform gotchas). Columns: `id, institution_key, changed_by, changed_at, previous_config, new_config`. Ordered newest-first by `changed_at` (indexed).

See also: [devices.config-audit] Overview (customer view), [technical.devices], [platform.service-ownership].
