Zero-dependency, JIT-decrypted environment variable encryption for Python, Node.js, and C.
EnvShield encrypts your .env secrets at rest using AES-256-GCM and decrypts them strictly Just-In-Time in memory. No plaintext keys on disk. No pip install cryptography. No npm install dotenv-vault. Just native OS primitives and raw libcrypto bindings.
Every major supply-chain breach in the last year shares the same root cause: plaintext secrets sitting where they shouldn't be.
- Grafana (May 2026) — A stolen GitHub token gave attackers full codebase access.
- Trivy (March 2026) — Compromised GH Actions scraped
/proc/*/environto exfiltrate PATs, AWS keys, and crypto wallets from CI runners.
EnvShield was built to kill this attack vector dead:
- Secrets are never stored in plaintext —
.env.enccontains AES-256-GCM ciphertexts. - Master keys live in the OS keyring — GNOME Keyring, macOS Keychain, or Windows Credential Locker. Not in files. Not in env vars.
- Active environment wiping — If a CI runner injects a key via
ENV_SHIELD_KEY, EnvShield reads it once, thenmemsets the raw C environ block to null bytes. Any malware polling/procfinds nothing.
┌──────────────────────────────────────────────────────────────────┐
│ Developer Machine │
│ │
│ .env (plaintext) │
│ │ │
│ ▼ │
│ envshield-cli.py .env → .env.enc (AES-256-GCM ciphertext) │
│ setup wizard → ~/.envshield/config {"mode":"tpm"} │
│ → Master key → TPM / Keyring / file │
│ │
│ ┌─── Your Application ────────────────────────────────────┐ │
│ │ import envshield │ │
│ │ api_key = os.environ['SECRET_API_KEY'] │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 1. Read ~/.envshield/config → determine PK source │ │
│ │ 2. Fetch PK from configured source (tpm/keyring/env) │ │
│ │ 3. Decrypt ciphertext JIT via native libcrypto GCM │ │
│ │ 4. Wipe ENV_SHIELD_KEY from /proc (if env mode) │ │
│ │ 5. memset(0) the PK bytes from process memory │ │
│ │ 6. Return plaintext (key material destroyed) │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
The fastest way to get running is the interactive wizard. It detects your system, lets you pick a key storage mode, generates keys, and sets up integrations:
bash scripts/envshield-setup.shpowershell -ExecutionPolicy Bypass -File scripts\envshield-setup.ps1The wizard will walk you through:
- System detection — Checks for Python 3, libcrypto, TPM 2.0, and OS keyring availability.
- Mode selection — TPM (hardware), OS Keyring (software), CI/CD env var, or file-based.
- Key generation — Generate a new 256-bit key or import an existing one.
- Encryption — Encrypt a
.envfile or store individual service tokens. - Git integration — Optionally configure the Git credential helper.
Tip: For CI/CD pipelines, select the "CI/CD Environment Variable" mode. The wizard will display the key for you to paste into your provider's secrets UI.
python3 cli/envshield-cli.py .envThis generates:
.env.enc— JSON containing IV + auth tag + ciphertext per variable.- Master key → stored in your OS keyring automatically.
Headless / CI mode: Pass
--fileto write the key toenv-shield.keyinstead.python3 cli/envshield-cli.py --file .env
Drop the python/envshield/ directory into your project (or pip install . from python/):
import envshield # patches os.environ on import
import os
# Transparently decrypted JIT — the value never touches os.environ
api_key = os.environ['SECRET_API_KEY']The master key source is determined by ~/.envshield/config (written by the setup wizard):
| Mode | Config Value | Source | Use Case |
|---|---|---|---|
| TPM 2.0 | {"mode": "tpm"} |
Hardware-sealed blob via tpm2_unseal |
Production workstations, locked-down servers |
| OS Keyring | {"mode": "keyring"} |
GNOME Keyring / macOS Keychain / Windows Cred Locker | Desktops with session-encrypted keystores |
| Env Var | {"mode": "env"} |
ENV_SHIELD_KEY (auto-wiped from /proc after read) |
CI/CD runners (GitHub Actions, GitLab CI, Jenkins) |
| File | {"mode": "file"} |
Local env-shield.key file |
Development / testing only |
Set
ENVSHIELD_DEBUG=1for detailed step-by-step logging to stderr.
require('./node/envshield');
// process.env is intercepted via Proxy
const apiKey = process.env.SECRET_API_KEY;#include "envshield.h"
char *key = envshield_get("SECRET_API_KEY");
// Use key...
free(key); // caller owns the allocationThe --store command encrypts a single token and saves it globally under ~/.envshield/:
# GitHub PAT
python3 cli/envshield-cli.py --store github "ghp_xxxxxxxxxxxx"
# AWS Access Key
python3 cli/envshield-cli.py --store aws "AKIAIOSFODNN7EXAMPLE"
# NPM Token
python3 cli/envshield-cli.py --store npm "npm_xxxxxxxxxxxx"Master keys are stored in the most secure available backend (TPM 2.0 > OS keyring) under envshield-<service>.
EnvShield ships a native Git credential helper that decrypts your PAT JIT for every git push / git pull:
# 1. Store the token:
python3 cli/envshield-cli.py --store github "ghp_your_token"
# 2. Register the helper:
git config --global credential.helper \
"/usr/bin/env python3 $(realpath cli/git-credential-envshield.py)"The helper implements the standard Git credential protocol. It maps hosts to service names automatically:
| Host | Service |
|---|---|
github.com |
github |
gitlab.com |
gitlab |
bitbucket.org |
bitbucket |
When ENV_SHIELD_KEY is passed as an environment variable (typical in CI/CD), EnvShield performs a C-level memory scrub immediately after reading it:
# What happens internally:
libc = ctypes.CDLL(None)
environ_ptr = ctypes.POINTER(ctypes.c_char_p).in_dll(libc, "environ")
# Walk the array, find the entry, memset it to 0x00
libc.memset(environ_ptr[i], 0, len(raw_entry))Result: getenv("ENV_SHIELD_KEY") returns NULL. Child processes don't inherit it. Background malware scraping the environ array finds nothing.
Note:
/proc/self/environis a kernel-level snapshot created atexecve()time and is immutable. The wipe targets the live Cenvironarray, which is whatgetenv(), child process inheritance, and most scraping tools actually read.
You can also call the wipe function directly:
from envshield import wipe_environ_key
wipe_environ_key('AWS_SECRET_ACCESS_KEY')
wipe_environ_key('GITHUB_TOKEN')The master key source is explicitly configured — EnvShield does not guess. The setup wizard writes ~/.envshield/config with the selected mode.
// ~/.envshield/config
{"mode": "tpm"}Valid modes: tpm, keyring, env, file.
Every time your application reads an encrypted variable, EnvShield executes this exact sequence:
- Load
.env.encciphertext into memory. - Read
~/.envshield/configto determine the PK source. - Fetch the master key from the configured source only.
- Decrypt the ciphertext JIT via native libcrypto AES-256-GCM.
- Wipe
ENV_SHIELD_KEYfrom/proc/<pid>/environ(if env mode —memsetto null bytes). - Destroy the master key bytes from process memory (
memset(0)on the mutable buffer). - Return the plaintext. Key material no longer exists in the process.
Seals the master key to the TPM's Storage Root Key (SRK). Only unsealable on the same physical machine.
sudo apt install tpm2-tools # Ubuntu/Debian| Property | Detail |
|---|---|
| Seal target | Owner hierarchy SRK (tpm2_createprimary -C o) |
| Operations | tpm2_create → tpm2_load → tpm2_unseal |
| Portability | None (by design — key is bound to the silicon) |
| Disk artifact | ~/.envshield/tpm/<service>/key.pub + key.priv (useless without the TPM) |
Recovery: If the TPM is cleared or the hardware changes, the sealed key is irrecoverable. Use
--fileto create an offline backup before provisioning.
| Platform | Backend | Command |
|---|---|---|
| Linux | GNOME Keyring / KDE Wallet via secret-tool |
secret-tool lookup service envshield |
| macOS | Keychain via security |
security find-generic-password -s envshield -w |
| Windows | Credential Locker via advapi32.dll |
CredReadW / CredWriteW |
For headless CI/CD. EnvShield reads ENV_SHIELD_KEY once, then memsets the raw C environ block to null bytes.
env:
ENV_SHIELD_KEY: ${{ secrets.ENV_SHIELD_KEY }}For local development only. Writes env-shield.key to the working directory.
python3 cli/envshield-cli.py --file .envDo not commit this file. Add it to .gitignore.
The Python package has zero external dependencies. Install it locally:
cd python && pip install .Then use it in any script:
from envshield.core import encrypt, decrypt
from envshield import keyring
# Encrypt a value
key = os.urandom(32)
ciphertext = encrypt("my_secret", key)
# Store the key in the OS keyring
keyring.store_key("my-service", key.hex())
# Later: retrieve and decrypt
key_hex = keyring.get_key("my-service")
plaintext = decrypt(ciphertext, bytes.fromhex(key_hex))| Property | Implementation |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption with associated data) |
| Crypto Backend | OS-native libcrypto.so via Python ctypes / Node crypto / C libssl |
| Key Storage | TPM 2.0 (hardware) > OS keyring (software) — never plaintext on disk in production |
| Memory Safety | Decrypted values are transient — never written to os.environ or process.env |
| Environment Wiping | C-level memset(0) on the raw environ block after key extraction |
| Dependencies | Zero. No pip. No npm. No cgo. Pure native bindings. |
# Ubuntu / Debian
sudo apt install build-essential libssl-dev
# RHEL / Fedora
sudo yum install gcc openssl-devel
# macOS (Homebrew)
brew install openssl# Static library (libenvshield.a)
make
# Shared library (libenvshield.so)
make shared
# Build and run tests
make test
# Install to /usr/local
sudo make install# Static linking
gcc -o myapp myapp.c -L. -lenvshield -lssl -lcrypto
# Shared linking
gcc -o myapp myapp.c -L. -lenvshield -lssl -lcrypto
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH#include "envshield.h"
int main() {
char *key = envshield_getenv("SECRET_API_KEY");
if (key) {
printf("Key: %s\n", key);
envshield_free(key); // securely wipes + frees
}
return 0;
}Important: Always call
envshield_free()on the returned pointer. It usesvolatilememory zeroing beforefree()to ensure the plaintext doesn't linger in freed heap blocks.
Homebrew installs OpenSSL to a non-standard path. Uncomment the OPENSSL_DIR lines in the Makefile or pass it explicitly:
make OPENSSL_DIR=/usr/local/opt/openssl@3env-shield-project/
├── Makefile # C library build system
├── cli/
│ ├── envshield-cli.py # Encryption CLI (AES-256-GCM via libcrypto ctypes)
│ └── git-credential-envshield.py # Git credential helper (standard protocol)
├── python/
│ ├── setup.py # pip-installable package
│ └── envshield/
│ ├── __init__.py # os.environ interceptor + env wiping
│ ├── core.py # AES-256-GCM encrypt/decrypt via ctypes
│ └── keyring.py # Cross-platform keyring (TPM + OS + Windows)
├── node/
│ └── envshield.js # process.env Proxy interceptor
├── c/
│ ├── envshield.h # Public API header
│ └── envshield.c # Native C interceptor (links libssl)
├── scripts/
│ ├── envshield-setup.sh # Interactive setup wizard (Linux/macOS)
│ └── envshield-setup.ps1 # Interactive setup wizard (Windows)
├── test.sh # E2E integration tests
└── README.md
# Full E2E (Python + Node.js + C)
./test.sh
# C library only
make testMIT