Commit 7604729
authored
Encryption at rest: operator surface (P3A) (#7501)
# Encryption at rest: operator surface (P3A)
Encryption at rest has shipped twice and has never been visible in the
product. P1 (#7155) built the
crypto, P2 (#7173) built the admin API. Today an operator enables it by
editing YAML, revokes a key
with curl, and has no screen anywhere telling them the feature exists or
whether their files are
actually encrypted.
This PR puts the P2 API behind a UI. No crypto changes, no schema
changes.
## Scope
In: the Storage tab re-enabled with encryption as its content, key
actions, migration control,
rotation status, the encrypted badge on documents, and honest licence
messaging.
Note on placement: #7497 removed the Infrastructure tabs backed by
mock-only
`/v1/infrastructure/*` data, including the old Storage tab, leaving
`storage` as a disabled
placeholder. This PR re-enables it, because it now reads the real
`/api/v1/admin/storage-encryption` surface, which is the criterion #7497
used. The mock usage,
quota, provider and retention UI that used to live there is not
restored.
Out: per-source encryption (P3B, needs a schema change), KMS and BYOK
backends (P4), FIPS builds
(P4).
## User stories
### Knowing where you stand
**US-1. See whether encryption is actually on**
As an administrator, I want one screen that tells me whether stored
files are being encrypted, so
that I can answer "are we encrypted at rest?" without reading config
files or logs.
- [x] The Storage tab under Infrastructure is enabled again, with
encryption as its content
- [x] Shows write state from `writeEnabled`: new uploads are encrypted,
or they are not
- [x] Shows `encryptedFiles` and `plaintextFiles` counts, so partial
coverage is obvious
- [x] Distinguishes `writeEnabled: false, active: true` (decrypt-only,
the state after turning the
flag off) from `writeEnabled: false, active: false` (feature never used)
- [x] Empty state when no keys exist yet: explains that a key is created
on the first upload, not at
startup
- [x] When storage is disabled the API returns 403 "Storage is disabled"
before touching the
database. The panel shows that as an explanatory state, not an error
toast
- [x] When the key registry cannot be read the API returns 503. The
panel says the registry is
unavailable and suggests checking the database, rather than showing zero
keys
**US-2. Verify my key backup matches the running system**
As an administrator, I want to compare the live master key against my
backup without exposing key
material, so that I can prove my disaster recovery actually works before
I need it.
- [x] Displays `masterKeyFingerprint` (SHA-256 prefix, 16 hex
characters) and `masterKeyVersion`
- [x] Copy-to-clipboard on the fingerprint
- [x] Explains in one line what the fingerprint is for: it matches the
fingerprint logged at startup,
so an operator can check their archived key is the one in use
- [x] Never renders anything that could be key material, and the field
is absent when the key
machinery has not been materialised
**US-3. Be told when my licence gives me no audit trail**
As a compliance reviewer on a Pro licence, I want the product to tell me
that encryption events are
not being recorded, so that I do not report an audit trail to my auditor
that does not exist.
- [x] On a non-Enterprise licence the panel carries a persistent notice:
encryption is active, audit
events are not recorded, this requires Enterprise
- [x] The notice sits next to any claim about auditing, not in a
separate help page
- [x] On Enterprise the notice is absent
- [x] Wording matches the startup warning and
`devGuide/STORAGE_ENCRYPTION_AT_REST.md`, so the log,
the docs and the UI agree
Note: encryption is gated at Pro but `AuditService` only records on
Enterprise. Today this is
visible only in a startup log line. If we would rather move audit down
to Pro, that is a licence
decision and this story changes to "remove the notice".
**US-4. Be told my master key was generated for me**
As an administrator of a single-node install, I want to know that the
system created an encryption
key on my behalf, so that I do not discover an unbacked-up secret after
losing it.
- [x] When the key came from the auto-generated `file-encryption.key`
file rather than explicit
config, the panel says so and states the consequence: lose this file and
encrypted files
cannot be recovered
- [x] Links to the backup section of the runbook
- [x] Requires a small `/status` addition to report key provenance
(config, environment, or generated
file). Included in this PR
### Acting on it
**US-5. Revoke a scope's stored content**
As an administrator responding to an incident, I want to revoke access
to one team's stored files,
so that their content cannot be read while I investigate.
- [x] Each key row has a Revoke action, calling `POST
/keys/{keyId}/disable`
- [x] The confirmation states what revocation actually does, in plain
words:
- reads of existing files under this key start failing with 403
- the team can still upload new files, which get a fresh key
- it is reversible, and no key material is destroyed
- on a cluster, other nodes take up to 60 seconds to catch up
- [x] The row shows DISABLED, plus `statusChangedBy` and
`statusChangedAt`
- [x] The 60 second cluster note is only shown when clustering is on
Note: the "still uploads" and "60 seconds" points are not padding. Both
are real behaviour that
surprised us during review, and an admin who believes revoke means
"frozen instantly" will be wrong.
**US-6. Restore access, and understand what came back**
As an administrator, I want to undo a revocation and see exactly what
state the key returned to, so
that I am not misled into thinking a key is wrapping new files when it
is not.
- [x] Enable action on DISABLED keys only, calling `POST
/keys/{keyId}/enable`
- [x] The response status is what the row shows: ACTIVE if the scope had
no other active key,
RETIRED if one was minted while it was revoked
- [x] When the result is RETIRED, the UI explains it: this key decrypts
its existing files again,
and a newer key is wrapping new uploads
- [x] Enable is not offered on ACTIVE or RETIRED keys, matching the
API's 409
- [x] A key that no longer exists returns 404 and surfaces as a clear
message, not a crash
**US-7. Encrypt the files I already had**
As an administrator who has just enabled encryption, I want to encrypt
the existing plaintext
backlog and watch it happen, so that "encrypted at rest" is true of my
whole estate rather than only
new uploads.
- [x] Start action calls `POST /migrate`, disabled when `plaintextFiles`
is zero
- [x] Progress from `GET /migrate/status`: state, total, processed,
skipped, failed, started time
- [x] All four terminal states render distinctly: IDLE, RUNNING,
COMPLETED, FAILED
- [x] FAILED is a real, explained state, not a stalled spinner. The run
now ends FAILED when the
write flag is turned off mid-run, and the message says so and what to do
- [x] Skipped is explained on hover: a user replaced the file
mid-migration, so the job left their
copy alone
- [x] 409 when a run is already going, or when encryption is off,
surfaces as a message rather than a
failure
- [x] Progress is in-memory, so a backend restart resets it to IDLE. The
UI says that rather than
appearing to lose the run
- [ ] Adds a cancel endpoint and page-size and pause knobs to the API,
since exposing a long job in a
UI without a stop button is not defensible
**US-8. Finish a key rotation without sealing my files**
As an administrator rotating the master key, I want to see how many key
rows are still on the old
key, so that I do not remove the outgoing key while rows still depend on
it.
- [x] Shows the current `masterKeyVersion` and the number of key rows
below it
- [x] Re-wrap action calls `POST /master/rotate` and reports the
`rewrapped` count
- [x] States plainly that key material never travels over HTTP: the new
key is a config and restart
operation, and this button only performs the re-wrap step
- [x] Warns while any row is behind: removing the previous key now would
make those files
unreadable
- [x] Links to the rotation runbook
### Everyone else
**US-9. See which of my files are encrypted** — moved to #7550
The badge component was built here but never wired to a surface, and no
documents endpoint returns
`encryptionKeyId`, so it could not render in the product. It moved to
its own draft PR rather than
shipping as unreachable code behind a screenshot.
**US-10. Understand why a file will not open**
As a user whose team's key has been revoked, I want a clear message
rather than a generic error, so
that I raise the right request with the right person.
- [ ] A 403 from a revoked key renders as "access to this file has been
revoked" wording, not a
generic failure toast
- [ ] Distinguished from a permissions 403
- [ ] Applies to both My Files and workflow document reads, which P2
made consistent
## Definition of done
- [x] New API client module under `portal/api/`, following the
conventions in `sources.ts`
- [x] All copy is i18n keys in the editor `en-US` catalogue, which is
the source of truth
- [x] Storybook stories for every state named above, including the
empty, 403, 503 and FAILED states
- [x] a11y baseline recorded with the full task, so a partial scan does
not delete other entries
- [x] Light and dark themes both checked
- [x] No raw `<button>` elements, per the lint rule
- [ ] `/status` paginates its key list, since this UI is the consumer
that makes an unbounded list
matter
## Not covered in this PR
The five unticked boxes above, all tracked elsewhere. Everything else
was checked against the code,
not assumed.
**Split into follow-up PRs:**
- **#7550** — the encrypted badge on documents (US-9). Needs a documents
endpoint to return
`encryptionKeyId` before it can render anywhere
- **#7551** — the revoked-key message (US-10), the migration cancel
endpoint with page-size and
pause knobs, and pagination on the `/status` key list
**Also tracked:** #7549, to publish the operator docs. The two runbook
links in this PR point at
`devGuide/STORAGE_ENCRYPTION_AT_REST.md` until those pages exist; both
are constants in
`storageEncryption.ts`, so swapping them is a one-file change.
## Backend changes riding along
Two fields added to `/status`. Nothing else in the backend changed:
| Change | Why |
|---|---|
| `masterKeySource` (config, environment, generated) | US-4 cannot be
built without it. The value already existed inside
`FileEncryptionMasterKey.resolveKey` and was simply never returned |
| `provider` (local, database, s3) | The panel names the storage
backend, and warns that object-store downloads stream through the app
once anything is encrypted |
| `pendingRotationRows` | The rotation warning needs a whole-table
count, which the returned key list cannot give |
Two items from the original plan are still not done and are called out
below: key-list pagination,
and the migration cancel endpoint and knobs.
## Screenshots
Replace each placeholder with the matching image.
| | |
|---|---|
| Encryption off, nothing encrypted yet | <img width="2560"
height="1800" alt="01-panel-encryption-off"
src="https://github.com/user-attachments/assets/36ae3c9a-2c54-48b1-8837-c7c68e157961"
/> |
| Encryption on, healthy, key table | <img width="2560" height="2036"
alt="02-panel-active"
src="https://github.com/user-attachments/assets/949d930c-7e13-409d-b960-49d44e49fc21"
/> |
| Pro licence audit notice | <img width="2560" height="2226"
alt="03-licence-notice"
src="https://github.com/user-attachments/assets/f5e1b39e-c221-4e42-87c1-0f45f8ecd2a3"
/> |
| Revoke confirmation | <img width="2560" height="2036"
alt="04-revoke-confirm"
src="https://github.com/user-attachments/assets/0c86a40c-3bf1-4a0d-aa19-c28691986e2f"
/> |
| Revoked key row | <img width="2560" height="1912" alt="05-key-revoked"
src="https://github.com/user-attachments/assets/2333bc0d-968f-4472-b253-7877fe1d41a2"
/> |
| Re-enabled as RETIRED, with explanation |<img width="2560"
height="1912" alt="06-key-retired"
src="https://github.com/user-attachments/assets/a4e84165-020b-4377-8506-49f4312783e4"
/> |
| Migration running | <img width="2560" height="2222"
alt="07-migration-running"
src="https://github.com/user-attachments/assets/6ca07208-ad58-441d-85ba-e5617f36bb29"
/> |
| Migration failed after the flag was turned off | <img width="2560"
height="2380" alt="08-migration-failed"
src="https://github.com/user-attachments/assets/35a54549-1b92-4c17-9fad-4790bda8c24b"
/> |
| Rotation with rows still on the old key | <img width="2560"
height="2232" alt="09-rotation-pending"
src="https://github.com/user-attachments/assets/991e1811-3103-45d1-8bd6-a83b6eb9291b"
/> |
| Storage disabled state | <img width="2560" height="1800"
alt="11-storage-disabled"
src="https://github.com/user-attachments/assets/c52c5087-2a4a-4189-a3c1-22e69016fc2e"
/> |
| Dark theme, active panel | <img width="2560" height="2036"
alt="12-panel-active-dark"
src="https://github.com/user-attachments/assets/7d6c3ff5-7824-4ce6-92c7-d8af82d52c10"
/> |
Extras captured while iterating, not required in the body: registry
unavailable
<img width="2560" height="1800" alt="13-registry-unavailable"
src="https://github.com/user-attachments/assets/4c8df8ce-5659-41f0-9484-010cc9f74c2d"
/>
(`13`), generated master key (`14`)
<img width="2560" height="2226" alt="14-generated-master-key"
src="https://github.com/user-attachments/assets/3ff765ed-a24f-48fa-b9b8-d92de3cc2af0"
/>
, dark migration (`15`)
<img width="2560" height="2222" alt="15-panel-active-dark-migration"
src="https://github.com/user-attachments/assets/7a2ae906-8c23-404b-b778-b40ea74c3058"
/>
.
## Test plan
What is actually covered:
- Storybook covers every state named above, driven by fixtures rather
than a live backend:
encryption off, active, Pro notice, revoke dialog, revoked row,
restore-as-RETIRED, migration
running, migration FAILED, rotation pending, storage disabled (403),
registry unreadable (503),
and generated master key
- `Infrastructure.test.tsx` covers the tab wiring: Storage is enabled,
reachable by click and by
`?tab=storage`, and disabled tabs stay inert
- `FileEncryptionMasterKeySourceTest` covers key provenance, including
that the wire names match the
values the UI switches on
- The shared `Tooltip` has a play-function story asserting it opens on
keyboard focus and wires
`aria-describedby`
- a11y scanned clean across both themes, light and dark, and the shared
baseline is unchanged
- Typecheck across all nine build variants, oxlint, stylelint and a
production build
Not covered, and worth a reviewer's attention:
- No component tests for the encryption UI itself. The migration state
machine and the
enable-returns-RETIRED path are exercised by stories, which render them
but assert almost nothing.
This is the biggest remaining gap in the PR
- No manual pass against a live backend with encryption on, a seeded
plaintext backlog and a
revoked key. Every state in this PR was driven by fixtures
## Notes for reviewers
The revoke confirmation copy is deliberately blunt about two things that
are easy to get wrong:
revoking does not stop the team storing new files, and on a cluster it
takes up to a minute. Both
were found during the P2 review. If the copy reads as over-explaining,
that is the intent.1 parent 235d8fd commit 7604729
24 files changed
Lines changed: 2528 additions & 22 deletions
File tree
- app/proprietary/src
- main/java/stirling/software/proprietary/storage
- controller
- crypto
- model/api
- test/java/stirling/software/proprietary/storage/crypto
- devGuide
- frontend/editor
- public/locales/en-US
- src
- core
- i18n
- ui
- portal
- api
- components
- infrastructure
- views
Lines changed: 10 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
101 | 102 | | |
102 | 103 | | |
| 104 | + | |
103 | 105 | | |
104 | 106 | | |
105 | 107 | | |
106 | 108 | | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
107 | 114 | | |
108 | 115 | | |
109 | 116 | | |
110 | 117 | | |
111 | 118 | | |
| 119 | + | |
| 120 | + | |
112 | 121 | | |
113 | 122 | | |
| 123 | + | |
114 | 124 | | |
115 | 125 | | |
116 | 126 | | |
| |||
Lines changed: 32 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
52 | 70 | | |
53 | 71 | | |
54 | 72 | | |
| |||
66 | 84 | | |
67 | 85 | | |
68 | 86 | | |
69 | | - | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
70 | 90 | | |
71 | 91 | | |
72 | 92 | | |
| |||
90 | 110 | | |
91 | 111 | | |
92 | 112 | | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
93 | 117 | | |
94 | 118 | | |
95 | 119 | | |
96 | 120 | | |
97 | | - | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
98 | 124 | | |
99 | 125 | | |
| 126 | + | |
100 | 127 | | |
101 | 128 | | |
102 | 129 | | |
| 130 | + | |
103 | 131 | | |
104 | 132 | | |
105 | | - | |
| 133 | + | |
106 | 134 | | |
107 | 135 | | |
108 | 136 | | |
| |||
111 | 139 | | |
112 | 140 | | |
113 | 141 | | |
114 | | - | |
| 142 | + | |
115 | 143 | | |
116 | 144 | | |
117 | 145 | | |
| |||
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
16 | 20 | | |
17 | 21 | | |
| 22 | + | |
| 23 | + | |
18 | 24 | | |
19 | 25 | | |
20 | 26 | | |
| |||
Lines changed: 30 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
77 | 100 | | |
78 | 101 | | |
79 | 102 | | |
| |||
163 | 186 | | |
164 | 187 | | |
165 | 188 | | |
166 | | - | |
167 | | - | |
168 | | - | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
169 | 197 | | |
170 | 198 | | |
171 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8344 | 8344 | | |
8345 | 8345 | | |
8346 | 8346 | | |
| 8347 | + | |
| 8348 | + | |
| 8349 | + | |
| 8350 | + | |
| 8351 | + | |
| 8352 | + | |
| 8353 | + | |
| 8354 | + | |
| 8355 | + | |
| 8356 | + | |
| 8357 | + | |
| 8358 | + | |
| 8359 | + | |
| 8360 | + | |
| 8361 | + | |
| 8362 | + | |
| 8363 | + | |
| 8364 | + | |
| 8365 | + | |
| 8366 | + | |
| 8367 | + | |
| 8368 | + | |
| 8369 | + | |
| 8370 | + | |
| 8371 | + | |
| 8372 | + | |
| 8373 | + | |
| 8374 | + | |
| 8375 | + | |
| 8376 | + | |
| 8377 | + | |
| 8378 | + | |
| 8379 | + | |
| 8380 | + | |
| 8381 | + | |
| 8382 | + | |
| 8383 | + | |
| 8384 | + | |
| 8385 | + | |
| 8386 | + | |
| 8387 | + | |
| 8388 | + | |
| 8389 | + | |
| 8390 | + | |
| 8391 | + | |
| 8392 | + | |
| 8393 | + | |
| 8394 | + | |
| 8395 | + | |
| 8396 | + | |
| 8397 | + | |
| 8398 | + | |
| 8399 | + | |
| 8400 | + | |
| 8401 | + | |
| 8402 | + | |
| 8403 | + | |
| 8404 | + | |
| 8405 | + | |
| 8406 | + | |
| 8407 | + | |
| 8408 | + | |
| 8409 | + | |
| 8410 | + | |
| 8411 | + | |
| 8412 | + | |
| 8413 | + | |
| 8414 | + | |
| 8415 | + | |
| 8416 | + | |
| 8417 | + | |
| 8418 | + | |
| 8419 | + | |
| 8420 | + | |
| 8421 | + | |
| 8422 | + | |
| 8423 | + | |
| 8424 | + | |
| 8425 | + | |
| 8426 | + | |
| 8427 | + | |
| 8428 | + | |
| 8429 | + | |
| 8430 | + | |
| 8431 | + | |
| 8432 | + | |
| 8433 | + | |
| 8434 | + | |
| 8435 | + | |
| 8436 | + | |
| 8437 | + | |
| 8438 | + | |
| 8439 | + | |
| 8440 | + | |
| 8441 | + | |
| 8442 | + | |
| 8443 | + | |
| 8444 | + | |
| 8445 | + | |
| 8446 | + | |
| 8447 | + | |
| 8448 | + | |
| 8449 | + | |
| 8450 | + | |
| 8451 | + | |
| 8452 | + | |
| 8453 | + | |
| 8454 | + | |
| 8455 | + | |
| 8456 | + | |
| 8457 | + | |
| 8458 | + | |
| 8459 | + | |
| 8460 | + | |
| 8461 | + | |
| 8462 | + | |
| 8463 | + | |
| 8464 | + | |
| 8465 | + | |
| 8466 | + | |
| 8467 | + | |
| 8468 | + | |
| 8469 | + | |
| 8470 | + | |
| 8471 | + | |
| 8472 | + | |
| 8473 | + | |
| 8474 | + | |
| 8475 | + | |
| 8476 | + | |
| 8477 | + | |
| 8478 | + | |
| 8479 | + | |
| 8480 | + | |
| 8481 | + | |
| 8482 | + | |
| 8483 | + | |
| 8484 | + | |
| 8485 | + | |
| 8486 | + | |
| 8487 | + | |
| 8488 | + | |
| 8489 | + | |
| 8490 | + | |
| 8491 | + | |
| 8492 | + | |
| 8493 | + | |
| 8494 | + | |
| 8495 | + | |
| 8496 | + | |
| 8497 | + | |
| 8498 | + | |
| 8499 | + | |
| 8500 | + | |
| 8501 | + | |
| 8502 | + | |
| 8503 | + | |
| 8504 | + | |
| 8505 | + | |
| 8506 | + | |
| 8507 | + | |
| 8508 | + | |
| 8509 | + | |
| 8510 | + | |
| 8511 | + | |
| 8512 | + | |
8347 | 8513 | | |
8348 | 8514 | | |
8349 | 8515 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
115 | 120 | | |
116 | 121 | | |
117 | 122 | | |
| |||
0 commit comments