Devsy
Developing in a Workspace

Secrets in a Workspace

Devsy can inject sensitive values into a workspace without putting plaintext in devcontainer.json, command-line arguments or dotenv files. Values come from two places:

  • Devsy-managed secrets, stored in your OS keyring or in an encrypted local file.
  • External sources, such as a SOPS-encrypted file. Devsy reads them when needed and never copies them into its own store.

Both use the same delivery paths: environment variables for lifecycle commands, in-memory files under /run/secrets, and build secrets.

Devsy-managed secrets

Secrets are scoped to the active context. With the default auto backend, Devsy uses the OS keyring when there is one (Keychain, Credential Manager, or Secret Service on Linux). Otherwise it uses an age-encrypted secrets.enc file in the Devsy config directory. Only metadata such as names and timestamps is stored in plaintext.

devsy secret set DB_PASSWORD                                  # hidden prompt
printf '%s' "$MY_VALUE" | devsy secret set DB_PASSWORD --stdin # for scripts
devsy secret set TLS_KEY --from-file ./tls.key
devsy secret list      # never prints values
devsy secret get DB_PASSWORD
devsy secret delete DB_PASSWORD

In Desktop, Workspace Variables manages secrets and environment variables. Secret values are never shown.

External sources (SOPS)

Devsy reads SOPS-encrypted YAML, JSON and dotenv files. It does not need the sops executable. The files must be flat top-level key/value pairs:

DATABASE_PASSWORD: ENC[...]
API_TOKEN: ENC[...]

Register a local file as a named source. Devsy checks that it can decrypt the file first, and stores only the name, type, path and format override:

devsy secret source add sops project ./secrets.enc.yaml
devsy secret source list
devsy secret source remove project

A source cannot be removed while a context secret attachment still references it.

Sources declared by a repository

A repository can declare sources in customizations.devsy in its devcontainer.json:

{
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "customizations": {
    "devsy": {
      "secretSources": [
        { "name": "project", "type": "sops", "path": "./secrets.enc.yaml" }
      ],
      "secrets": ["sops:project/DATABASE_PASSWORD"]
    }
  }
}

Paths are relative to the repository root. Paths that escape it, including through symlinks, are rejected. This works for a local checkout (devsy workspace up .) and a remote repository, where Devsy reads the files from the same Git revision.

Credentials needed to clone a private repository must exist before Devsy can read the repository's SOPS files. A repository's own secret cannot authenticate the clone of that repository. --git-token can reference a managed secret or another source that is already available locally.

Credentials for SOPS

Devsy leaves key discovery to SOPS. For age, set SOPS_AGE_KEY or SOPS_AGE_KEY_FILE, or use the standard age key location. For AWS KMS, GCP KMS, Azure Key Vault and PGP, use the same configuration you would use with SOPS.

Referencing secrets

An unqualified name is a Devsy-managed secret: DB_PASSWORD. An external secret is source-qualified: sops:project/DB_PASSWORD. Devsy never searches other sources when a reference does not resolve. If any requested value cannot be resolved, workspace up fails.

Environment variables

--secret is repeatable and exposes the value to lifecycle commands during that workspace up. It does not appear in later terminal sessions.

devsy workspace up . --secret DB_PASSWORD
devsy workspace up . --secret sops:project/DATABASE_PASSWORD,target=DB_PASSWORD

target= renames the variable. Repository secrets bindings have the same scope.

Files

type=mount writes the value to an in-memory file under /run/secrets:

devsy workspace up . --secret sops:project/TLS_KEY,type=mount,target=tls.key

The file is /run/secrets/tls.key. Providers without an in-memory mount reject this with an error.

Build secrets

devsy workspace up . --build-secret sops:project/NPM_TOKEN

The build secret ID is the key (NPM_TOKEN), not the full reference. Use it in a Dockerfile with RUN --mount=type=secret,id=NPM_TOKEN ....

Attach a secret to a context

An attached secret is injected during workspace setup and into every new Devsy-managed SSH or terminal session, including devsy workspace ssh and the Desktop terminal:

devsy secret attach DB_PASSWORD
devsy secret attach sops:project/API_TOKEN
devsy secret detach DB_PASSWORD

