feat(fleet): Fleet Secrets tab with env-var bundles (v1 MVP) (#965)

* feat(fleet): add Fleet Secrets tab with versioned env-var bundles (Skipper+)

Centralized, encrypted-at-rest secret bundles that can be pushed to labeled
nodes' stacks. Each save bumps a monotonic version; each push records a
per-node-per-version row in `secret_pushes` plus an entry in `audit_log`.
Conflict detection shows added/changed/unchanged/removed (informational)
diffs before write. Overlay merge preserves keys missing from the bundle.

- Adds `secrets`, `secret_versions`, `secret_pushes` tables.
- New `SecretsService` reuses CryptoService for AES-256-GCM, NodeLabelService
  for selectors, and direct fetch + Bearer for outbound calls to remote nodes.
- New `secretsRouter` with 9 endpoints under `/api/secrets`, gated by
  `requirePaid`. Mounted after the auth gate.
- Audit summary patterns added for the new routes.
- New Fleet › Secrets tab with bundle list, editor sheet (key=value rows,
  versions tab), and push wizard (selector, target stack, env file picker,
  per-node diff preview, results pills).
- Documentation: docs/features/fleet-secrets.mdx + docs.json nav entry.
- 26 Vitest cases cover parser, encryption, versioning, push aggregation,
  tier gating.

* fix(fleet): use const for rawValue in env parser

ESLint prefer-const flagged the let declaration as a CI-blocking error;
the variable is never reassigned.
This commit is contained in:
Anso
2026-05-07 06:03:39 -04:00
committed by GitHub
parent 52b46753af
commit 0f0b22c51a
14 changed files with 2550 additions and 21 deletions
+1
View File
@@ -132,6 +132,7 @@
"features/fleet-federation",
"features/fleet-actions",
"features/fleet-sync",
"features/fleet-secrets",
"features/fleet-backups",
"features/remote-updates",
"features/scheduled-operations"
+101
View File
@@ -0,0 +1,101 @@
---
title: "Fleet Secrets"
description: "Centralized, encrypted, versioned env-var bundles you can push to labeled nodes' stacks."
---
Fleet Secrets gives you one place to author the environment variables a stack needs, and one action to push them to every node that runs that stack. Bundles are encrypted at rest, every save bumps a version, and every push is recorded in the audit log with a per-node diff.
Fleet Secrets is available on **Skipper** and **Admiral** licenses. The tab is hidden on Community.
## What's in scope
| Today | Not yet |
|-------|---------|
| Bundles of `KEY=value` env pairs | Certificates, key files, credential JSON blobs |
| Push to nodes by label selector | Pin a bundle to specific node IDs |
| Versioning on every save with a change note | Scheduled or auto-rotation |
| Diff preview before each push | Multi-region or per-environment overlays |
If a feature is not in the table above, it's deferred to a later release. Fleet Secrets in 1.0 is intentionally focused on env-var drift across copies of the same stack.
## Create a bundle
1. Open **Fleet → Secrets**.
2. Click **New bundle**.
3. Give it a name (letters, digits, dot, dash, underscore; 264 chars) and an optional description.
4. Add `KEY=value` rows. Keys must match the env-var convention (`A-Z`, digits, underscore; cannot start with a digit). The eye icon toggles value visibility; the copy icon copies a single value to the clipboard.
5. Add a **Change note** (optional but recommended) explaining why this version exists.
6. Click **Save**. The bundle is now version `v1`.
Saving the bundle encrypts the payload with the instance's data key (AES-256-GCM via the same key that protects MFA secrets and registry credentials). The plaintext only ever exists in memory while you're editing.
## Edit and version
Editing a bundle creates a new version. Each version is immutable once saved. Open a bundle and switch to the **Versions** tab to see who saved each version and any change notes attached.
Saves are last-write-wins. There's no row-level lock; if two operators edit the same bundle concurrently, the later save creates the higher version number and the earlier save remains visible in history.
## Push to nodes
A push reads the bundle's current version, computes the diff against each target's existing env file, and writes the merged result.
1. From the bundle row, click the **Send** action to open the push wizard.
2. **Target tab:**
- Pick one or more node labels.
- Choose **any** (a node matches if it has any of the selected labels) or **all** (a node must have all selected labels).
- Enter the **Stack name** that the bundle should be applied to. The stack must exist on each target node by that name.
- Pick the **Env file** to write. The dropdown lists every file declared by the stack's compose on a representative target node — the canonical `.env` plus any files referenced via `env_file:` directives. Per-node compose files can differ; nodes that do not declare the chosen file are reported as failed for that push.
3. Click **Preview**.
4. **Preview tab:** the table shows one row per matched node. Each row reports a quick header (`+added · ~changed · ·unchanged`) and an expander with the per-key diff. Drift — keys present on the target that are absent from the bundle — is shown as informational `removed` rows. The push will not delete those keys.
5. Click **Push to N nodes**.
6. **Results tab:** per-node status pills (`ok`, `failed`, `skipped`). Hover a failure to see the error.
Pushes are sequential per bundle to make audit ordering predictable. Only one push for a given bundle can run at a time; a second concurrent push attempt returns `409 Conflict` until the first finishes.
## Merge semantics: overlay
Fleet Secrets uses **overlay** merge:
- A key in the bundle that is missing from the target is **added**.
- A key in both with a different value is **changed** to the bundle's value.
- A key in both with the same value is **unchanged**.
- A key on the target that is **not in the bundle** is preserved on the target. The diff shows it under `removed (drift)` so you can see the divergence, but the push leaves it alone.
This is deliberately conservative for a v1 push action. If a key truly needs to be removed from a target's env file, do it in the stack editor on that node, then push the bundle to bring the rest of the keys in line.
## Audit trail
Every mutating action shows up in **Audit Log**:
| Action | Audit summary |
|--------|---------------|
| Create bundle | `Created secret` |
| Update bundle (new version) | `Updated secret: <id>` |
| Delete bundle | `Deleted secret: <id>` |
| Import env from a stack | `Imported env into secret: <id>` |
| Preview a push | `Previewed secret push: <id>` |
| Execute a push | `Pushed secret: <id>` |
The execute-push action also writes one row per target node in `secret_pushes`, capturing the bundle id, version, push id (a UUID grouping all rows from the same push), node id, stack name, env file, status, error message, and per-node `added` / `changed` / `unchanged` counts.
## Troubleshooting
### "env file '<name>' not found on node <X>"
The node's compose file for that stack does not declare the env file you asked Fleet Secrets to write to. Either pick a different env file in the wizard's **Env file** dropdown, or update that node's compose to reference the file via `env_file:`.
### "stack not found"
The named stack does not exist on that node. Fleet Secrets pushes to existing stacks only — it does not create stack directories.
### A push partially succeeded
Per-node failures are independent. The bundle and other targets are unaffected; only the failing node missed the push. Fix the underlying cause (file declared in compose, stack created, node reachable) and re-run the push. Idempotent overlay means re-pushing is safe.
### "A push for this secret is already running"
Only one push per bundle runs at a time. Wait for the previous push to finish, then retry.
### Values look wrong on the target
Open the bundle and switch to **Versions** — confirm the version you intended to push is the current one. Open the wizard, run **Preview** without pushing, and read the diff. The preview is a faithful representation of what the next push will write.