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

Environment Forge keeps your encrypted vault outside the project directory using a Docker named volume. Secrets never live inside the application image.

Quick start

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

Docker Compose (recommended)

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 volume

The vault is stored on a Docker named volume โ€” not in your source code, not baked into the image, and fully persistent across container restarts.

Dockerfile

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

Pre-populating the volume

# 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 /eforge

Using EFORGE_SECRET (no key file)

If 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 myapp

Auto-detection

environment_forge.load() finds the vault automatically:

  1. EFORGE_VAULT_PATH env var โ†’ explicit override
  2. /eforge โ†’ Docker named volume mount
  3. /run/secrets/eforge โ†’ Docker secrets mount
  4. .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.3.tar.gz (29.3 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.3-py3-none-any.whl (22.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: environment_forge-0.0.3.tar.gz
  • Upload date:
  • Size: 29.3 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.3.tar.gz
Algorithm Hash digest
SHA256 1dc1e5c6999278592e0c82942e3a5c1a7b69907f1324b8ff4d2da55d913721cd
MD5 4cd1476c35228213450bce7480ceee93
BLAKE2b-256 702a06fa2ea86b2be7ec215ffe275827a83ea9ace88faafbb377409205cab88e

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for environment_forge-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 8cd0cf580507f1b81c49048711c0926c4f3f4305dab738af5485c6b53ff65c6f
MD5 e8ff6f7676c83e35057459ecf8d44c91
BLAKE2b-256 dcf059ceb9755a77692b58d640f4e3c3e78ec56aa4476cbab3628ccbf70976d4

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