Skip to content

Security: adericbourg/env-starter

SECURITY.md

Security Policy

Supported versions

Only the latest release receives security fixes.

Reporting a vulnerability

Please do not open a public GitHub issue for a security vulnerability.

Instead, report it privately via GitHub Security Advisories. Provide as much detail as you can: affected versions, reproduction steps, and potential impact. You will receive a response within a few business days.

Self-update trust model

When env-starter update (or the startup update prompt) applies a new release, it goes through a two-step verification chain:

  1. TLS transport integrity — the release assets are fetched from https://github.com only; non-HTTPS redirects are rejected.
  2. Cosign signaturechecksums.txt is verified against a detached checksums.txt.sig using ECDSA-P256 over SHA-256. The project's public key is embedded in the binary at build time (internal/update/verify.go). A missing or invalid signature is a hard failure: the update is aborted and the binary is not replaced.
  3. Archive integrity — the sha256 digest of every downloaded archive is verified against the entry in checksums.txt before extraction.
  4. Extraction safety — zip-slip is prevented (only filepath.Base of the archive entry name is used, never the full path); the extracted binary is capped at 512 MiB to guard against decompression bombs.

The binary is replaced atomically (rename) so a partial update cannot leave the tool in a broken state.

Build provenance & SBOM

Each release archive also carries a Sigstore build-provenance attestation (keyless, via actions/attest-build-provenance) proving it was built by the Release GitHub Actions workflow from a specific commit, and an SPDX SBOM (<archive>.spdx.json, generated by syft) listing everything bundled in it. Verify an archive with:

gh attestation verify env-starter_<version>_<os>_<arch>.tar.gz \
  --repo adericbourg/env-starter

These are additive transparency features, not part of the runtime trust chain above: the self-updater still relies solely on the cosign-signed checksums.txt and the embedded public key, so the offline trust model is unchanged.

Local trust model

env-starter is a strictly per-user tool. Everything it persists lives under os.UserCacheDir()/env-starter/ (~/.cache/env-starter on Linux), which is enforced owner-only (0700): the directory is created private, a pre-existing directory with looser permissions is tightened, and a directory owned by another user is rejected outright.

Inside that boundary:

  • Daemon socket — the background daemon listens on a unix socket (daemon.sock, 0600) inside the owner-only directory. The socket is created with a restrictive umask so it is never connectable by other users, even briefly. As defence in depth the daemon also verifies each connecting peer's uid against its own (SO_PEERCRED on Linux, LOCAL_PEERCRED on macOS) and rejects mismatches. This matters because a connected peer can start configured environments — i.e. run the owner's commands — and shut the daemon down.
  • Source cache — downloaded url sources and GitHub clones live in predictable subdirectories and are executed as code, so pre-existing cache content is only reused after the ownership check above passes.
  • Log files — per-command logs are 0600 in an owner-only directory and are never written to a shared location: if the per-user cache directory cannot be resolved, env-starter fails instead of falling back to a world-readable temp dir.

There is no setuid, no TCP listener, and no cross-user IPC of any kind.

URL source integrity

url sources are downloaded over https only (redirects must stay on https) and are then executed as code. Declare a checksum so a tampered or swapped artifact at the origin is detected; without one the download is trusted on TLS alone and env-starter prints a startup warning. Set require-checksums: true at the top level of the config to make a missing checksum a hard validation error (recommended for shared configs; an overlay can never relax it).

Config trust (approval on first use)

A command's run, setup, teardown, and readiness.shell fields are passed directly to sh -c. This is intentional — they are shell scripts — so env-starter does not sanitize them; instead it gates which config files it will load from at all.

Every config file (the base config and any --config-overlay) is hashed with sha256, and env-starter refuses to load, execute, or hot-reload a file whose current hash has not been explicitly approved. Approval is recorded under the owner-only cache dir (trust.json) and is invalidated the instant a file's content changes, so a later edit — legitimate or a tampered/slipped-in change — always requires a fresh review before anything from it runs.

Review and approve a config with:

env-starter allow            # preview every run/setup/teardown/readiness.shell command, then prompt
env-starter allow --print    # preview only, writes no approval
env-starter allow --yes      # approve without prompting (e.g. in scripts)

This defends against a config or overlay that was tampered with or slipped in without your knowledge — it does not make the fields themselves any safer to author. Only write (or approve) run/setup/teardown commands you would be comfortable running yourself: approving a config is equivalent to granting it code execution under your user account.

Secrets in configuration

Commands and environments can declare env (environment variables) — see Configuration reference: env and Secrets and overrides. This is the intended place for secrets such as API keys and tokens:

  • Never commit or share secrets in the base config. Put them in a separate, non-committed, non-shared file and apply it with --config-overlay, added to .gitignore. The overlay still goes through the same approval gate as the base config.
  • Prefer env over inlining secrets into run/setup/teardown. Values passed via env are not visible in ps or /proc/<pid>/cmdline, unlike values interpolated directly into a command string. (/proc/<pid>/environ remains readable by the same user or root — standard OS behavior, unrelated to env-starter.)
  • env-starter never logs env values. Configuration validation errors (including the conflict check for a shared command's env, see Configuration reference: Validation rules) reference key names and environment names only, never values.
  • A shared/overlaid env that sets a variable like PATH or LD_PRELOAD/DYLD_INSERT_LIBRARIES can influence another environment's command the next time it starts. This is equivalent to arbitrary code via run/setup/teardown and is covered by the same config-trust boundary above — only approve configs and overlays you trust.
  • Command output is not redacted (see Command log confidentiality below): a process that echoes an env var containing a secret will have it appear in its log.

Command log confidentiality

Command stdout/stderr is stored in log files under os.UserCacheDir()/env-starter/. These files are created with 0600 permissions (owner read/write only). Logs are not redacted: if a subprocess echoes secrets (tokens, passwords), those appear in the log file. Protect the cache directory accordingly.

There aren't any published security advisories