feat(secrets): managed secrets stored locally and injected into workspaces#647
Draft
skevetter wants to merge 13 commits into
Draft
feat(secrets): managed secrets stored locally and injected into workspaces#647skevetter wants to merge 13 commits into
skevetter wants to merge 13 commits into
Conversation
👷 Deploy Preview for images-devsy-sh processing.
|
✅ Deploy Preview for devsydev canceled.
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add a devsy secret command group (set/get/list/delete/attach/detach) that stores named secrets locally in the OS keyring, with an age-encrypted file fallback whose key is auto-generated (or passphrase-derived) and recorded in a metadata index. Secrets are per-context and can be bound to a context. Inject stored secrets into workspaces with 'up --secret NAME[,type=env|mount] [,target=X]': env delivery sets a lifecycle env var; mount delivery writes the value to a tmpfs at /run/secrets. Secret values travel to the container over the agent tunnel (not argv) and are redacted from lifecycle logs. A driver mount capability lets the Kubernetes driver map tmpfs to an in-memory emptyDir and rejects unsupported providers with a clear error. --secrets-file now expects flat JSON to match the devcontainer CLI. Includes CLI, desktop UI, docs, and e2e coverage.
skevetter
force-pushed
the
feat/managed-secrets
branch
from
July 24, 2026 04:26
54327d3 to
0758ded
Compare
Wire --build-secret values into image builds as BuildKit secrets so Dockerfiles consume them via RUN --mount=type=secret,id=NAME. The internal BuildKit builder serves them through a session secret provider; the docker buildx strategy passes them as env-backed --secret references (never argv or layers). Docker compose builds, which cannot mount build secrets, reject them with a clear error.
Wire the resolved --git-token into the credential path: the tunnel GitCredentials RPC returns the token only for its own host (never leaked to other hosts git contacts), served at the up and setup clone sites. Foundation resolves the token secret and infers the username from the repo host.
Add a per-entry Sensitive flag to the managed-value store: sensitive values go to the keyring/encrypted-file backend as before; non-sensitive values are stored inline in the index (no keyring, no encryption). Add a 'devsy env' facade (set/get/list/delete) over the same store for non-sensitive vars, and an 'up --env NAME[=TARGET]' flag to inject them. 'devsy secret' and 'devsy env' are thin facades differing only in sensitivity.
UpCmd.BuildSecrets shadowed the embedded CLIOptions.BuildSecrets, so resolved build secrets were written to the flag field and never reached CLIOptions (BuildKit saw none). Rename the flag field to BuildSecretNames and write resolved values to CLIOptions. Also split resolveStoredSecrets into per-type appliers (cyclop) and add RunWithEnv for buildx build secrets.
- Strip GitToken and BuildSecrets from the CLIOptions embedded in the container setup command argv (they were recoverable via ps/proc; now delivered only over the tunnel) [security] - Enforce the sensitive/non-sensitive boundary on reads: secret get/list/delete operate on secrets only, env get/delete on env vars only [security] - Guard the index so a sensitive value can never be serialized inline - Clear the stale backend value when an entry changes from sensitive to non-sensitive - Only verify the key source when a sensitive entry is actually accessed - Add Store.Meta for kind-aware facade checks
The CLI command group was renamed secrets->secret with no alias; the desktop IPC handlers still shelled to 'secrets', breaking every Secrets-page action.
Apply idiomatic-Go and desktop-UI review findings: - Replace opaque sensitive bool in Store API with a typed Kind enum (KindSecret/KindEnv), matching the repo's Backend/DockerBuilder pattern - Drop hand-rolled splitKeyValue for strings.Cut - Register secret/env value flags via pkg/flags builder + names constants - Restructure cmd/env into one-file-per-subcommand with exported *Cmd types - Add desktop env-var UI: EnvVar type, env_* IPC handlers, invokers, store, EnvPage, /env route, sidebar nav (plaintext value UX with reveal toggle) - Fix stale 'devsy secrets list' comment in ipc.ts
…tore) - Drop secret_attach/secret_detach IPC handlers and secretAttach/secretDetach invokers: never referenced by the renderer. CLI attach/detach stays; it is a live feature (bound context.Secrets are read at up via collectSecretRequests). - Remove secrets.NewStore(): zero callers, all paths use NewStoreForConfig.
- HIGH: redact build-secret values in debug log (build.go dumped BuildOptions with %+v, exposing NAME=VALUE plaintext despite out-of-band buildx delivery) - Harden auto key generation against a first-init TOCTOU: write the key file exclusively (O_EXCL, first-wins) and re-read after save so concurrent initializers converge on one persisted key instead of undecryptable splits - Fail --git-token when the source has no HTTP(S) host rather than building an unscoped token with Host="" - Align cmd/secrets Run methods with cmd/env: unused ctx -> _ context.Context
- HIGH: reject a sensitive secret passed via --env. applyEnvVars routed the value into WorkspaceEnv, which is NOT stripped from the compressed (gzip+ base64, reversible) setup argv, exposing it in the host process list. Now errors and directs to --secret (tunnel-delivered). - HIGH: path-traversal in writeSecretFiles. A mount target like target=../../etc/x escaped /run/secrets via filepath.Join; add secretMountPath containment guard. - HIGH: unset Kind silently downgraded a secret to a plaintext env var on load (empty kind => Sensitive()==false => Get returns ""). Normalize unknown Kind to KindSecret (fail-safe). - MEDIUM: index/backend writes were non-atomic and unlocked. Add atomicWriteFile (temp+rename) so a crash cannot corrupt the store, and a cross-process flock around store set/delete/last-used so concurrent writes cannot lose entries. - MEDIUM: key-gen convergence did not hold for the keyring backend; serialize auto-key resolution with a cross-process lock (covers both backends). - LOW: mount-secret values were absent from the lifecycle-hook log redactor; add them for redaction only (never injected into hook env). Adds tests for each. 1313 tests pass; golangci-lint clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a persistent, managed secrets feature. Previously secrets were ephemeral (a
--secrets-fileparsed atuptime); users can now store named secrets once and reference them by name.pkg/secrets): aStorebehind a smallbackendinterface with two implementations — the OS keyring (zalando/go-keyring) and anage-encrypted file fallback (filippo.io/age) for headless/CI. Only non-sensitive metadata (names, timestamps) is written to~/.devsy; values live in the backend, which is the source of truth. A YAML index enables listing since keyrings can't enumerate.SECRETS_BACKENDcontext option (auto/keyring/file), overridable per-invocation viaDEVSY_SECRETS_BACKEND.devsy secrets set/list/get/delete/attach/detach. Values are read via stdin/hidden prompt to keep them off argv.devsy up --secret NAME[=ENV_VAR](repeatable) plus per-context binding viaattach. Resolves into the existing lifecycle-env path. A missing requested/bound secret is a hard error, not a silent drop.SecretsPage+ nav entry.Secrets are per-context; deleting a context cascades to its secrets.
Notes / follow-ups
filebackend requiresDEVSY_SECRETS_PASSPHRASE; without a passphrase and without a usable keyring, operations fail with a clear error rather than proceeding insecurely.proserver-side secret sync is out of scope for this PR.New dependencies (
zalando/go-keyring,filippo.io/age) are cgo-free and cross-compile clean for all release targets.