Skip to main content

Encrypted environment variable manager โ€” set, read, and validate secrets with Fernet encryption.

Project description

Python versions License Version

๐Ÿ” Environment Forge

Encrypted environment variable manager for Python.
Pure Python โ€ข No framework dependencies โ€ข Works everywhere

pip install environment-forge


Why 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

Quick Start

1. Install

pip install environment-forge

# Optional: install with rich for beautiful CLI tables
pip install environment-forge[rich]

2. Initialise a vault

eforge init

This 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)

3. Store your secrets

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

4. Use in your project

# 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.


Framework Integration

Django

# 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"

FastAPI

# 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()

Flask

# 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"]

Plain Python / Scripts

import os
import environment_forge

environment_forge.load()

api_key = os.environ["API_KEY"]

Schema & Validation

Define what variables your project expects, then validate before your app starts.

Option A: CLI (no Python needed)

# 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

Option B: Python code (for programmatic schemas)

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

CLI Reference

Vault Management

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

Import / Export

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

Schema & Validation

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

Schema add options

--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)

Other

Command Description
eforge destroy [-f] Delete vault and key from disk

Global Options

eforge --vault /path/to/.eforge set KEY VALUE   # custom vault location

Sections

Organise 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 vars

In Python:

import environment_forge

# Inject only a specific section
environment_forge.load(section="postgres")

# Or inject everything
environment_forge.load()

Docker

Mount the vault as a volume

# Build your vault locally
eforge init && eforge import .env

# Mount into container
docker run -v $(pwd)/.eforge:/app/.eforge myapp

Pass the secret via environment

docker run -e EFORGE_SECRET=$(cat .eforge/secret.key) myapp

Docker Compose

services:
  app:
    build: .
    volumes:
      - ./.eforge:/app/.eforge:ro

In your Dockerfile

FROM python:3.12-slim
RUN pip install environment-forge
COPY . /app
WORKDIR /app

# Option 1: Shell injection
CMD ["sh", "-c", "eval $(eforge inject) && python manage.py runserver"]

# Option 2: Python injection (in your entrypoint)
CMD ["python", "manage.py", "runserver"]
# Just add `import environment_forge; environment_forge.load()` to settings.py

Auto-detection

environment_forge.load() finds the vault automatically:

  1. EFORGE_VAULT_PATH env var โ†’ explicit path
  2. /run/secrets/eforge โ†’ Docker secrets mount
  3. .eforge/ in current directory โ†’ local development

Python API Reference

environment_forge.load()

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
) -> Vault

environment_forge.load_and_validate()

load_and_validate(
    schema: EnvSchema,          # your schema object
    section: str = None,
    overwrite: bool = False,
    vault_path: str = None,
) -> ValidationResult

Vault

vault = 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 sections

EnvSchema & EnvVar

schema = 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/")

Environment Variables

Variable Description
EFORGE_SECRET Master secret (alternative to secret.key file)
EFORGE_VAULT_PATH Path to vault directory (overrides auto-detection)

Security

  • Encryption: Fernet (AES-128-CBC + HMAC-SHA256) via cryptography
  • Key derivation: SHA-256 hash โ†’ 32-byte Fernet key
  • File permissions: secret.key and vault.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)

Best Practices

  1. Add .eforge/ to .gitignore โ€” never commit secrets
  2. Back up secret.key separately โ€” without it, the vault is unrecoverable
  3. Use EFORGE_SECRET env var in CI/CD pipelines
  4. Use eforge validate in your entrypoint to fail fast

Project Structure

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

Development

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 PyPI

License

MIT โ€” see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

environment_forge-0.0.2.tar.gz (26.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

environment_forge-0.0.2-py3-none-any.whl (21.4 kB view details)

Uploaded Python 3

File details

Details for the file environment_forge-0.0.2.tar.gz.

File metadata

  • Download URL: environment_forge-0.0.2.tar.gz
  • Upload date:
  • Size: 26.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for environment_forge-0.0.2.tar.gz
Algorithm Hash digest
SHA256 19627be3cf59ffb2a5fb40d1e0eedd46b024d1b024c783c892c79c3f5904349e
MD5 61ecc27718fe775c45690d06057626b6
BLAKE2b-256 afccb3773a2e70bba74506c1382133600cd97cd4b7da61dceb9017d961e9c85c

See more details on using hashes here.

File details

Details for the file environment_forge-0.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for environment_forge-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1868078e366b7401dfb91876089ff131cc72401d24549cf6ba3816cb5ff6a8e4
MD5 b780ab2c3a8ddae8347e3c46eda0a4d8
BLAKE2b-256 0802991728738ef442b29abce700ea063c035edbd545251f7e93d7eab01a31c8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page