Skip to content

feat(secrets): managed secrets stored locally and injected into workspaces#647

Draft
skevetter wants to merge 13 commits into
mainfrom
feat/managed-secrets
Draft

feat(secrets): managed secrets stored locally and injected into workspaces#647
skevetter wants to merge 13 commits into
mainfrom
feat/managed-secrets

Conversation

@skevetter

Copy link
Copy Markdown
Contributor

Summary

Adds a persistent, managed secrets feature. Previously secrets were ephemeral (a --secrets-file parsed at up time); users can now store named secrets once and reference them by name.

  • Storage (pkg/secrets): a Store behind a small backend interface with two implementations — the OS keyring (zalando/go-keyring) and an age-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.
  • Backend selection: SECRETS_BACKEND context option (auto/keyring/file), overridable per-invocation via DEVSY_SECRETS_BACKEND.
  • CLI: devsy secrets set/list/get/delete/attach/detach. Values are read via stdin/hidden prompt to keep them off argv.
  • Injection: devsy up --secret NAME[=ENV_VAR] (repeatable) plus per-context binding via attach. Resolves into the existing lifecycle-env path. A missing requested/bound secret is a hard error, not a silent drop.
  • Docs: new Secrets page.
  • Desktop: IPC + store + SecretsPage + nav entry.

Secrets are per-context; deleting a context cascades to its secrets.

Notes / follow-ups

  • The file backend requires DEVSY_SECRETS_PASSPHRASE; without a passphrase and without a usable keyring, operations fail with a clear error rather than proceeding insecurely.
  • Interactive TTY passphrase prompt for the file backend is not yet implemented (env-var only today).
  • Platform/pro server-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.

@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

👷 Deploy Preview for images-devsy-sh processing.

Name Link
🔨 Latest commit b2b3215
🔍 Latest deploy log https://app.netlify.com/projects/images-devsy-sh/deploys/6a631e91a221c60008974786

@netlify

netlify Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

Name Link
🔨 Latest commit b2b3215
🔍 Latest deploy log https://app.netlify.com/projects/devsydev/deploys/6a631e923d20bd0007248e3a

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ae1fb431-7e37-4050-96f7-027a05e136a1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
skevetter force-pushed the feat/managed-secrets branch from 54327d3 to 0758ded Compare July 24, 2026 04:26
skevetter added 12 commits July 23, 2026 23:37
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant