Skip to content

Why your .env secrets shouldn't be plaintext on disk

On March 19, 2026, a threat actor known as TeamPCP compromised Aqua Security’s Trivy vulnerability scanner in a supply chain attack (CVE-2026-33634). The malicious code swept 50+ filesystem locations on CI/CD runners, harvesting SSH keys, AWS/GCP/Azure credentials, .env files, database passwords, Docker configs, and Kubernetes tokens. It then dumped Runner.Worker process memory to extract additional secrets.

The damage didn’t stop there. Credentials stolen from CI/CD pipelines cascaded downstream — within days, TeamPCP used harvested PyPI tokens to backdoor LiteLLM, an AI gateway library with ~3.4 million daily downloads. The compromised versions deployed a three-stage payload: credential harvesting, Kubernetes lateral movement, and a persistent backdoor for remote code execution. Other projects were also targeted in the same campaign.

This wasn’t an isolated pattern. In August 2024, Palo Alto’s Unit 42 documented an extortion campaign that found exposed .env files on 110,000+ domains and stole 90,000+ environment variables. GitHub’s own data shows 39 million secrets were committed to public repositories in 2024 alone.

The pattern is consistent: plaintext secrets on disk are a high-value, low-effort target.

Several tools address parts of this problem, and they’re all good at what they do:

SOPS encrypts config files in-place and supports cloud KMS backends (AWS, GCP, Azure). It’s excellent for production config management. But it has no shell integration for developer workflows, and multi-user key sharing requires manual .sops.yaml configuration.

direnv provides the shell integration developers want - environment variables load automatically when you cd into a project. But it provides zero encryption. Your secrets are still plaintext in .envrc.

HashiCorp Vault, Doppler, Infisical are powerful platforms with dynamic secrets, rotation, and web dashboards. They’re the right choice for production. But they require server infrastructure or SaaS accounts, overhead that doesn’t make sense when a developer just needs to share an API key with a teammate.

git-crypt encrypts files transparently in git, but operates at the file level with no per-secret granularity and no sharing workflow.

The gap: no tool combined encryption at rest + shell auto-loading + simple multi-user sharing without requiring a server or cloud account.

dotsecenv is a Go CLI that encrypts environment secrets at rest using hybrid encryption: AES-256-GCM for data, GPG for key exchange. Secrets are stored in a JSONL vault file that’s safe to commit to git. However, that is purely optional - you don’t actually have to commit the vault.

Terminal window
# Store a secret (encrypted in the vault)
echo "my-api-key" | dotsecenv secret store API_KEY
# Retrieve it (decrypted on-demand via GPG)
dotsecenv secret get API_KEY
# Share with a teammate (their GPG public key)
dotsecenv secret share API_KEY teammate@company.com

A shell plugin for zsh, bash, and fish decrypts and loads secrets when you enter a directory - like direnv, but your secrets stay encrypted on disk:

Terminal window
# .secenv file (safe to commit)
API_KEY={dotsecenv}
DATABASE_PASSWORD={dotsecenv}
# cd into the project directory, secrets load automatically
cd my-project/
echo $API_KEY # decrypted on-demand

GPG over age was a tradeoff - age is simpler, but I needed signatures for tamper detection and audit trails. Signatures enable:

  • Verifying who created each secret: every vault entry is signed by the originator’s key
  • Tamper detection: dotsecenv validate checks cryptographic integrity
  • Non-repudiation: audit trails with provable authorship

Plus most developers already have a GPG key from GitHub or GitLab commit signing. dotsecenv builds on that existing infrastructure. If, however, you don’t have a GPG key, you can generate one with dotsecenv identity create, without needing to get into the GPG nitty-gritty!

Algorithm defaults align with FIPS 186-5 (Ed25519, ECDSA P-384, RSA-3072+), and the tool refuses to operate with weaker algorithms.

I want to be upfront about the boundaries:

  • Process memory: once secrets are loaded as environment variables, they’re readable via /proc or memory dumps. The Trivy attack also dumped Runner.Worker process memory; encryption at rest doesn’t help there.
  • Compromised GPG private key: if your private key is stolen and unprotected, your secrets are exposed.
  • Root/admin access: a privileged attacker can read anything.
  • Production runtime: dotsecenv targets the development lifecycle (SDLC), not production secret injection. For production, use Vault, AWS Secrets Manager, or similar.

dotsecenv is defense-in-depth - it eliminates the plaintext-on-disk attack surface. If the Trivy attacker’s disk sweep had found GPG ciphertext instead of plaintext .env files, that component of the attack would have yielded nothing usable, or at the very least made the exfiltration much harder to effect. The LiteLLM cascade — where a single set of stolen plaintext credentials led to the compromise of a library with millions of daily downloads — illustrates exactly why encryption at rest matters at every stage of the supply chain.

dotsecenv is functional and I use it daily. It has:

There are rough edges. Windows support is WIP. The vault format is v2 and may evolve; I will generally aim to provide migration tooling for any breaking changes, but it may not always be viable. I’m looking for feedback from developers who deal with secrets in their daily workflow; if you try it out and have feedback, please open an issue on GitHub!

Thank you! 🙏


Further reading on the Trivy/TeamPCP campaign

Section titled “Further reading on the Trivy/TeamPCP campaign”

Next steps: