Skip to content

feat(sse): enable local KMS evaluation workflows - #189

Merged
cxymds merged 2 commits into
mainfrom
reatang/local-kms-experience-safety
Aug 1, 2026
Merged

feat(sse): enable local KMS evaluation workflows#189
cxymds merged 2 commits into
mainfrom
reatang/local-kms-experience-safety

Conversation

@reatang

@reatang reatang commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

Enable Local KMS configuration and safe day-to-day key management in Console for evaluation environments. The UI explicitly warns that Local KMS is not for production, opts into plaintext development mode only when no server-managed local master key exists, and locks fields that cannot be changed safely after initial configuration.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test improvements
  • Security fix

Testing

  • Unit tests added/updated
  • Manual testing completed
node scripts/apply-theme-overrides.js
tsc --noEmit
eslint "app/(dashboard)/sse/page.tsx" types/kms.ts tests/lib/sse-safety.test.js
node --experimental-strip-types --import ./scripts/register-typescript-loader.mjs --test tests/lib/*.test.{js,ts}

Checklist

  • Code follows the project's style guidelines
  • Self-review completed
  • TypeScript types are properly defined
  • All commit messages are in English (Conventional Commits)
  • All existing tests pass
  • No new dependencies added, or they are justified

Related Issues

Related to rustfs/rustfs#5063.

Screenshots (if applicable)

N/A.

Additional Notes

Local KMS remains explicitly evaluation-only. After initial configuration, Console prevents changing the backend, key directory, and file permissions. It does not send local master-key material during updates. Default-key, cache, and timeout settings remain editable, and key creation can safely set the new key as the default.

@cxymds cxymds left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head eaa84a9d6c637e64bc4e0075337d39d93a7a9934. I found three P1 security/correctness issues and two P2 recovery/test-coverage issues. The current RustFS contract from rustfs/rustfs#5418 does preserve an existing Local master key, optional file permissions, and development-mode setting during reconfiguration, so that contract is not a blocker here. CI and focused local checks are green, but the current source-regex tests do not exercise the state and payload transitions described below.

"Local filesystem KMS configuration is read-only in Console until safe master-key rotation is available.",
),
payload: {
backend_type: "Local",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fail closed for absent or unsupported backend types

normalizeBackendType currently maps both the normal NotConfigured response (backend_type: null) and every future/unknown backend to local. Now that Local is writable, a fresh instance opens with Local selected and this payload opts into plaintext development mode even though the user never chose Local. An older Console talking to a server with a newer backend is worse: editing any field can submit backend_type: "Local"; the server currently permits a non-Local → Local transition and then forbids switching away from Local, potentially making data encrypted through the original backend unavailable. Return the initial/unselected state for NotConfigured, use an explicit read-only unsupported state for unknown values, and emit this payload only after an explicit Local selection. Please add behavior tests for both null and a future backend value.

backend_type: "Local",
key_dir: values.keyDir.trim(),
file_permissions:
localKmsConfigured && !hasStoredLocalFilePermissions ? undefined : (filePermissions ?? 384),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reject permissions that expose plaintext key material

This passes any parsed integer through as the Unix mode. With the new initial Local flow, no master key is present, so RustFS stores the key material as plaintext-dev-only; entering decimal 438 (0o666) or 511 (0o777) makes the resulting .key files readable by other local users when the directory is traversable. The form uses noValidate, and the server currently applies Permissions::from_mode(mode) without checking owner/group/other bits. Keep this owner-only (the smallest safe choice is fixed 384/0o600), or reject group/other and special bits before sending the request; the server should enforce the same final-boundary invariant. Tests should prove unsafe modes do not issue a request.

const statusKind = React.useMemo(() => (statusError ? "Error" : getStatusKind(status)), [status, statusError])
const isRunning = statusKind === "Running"
const hasConfiguration = !statusError && statusKind !== "NotConfigured"
const localKmsConfigured = hasConfiguration && status?.backend_type === "Local"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Keep status and the form baseline in the same snapshot

This lock is derived from the latest status, while handleRefresh and mutation reconciliation call loadStatus(false) and leave formState/baselineFormState untouched. For example, if another administrator configures Local while this page is open, Refresh makes localKmsConfigured true but leaves the old or empty form in place; the new backend/path locks then make those stale values unrecoverable through Reset/Save. Concurrent changes to default/cache settings can also be overwritten by the next full-payload submission. When the form is clean, refresh should rebuild both form and baseline from the returned status; when it is dirty and the server baseline changed, surface a persistent conflict and block a silent overwrite. Please cover the external-Local-config and committed-POST/failed-response cases.

Comment thread app/(dashboard)/sse/page.tsx Outdated
const mutationInFlight = Boolean(activeMutation || submittingConfig || creatingKey || processingKeyAction)
const canSetCreatedKeyAsDefault =
status?.backend_type !== "Local" && status?.backend_type !== "Static" && !isConfigDirty
const canSetCreatedKeyAsDefault = status?.backend_type !== "Static" && !isConfigDirty

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve recovery state after a create/set-default partial failure

Allowing Local here exposes it to the existing two-stage flow, but handleCreateKey closes the dialog and clears its state immediately after key creation, before the configuration update finishes. If the reconfigure request then fails—and especially if the list refresh also fails or the user is on a later page—the generated key ID is no longer visible, while the toast tells the user to save a form that does not contain that ID. Persist a partial-success state with the created ID and a retry/copy action, or populate defaultKeyId plus a durable inline warning after confirming that the second stage did not commit. Add an interaction test for create success + reconfigure failure + list failure.

assert.match(source, /const localKmsReadOnly = hasConfiguration && formState\.backendType === "local"/)
assert.match(source, /const localKmsConfigured = hasConfiguration && status\?\.backend_type === "Local"/)
assert.match(source, /backend_type: "Local"/)
assert.match(source, /allow_insecure_dev_defaults: !hasStoredLocalMasterKey/)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Test the state and payload behavior, not the source spelling

This assertion locks in one implementation string but never executes the payload builder or renders the form. The suite still passes when NotConfigured defaults to writable Local, when an unsupported backend is coerced to Local, when unsafe file modes are submitted, or when one of the immutable Local inputs is accidentally unlocked. Extract the payload/state transition into a testable function or add component tests with a mocked useSSE; cover the backend null/unknown matrix, safe and unsafe modes, all three Local immutable controls, refresh conflicts, and the create/set-default partial-failure path.

@cxymds
cxymds marked this pull request as ready for review August 1, 2026 03:30
@cxymds
cxymds merged commit 67a8abb into main Aug 1, 2026
10 checks passed
@cxymds
cxymds deleted the reatang/local-kms-experience-safety branch August 1, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants