Encrypted environment variable manager for Python.
Pure Python • No framework dependencies • Works everywhere
pip install environment-forge
| Problem | Solution |
|---|---|
.env files contain plaintext secrets |
Fernet-encrypted vault (AES-128-CBC + HMAC-SHA256) |
| Secrets leak through git, logs, env dumps | Single encrypted file — unreadable without the key |
| Different config for Docker vs local | Auto-detects the vault in both environments |
| No validation until runtime crash | Schema validation with type casting at startup |
| Tied to one framework | Pure Python — works with Django, FastAPI, Flask, or plain scripts |
pip install environment-forge
# Optional: install with rich for beautiful CLI tables
pip install environment-forge[rich]eforge initThis creates a .eforge/ directory in your project:
.eforge/
├── secret.key ← master key (auto-generated, chmod 600)
├── vault.enc ← encrypted variables
└── schema.json ← variable declarations (optional)
eforge set SECRET_KEY "django-insecure-change-me"
eforge set DATABASE_HOST localhost
eforge set DATABASE_PORT 5432
eforge set DATABASE_PASSWORD supersecret
eforge set DEBUG true# One line — works in any Python project
import environment_forge
environment_forge.load()
# Now os.environ has all your vault values
import os
print(os.environ["SECRET_KEY"]) # "django-insecure-change-me"
print(os.environ["DATABASE_HOST"]) # "localhost"That's it. No framework-specific setup. Pure Python.
# settings.py
import os
import environment_forge
# Load encrypted vault into os.environ BEFORE reading settings
environment_forge.load()
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ["DATABASE_HOST"],
"PORT": os.environ.get("DATABASE_PORT", "5432"),
"NAME": os.environ["DATABASE_NAME"],
"USER": os.environ["DATABASE_USER"],
"PASSWORD": os.environ["DATABASE_PASSWORD"],
}
}
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"# config.py
import os
import environment_forge
environment_forge.load()
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"# main.py
from config import DATABASE_URL, SECRET_KEY
app = FastAPI()# app.py
import os
import environment_forge
environment_forge.load()
app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ["DATABASE_URL"]import os
import environment_forge
environment_forge.load()
api_key = os.environ["API_KEY"]Define what variables your project expects, then validate before your app starts.
# Declare your schema
eforge schema add SECRET_KEY --sensitive --desc "Django secret key"
eforge schema add DATABASE_HOST --desc "Postgres host"
eforge schema add DATABASE_PORT --desc "Postgres port" --type int
eforge schema add DATABASE_PASSWORD --sensitive --desc "Postgres password"
eforge schema add DEBUG --optional --default false --type bool
# View it
eforge schema show╭───────────────────── Environment Schema ──────────────────────╮
│ # │ Key │ Required │ Type │ Default │ 🔒 │ ... │
├────┼───────────────────┼──────────┼──────┼─────────┼────┼─────┤
│ 1 │ SECRET_KEY │ ✔ │ str │ — │ 🔒 │ │
│ 2 │ DATABASE_HOST │ ✔ │ str │ — │ — │ │
│ 3 │ DATABASE_PORT │ ✔ │ int │ — │ — │ │
│ 4 │ DATABASE_PASSWORD │ ✔ │ str │ — │ 🔒 │ │
│ 5 │ DEBUG │ — │ bool │ false │ — │ │
╰────┴───────────────────┴──────────┴──────┴─────────┴────┴─────╯
# Validate the vault against the schema
eforge validate╭──────────────── Validation Report ─────────────────╮
│ Key │ Required │ Value │ Status │
├───────────────────┼──────────┼────────────┼─────────┤
│ SECRET_KEY │ required │ dja••••-me │ ✔ set │
│ DATABASE_HOST │ required │ localhost │ ✔ set │
│ DATABASE_PORT │ required │ 5432 │ ✔ set │
│ DATABASE_PASSWORD │ required │ sup••••ret │ ✔ set │
│ DEBUG │ optional │ false │ ↩ dflt │
╰───────────────────┴──────────┴────────────┴─────────╯
✔ All checks passed
from environment_forge import EnvSchema, EnvVar, load_and_validate
# Define schema as a list of EnvVar objects
SCHEMA = EnvSchema([
EnvVar("SECRET_KEY", required=True, sensitive=True, description="Django secret key"),
EnvVar("DATABASE_HOST", required=True, description="Postgres host"),
EnvVar("DATABASE_PORT", required=True, description="Postgres port", cast=int),
EnvVar("DATABASE_PASSWORD", required=True, sensitive=True, description="DB password"),
EnvVar("DEBUG", required=False, default="false", cast=bool, description="Debug mode"),
])
# Load vault + inject into os.environ + validate — all in one call
result = load_and_validate(SCHEMA)
# result.resolved contains type-casted values:
# {
# "SECRET_KEY": "django-insecure-change-me",
# "DATABASE_HOST": "localhost",
# "DATABASE_PORT": 5432, ← int
# "DATABASE_PASSWORD": "supersecret",
# "DEBUG": False, ← bool
# }If validation fails, you get a clear error and the process exits:
environment-forge: validation failed
Missing: REDIS_URL, API_KEY
Error: DATABASE_PORT: cannot cast 'abc' to int
| Command | Description |
|---|---|
eforge init |
Create a new encrypted vault |
eforge set KEY VALUE [-s section] |
Store or update a value |
eforge get KEY [-s section] [--raw] |
Retrieve a value |
eforge delete KEY [-s section] |
Remove a key |
eforge list [-s section] |
List all keys in a section |
eforge status |
Overview of all sections with schema annotations |
eforge sections |
List all section names |
| Command | Description |
|---|---|
eforge import .env [-s section] |
Import from a .env file into the vault |
eforge export [-s section] [-o file] |
Export as .env format |
eforge inject [-s section] |
Print export statements for shell sourcing |
| Command | Description |
|---|---|
eforge schema add KEY [options] |
Declare an expected variable |
eforge schema remove KEY |
Remove a declaration |
eforge schema show |
Display the full schema |
eforge validate |
Validate vault against schema |
--optional Mark as optional (default: required)
--desc TEXT Description
--type TYPE Type: str, int, float, bool
--default VALUE Default value when not set
--sensitive Mask value in output (passwords, tokens)
-s SECTION Section (default: default)
| Command | Description |
|---|---|
eforge destroy [-f] |
Delete vault and key from disk |
eforge --vault /path/to/.eforge set KEY VALUE # custom vault locationOrganise variables into logical groups:
eforge set HOST pg -s postgres
eforge set PORT 5432 -s postgres
eforge set URL redis://localhost -s redis
eforge list -s postgres # list only postgres keys
eforge export -s redis # export only redis section
eforge inject -s postgres # inject only postgres varsIn Python:
import environment_forge
# Inject only a specific section
environment_forge.load(section="postgres")
# Or inject everything
environment_forge.load()Environment Forge keeps your encrypted vault outside the project directory using a Docker named volume. Secrets never live inside the application image.
# 1. Build your vault locally
eforge init
eforge import .env
# 2. Copy vault to Docker volume path
eforge docker-init # copies .eforge → /eforge (or --path /my/path)services:
app:
build: .
volumes:
- eforge_data:/eforge # persistent named volume
environment:
- EFORGE_VAULT_PATH=/eforge # tell eforge where to look
volumes:
eforge_data: # Docker manages this volumeThe vault is stored on a Docker named volume — not in your source code, not baked into the image, and fully persistent across container restarts.
FROM python:3.12-slim
WORKDIR /app
RUN pip install environment-forge
# Declare the volume mount point
VOLUME /eforge
COPY . .
# Option 1: Shell injection
CMD ["sh", "-c", "eval $(eforge inject) && python manage.py runserver"]
# Option 2: Python injection (recommended)
# Just call environment_forge.load() in your settings/config
CMD ["python", "manage.py", "runserver"]# From your dev machine — copy local vault into a running container's volume
docker cp .eforge/vault.enc mycontainer:/eforge/vault.enc
docker cp .eforge/secret.key mycontainer:/eforge/secret.key
# Or use eforge docker-init with --copy-from
eforge docker-init --copy-from .eforge --path /eforgeIf you prefer not to store secret.key on the volume, pass the secret
via an environment variable:
services:
app:
build: .
volumes:
- eforge_data:/eforge
environment:
- EFORGE_VAULT_PATH=/eforge
- EFORGE_SECRET=${EFORGE_SECRET} # from .env or CI secrets
volumes:
eforge_data:# Get your secret key value
cat .eforge/secret.key
# Pass it securely (e.g. from CI/CD secrets manager)
docker run -e EFORGE_SECRET=<your-secret> -v eforge_data:/eforge myappenvironment_forge.load() finds the vault automatically:
EFORGE_VAULT_PATHenv var → explicit override/eforge→ Docker named volume mount/run/secrets/eforge→ Docker secrets mount.eforge/in current directory → local development
load(
section: str = None, # inject one section, or all if None
overwrite: bool = False, # overwrite existing os.environ keys?
vault_path: str = None, # explicit vault directory
validate: bool = False, # validate against schema.json if it exists
) -> Vaultload_and_validate(
schema: EnvSchema, # your schema object
section: str = None,
overwrite: bool = False,
vault_path: str = None,
) -> ValidationResultvault = Vault() # or Vault(path="/custom/.eforge")
vault.set("KEY", "value") # store
vault.set("KEY", "value", section="db") # store in section
vault.get("KEY") # retrieve (None if missing)
vault.delete("KEY") # remove
vault.all() # dict of all keys in default section
vault.flat() # dict of all keys across all sections
vault.sections() # list of section names
vault.import_env(".env") # import from .env file
vault.export_env() # export as .env text
vault.inject() # inject into os.environ
vault.inject_all() # inject all sectionsschema = EnvSchema([
EnvVar("KEY", required=True, description="...", cast=int, sensitive=True, default="0", section="default"),
])
result = schema.validate(vault)
# result.valid → bool
# result.resolved → dict of type-casted values
# result.missing → list of missing EnvVar objects
# result.errors → list of error strings
# Save/load schema as JSON (used by CLI validate)
schema.save(".eforge/")
schema = EnvSchema.load(".eforge/")| Variable | Description |
|---|---|
EFORGE_SECRET |
Master secret (alternative to secret.key file) |
EFORGE_VAULT_PATH |
Path to vault directory (overrides auto-detection) |
- Encryption: Fernet (AES-128-CBC + HMAC-SHA256) via
cryptography - Key derivation: SHA-256 hash → 32-byte Fernet key
- File permissions:
secret.keyandvault.enc→chmod 600 - Tamper detection: HMAC validated before decryption
- No plaintext on disk: All values in a single encrypted blob
- Only dependency:
cryptography(pure Python otherwise)
- Add
.eforge/to.gitignore— never commit secrets - Back up
secret.keyseparately — without it, the vault is unrecoverable - Use
EFORGE_SECRETenv var in CI/CD pipelines - Use
eforge validatein your entrypoint to fail fast
environment-forge/
├── src/
│ └── environment_forge/
│ ├── __init__.py # Public API: load(), load_and_validate()
│ ├── cli.py # eforge CLI (rich optional)
│ ├── crypto.py # Fernet encryption engine
│ ├── vault.py # Encrypted key-value store
│ ├── schema.py # EnvVar / EnvSchema / ValidationResult
│ └── loader.py # Framework-agnostic loader
├── tests/
├── pyproject.toml # pip install environment-forge
├── Makefile
├── LICENSE
└── README.md
git clone https://github.com/tarnasi/eforge.git
cd eforge
make dev # install with dev + rich deps
make test # run tests
make build # build for PyPI
make publish # upload to PyPIMIT — see LICENSE.