Only the reference is stored. Already-running sessions do not change. After detaching, run workspace up or recreate the workspace so new sessions lose the value. If names collide, a session uses: explicit session environment (workspace ssh --set-env), then attached secret, then the container's own environment.

Terminal secrets are copied to /run/devsy/secrets-env, an owner-only tmpfs mount. If the provider cannot create that mount, setup fails instead of writing to the container's disk. Devsy may recreate an existing managed workspace to add it.

Where each kind is available

SourceLifecycle environmentNew SSH or terminal sessionsFile mountImage build
Attached secretYesYesNoNo
--secret NAMEYesNoNoNo
--secret NAME,type=mountNoNo/run/secrets/<target>No
--secrets-fileYesNoNoNo
Repository secrets bindingYesNoNoNo
--build-secret NAMENoNoNoBuildKit only

Environment variables are inherited by child processes. Use type=mount for a narrower scope.

How secrets are protected

  • SOPS sources are decrypted in memory and never written to disk.
  • Secrets are delivered as environment variables or memory-backed files.
  • Known secret values are redacted from logs and terminal output.
  • Values are kept out of client-server transport and workspace metadata.

Storage backends

Metadata is in secrets.yaml. Each secret records its backend when created, and it does not move if you change the preference later. Backends:

  • keyring: the OS keyring.
  • file: the age-encrypted secrets.enc.
  • auto: use keyring when available, otherwise file. This is only a preference for new secrets.

Set the preference for a context, or for one command with DEVSY_SECRETS_BACKEND:

devsy context set -o SECRETS_BACKEND=file

Passphrase for the file store

The file store is protected either by an automatically managed key or by a passphrase. The mode applies to the whole file, across all contexts, and is independent of the backend preference.

devsy secret protection status
devsy secret protection set-passphrase
devsy secret protection change-passphrase
devsy secret protection remove-passphrase

Devsy looks for the passphrase in this order, and a credential that fails is an error, with no fallback:

  1. Explicit input from the UI or process.
  2. DEVSY_SECRETS_PASSPHRASE.
  3. DEVSY_SECRETS_PASSPHRASE_FILE.
  4. A remembered credential in the OS keyring.
  5. An interactive prompt, where supported.

For automation, first run devsy secret protection set-passphrase, then point DEVSY_SECRETS_PASSPHRASE_FILE at a file of up to 64 KiB. The file only unlocks an existing passphrase-protected store. It does not choose the protection mode for a new store. Devsy strips one trailing newline, rejects an empty passphrase, and on Unix requires owner-only permissions such as 0600.

Remembering the passphrase is optional and shared by the CLI and Desktop:

devsy secret protection remember
devsy secret protection forget

forget removes only the saved credential. It does not change encryption or delete secrets. In Desktop, use Manage security on the Secrets tab. Changing protection needs confirmation in a native dialog.

Recovery

Without the passphrase or a valid remembered credential, the file contents cannot be decrypted. As a last resort, reset the whole file store:

devsy secret protection reset-file-store

This lists every file-backed secret across all contexts and asks you to type RESET, or pass --yes. It moves secrets.enc to a quarantine file, removes the file-store metadata, and removes the remembered credential when the keychain is reachable. It leaves keyring secrets and env.yaml alone. You must recreate the lost secrets.

Managed environment variables

For non-sensitive settings, devsy env stores plaintext values in env.yaml, separate from secrets. It works even when the secret store is locked. Never put a sensitive value here.

devsy env set LOG_LEVEL=debug
devsy env set REGION --value us-east-1
devsy env list
devsy env get LOG_LEVEL
devsy env delete LOG_LEVEL

Inject them at startup. The optional right-hand side renames the variable:

devsy workspace up ... --env LOG_LEVEL --env REGION=AWS_REGION

--env accepts only managed environment variables, because it is not the protected delivery path.

Attach a variable to the context to inject it on every start or recreate:

devsy env attach LOG_LEVEL
devsy env detach LOG_LEVEL

An explicit --env with a target overrides the attachment for that run. Attaching or detaching does not change a running workspace. Deleting a value also removes its attachment.

Converting between the two

Detach a name before converting it between an environment variable and a secret. Converting an environment variable to a secret finishes when the plaintext entry is removed. If the process stops midway, the name can exist in both stores. Retry devsy secret set, or run devsy env delete to remove the plaintext entry.

On this page