vault-env-loader (Python)
Load secrets from HashiCorp Vault's KV engine into environment variables before your application starts. Your code keeps reading os.environ / os.getenv — Django settings, Flask config, pydantic BaseSettings, django-environ, python-decouple all keep working — and the .env file full of plaintext passwords can go.
- Zero dependencies. One file, standard library only, Python ≥ 3.6. Old projects with pinned
requestsversions are not affected. - Fails fast. If a secret cannot be loaded, your application does not start (exit code 78), instead of failing later in business code.
- Imports like Spring Cloud Vault.
vault://secret/my-app/prod/database. - The token stays out of your app.
VAULT_TOKENis removed from the environment your application sees. - Token or AppRole. With AppRole it logs in once per start, reads, and revokes the session before your app runs.
- Works with HashiCorp Vault (KV v1 and v2, auto-detected) and with Vault-compatible servers.
- Same configuration and behaviour as the Node.js package.
Quick start
pip install vault-env-loader
Configure it where you deploy (instead of putting secrets in .env):
VAULT_ADDR=https://vault.example.com
VAULT_TOKEN=hvs.xxxxx # or VAULT_TOKEN_FILE=/run/secrets/vault-token
VAULT_ENV_IMPORT="vault://secret/order-service/prod/database, vault://secret/order-service/prod/redis?prefix=REDIS_"
Then pick one way to hook it in.
A. Wrap the start command (recommended — no code change)
vault-env-loader -- gunicorn app.wsgi
vault-env-loader -- uvicorn app.main:app --host 0.0.0.0
vault-env-loader -- celery -A proj worker
vault-env-loader -- python manage.py migrate
python -m vault_env_loader -- python app.py # when the scripts directory is not on PATH
ENTRYPOINT ["vault-env-loader", "--"]
CMD ["gunicorn", "app.wsgi", "--bind", "0.0.0.0:8000"]
Works for anything: systemd, supervisor, cron, shell scripts — the wrapped program does not even have to be Python. On Linux and macOS the wrapper replaces itself with your command (exec): same PID, signals and exit code need no forwarding, and PID 1 in a container is your application.
B. One line at the top of your entry file
import vault_env_loader; vault_env_loader.load() # must run before anything reads the configuration
Where "the top" is:
| Framework | Put it at the top of |
|---|---|
| Django | manage.py, wsgi.py and asgi.py (before django is imported), or the first line of settings.py |
| Flask / FastAPI | the module that creates the app, before the config is read |
| Celery | the module that creates the Celery() app |
| python-dotenv | before load_dotenv() |
load() blocks until every import is loaded, writes os.environ, removes VAULT_TOKEN, and returns the names it set. It does nothing if the environment was already loaded (for example by the wrapper, or in a reloader's child process).
C. Just get the values
from vault_env_loader import fetch
values = fetch() # {'DB_PASSWORD': '...', ...}; os.environ is not touched
Configuration
Connection settings use Vault's standard variable names, so the vault CLI, hvac and other clients share them. Options of this tool start with VAULT_ENV_. Command-line flags of the same name take precedence.
| Variable | Flag | Meaning |
|---|---|---|
VAULT_ADDR |
--addr |
Vault address. Required. |
VAULT_TOKEN |
— | Token. There is deliberately no --token flag: arguments are visible in ps. |
VAULT_TOKEN_FILE |
--token-file |
Read the token from a file instead. |
VAULT_ROLE_ID / VAULT_ROLE_ID_FILE |
--role-id-file |
AppRole role ID, used only when no token is set. |
VAULT_SECRET_ID / VAULT_SECRET_ID_FILE |
--secret-id-file |
AppRole secret ID. There is no --secret-id flag, for the same reason as --token. |
VAULT_ENV_APPROLE_MOUNT |
--approle-mount |
AppRole auth mount. Default approle. |
VAULT_NAMESPACE |
— | Vault Enterprise namespace. |
VAULT_CACERT / VAULT_CAPATH |
— | Extra CA certificate file / directory (every *.pem and *.crt in it). The system trust store is always used too. |
VAULT_ENV_IMPORT |
--import |
What to load, comma or newline separated (flag repeatable). Required. |
VAULT_ENV_BASE |
--base |
Base path for relative imports. |
VAULT_ENV_OVERRIDE |
--override |
true: replace variables that are already set. Default: keep them. |
VAULT_ENV_KEEP_TOKEN |
--keep-token |
true: leave VAULT_TOKEN (or the AppRole variables) in the application's environment. |
VAULT_ENV_TIMEOUT |
--timeout |
Seconds per request. Default 10. |
VAULT_ENV_RETRIES |
--retries |
Retries for transient failures. Default 3 (1 s, 2 s, 4 s). |
VAULT_ENV_ALLOW_HTTP |
— | true to allow plain http:// to a host other than localhost. Prints a warning on every start. |
TLS verification cannot be turned off. If VAULT_SKIP_VERIFY is set, vault-env-loader stops with an error rather than silently ignoring it. Point VAULT_CACERT at your CA instead.
Proxies: like every Python HTTP client, requests honour HTTPS_PROXY / HTTP_PROXY and NO_PROXY. Servers often set a global proxy for pip; if Vault is reachable directly, add its host to NO_PROXY. (The Node.js package does not use proxies.)
If neither VAULT_ADDR nor VAULT_ENV_IMPORT is set, nothing is loaded and the command runs anyway — the same start command works on a laptop without Vault. Setting only some of them is an error.
AppRole
When no token is configured and VAULT_ROLE_ID (or VAULT_ROLE_ID_FILE) is, vault-env-loader authenticates with AppRole:
VAULT_ADDR=https://vault.example.com
VAULT_ROLE_ID=59d6d1ca-47bb-4e7e-a40b-8be3bc5a0ba8
VAULT_SECRET_ID_FILE=/run/secrets/vault-secret-id # or VAULT_SECRET_ID=...
VAULT_ENV_BASE=secret/order-service/prod # see below
VAULT_ENV_IMPORT="database, redis?prefix=REDIS_"
POST auth/approle/login(orauth/<VAULT_ENV_APPROLE_MOUNT>/login) with the role ID and, if set, the secret ID;- the imports are read with the session token it returns;
auth/token/revoke-selfrevokes the session — also when a read failed. If the revoke fails, a warning is printed and the session simply expires;- your command starts (on POSIX the wrapper
execs it, after the revoke). The session token never enters its environment, and the AppRole variables that were used are removed (unlessVAULT_ENV_KEEP_TOKEN=true, which leaves them so the application can log in itself).
Things to know:
- A token wins. If
VAULT_TOKENorVAULT_TOKEN_FILEis set, it is used exactly as before and the AppRole variables are left untouched (a note says so). - Every start uses the secret ID once, and so does
check. A secret ID limited to a number of uses (secret_id_num_uses) suits one-off jobs; a service that restarts needs an unlimited one. - The policy needs
readon the paths, and thedefaultpolicy (forlookup-selfandrevoke-self) — AppRole tokens get it unless the role setstoken_no_default_policy. - Relative imports need a base path. AppRole tokens usually carry only
role_nameas metadata, so setVAULT_ENV_BASE, or write absolutevault://paths. A server that putsapplicationandenvironmentin the session's metadata gets the base from there, as with tokens. - The login is not repeated once it was sent. Connection failures before that, 429 and 503 are retried; a timeout or 5xx after the request went out is not, because Vault may already have used up a secret ID use.
- Response wrapping (
secret_iddelivered as a wrapping token) is not supported.
Imports
[optional:]<location>[?prefix=<PREFIX>&name=auto|verbatim|upper&kv=1|2]
location: vault://<mount>/<path> absolute, as in Spring Cloud Vault
<path> relative to the base path
vault:// the base path itself
optional:— skip the import if the path does not exist. Permission errors and outages still fail.prefix=— prepend to every variable name from this import.kv=— the KV engine version. Without it, vault-env-loader asks Vault (sys/internal/ui/mounts, likevault kv getdoes); the token's policy needsreadonsys/internal/ui/mounts/*for that, otherwise version 2 is assumed.- When several imports set the same variable, the later one wins (with a warning).
Relative imports and the base path
The base path is taken from, in order:
VAULT_ENV_BASE(e.g.secret/order-service/prod);- the token's metadata: if it has both
applicationandenvironment, the base issecret/<application>/<environment>. Create such a token withvault token create -policy=... -metadata=application=order-service -metadata=environment=prod.
With a token-derived base, every environment uses the same VAULT_ENV_IMPORT="database, redis" and only the token differs.
Variable names
Vault keys may contain characters that are not valid in environment variable names (db.password, spring.datasource.url). The name= option decides:
name= |
Rule | DB_PASSWORD |
db.password |
spring.datasource.url |
database_url |
|---|---|---|---|---|---|
auto (default) |
Valid names unchanged; others upper-cased with invalid characters → _ |
DB_PASSWORD |
DB_PASSWORD |
SPRING_DATASOURCE_URL |
database_url |
verbatim |
Unchanged (os.environ['db.password'] works; shells cannot use it) |
DB_PASSWORD |
db.password |
spring.datasource.url |
database_url |
upper |
Always upper-cased, invalid characters → _ |
DB_PASSWORD |
DB_PASSWORD |
SPRING_DATASOURCE_URL |
DATABASE_URL |
If two keys of one import map to the same name (db.password and db-password), loading fails and names both keys — order never silently decides. On Windows, names are compared case-insensitively. Non-string values (Vault allows JSON) are injected as compact JSON text.
Precedence
Variables that already exist in the environment win by default — the same rule as python-dotenv and Spring. That lets you override a value locally, and it means load_dotenv() called after vault-env-loader cannot overwrite Vault's values (unless you pass override=True), so a stale .env can stay in place until you have verified the migration. vault-env-loader prints the names (never the values) of variables it left alone. VAULT_ENV_OVERRIDE=true reverses this.
Checking a configuration
vault-env-loader check
prints the token's expiry and metadata, the path and version each import resolved to, and the variables that would be set — names and lengths only, never values.
Failures
Nothing is started unless every non-optional import loads. The process exits with 78 (EX_CONFIG) and one line on stderr; load() and fetch() raise VaultEnvError.
| Situation | Retried? |
|---|---|
| 401 / 403 — invalid, expired or revoked token, or the policy does not allow the path | no |
| AppRole login 400 / 403 — wrong, expired or used-up role ID / secret ID | no |
| AppRole login: connection not established, 429, 503 | yes |
| AppRole login: timeout, connection lost or 5xx after the request was sent | no |
404 — path does not exist (skipped if optional:) |
no |
| 429, 500, 502, 503, 504, connection errors, timeouts | yes, VAULT_ENV_RETRIES times — except a 502 that names keys with a permanent reason (e.g. alias 'x' could not be resolved: not_found), which fails at once |
| 3xx redirects | not followed (the token would go with them); fails |
A token that expires within 14 days produces a warning (not an AppRole session: those are short-lived by design).
Things to know
- Rotation needs a restart. Values are read once, at start — just like editing
.env.checkshows each path's version so you can tell whether a restart is due. - gunicorn / uwsgi / celery — with the wrapper, the master loads once and workers inherit the environment. With
load(), put it where the master imports it (e.g.gunicorn.conf.py, or use--preload) so that workers do not each read Vault. - Reloaders (
runserver,uvicorn --reload) start children with the loaded environment andVAULT_ENV_LOADED, so they do not read again. Restart the outer command to pick up new values. - One value is limited to 128 KB (the Linux limit for one environment string); larger values are refused with the key name.
- No
.pthauto-loading. Loading on every interpreter start (includingpip) would be invisible and fragile; the wrapper already needs no code change. - Windows has no
exec: the wrapper runs the command as a child and passes its exit code through. - Token and AppRole authentication; Kubernetes auth may follow.
Library API
from vault_env_loader import load, fetch, VaultEnvError
load(env=None, **options) # -> names set in os.environ (or in the mapping `env`)
fetch(env=None, **options) # -> {NAME: value}; does not touch the environment
options mirror the variables above: addr, token, token_file, role_id, role_id_file, secret_id, secret_id_file, approle_mount, imports, base, namespace, cacert, capath, override, keep_token, timeout, retries, allow_http. env is the mapping configuration is read from (and, for load(), written to); default os.environ. VaultEnvError has a code: config, auth, not_found, resolve, collision, value, unavailable. Messages contain paths and key names, never values or tokens.
Security notes
- Secret values are never written to logs, error messages, argv or disk. Unexpected errors print only the exception type, never a traceback.
- Redirects are not followed, so the token (and the AppRole login) is only ever sent to
VAULT_ADDR. - The AppRole login body is never logged;
checkshows only the first 8 characters of the role ID. - Requests carry
User-Agent: vault-env-loader/<version> python/<version>, so server audit logs can tell which client read what.
License
MIT
Metadata
Release files for vault-env-loader 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vault_env_loader-0.2.0.tar.gz | 20.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vault_env_loader-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.3 kB
Release files / vault_env_loader-0.2.0.tar.gz
| Download URL | vault_env_loader-0.2.0.tar.gz |
|---|---|
| Size | 20.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f100457c2b931acac6086c85b0f02944e0c07cfb00fb50d5ef1b2cbd2dba31c7
|
|
BLAKE2b-256 checksum How to use checksums |
07d86c157902ab21cd3de168945d4e6eafdd846bf06ed2346d868b9b52a8b4a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / vault_env_loader-0.2.0-py3-none-any.whl
| Download URL | vault_env_loader-0.2.0-py3-none-any.whl |
|---|---|
| Size | 20.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c6ef63a0b5cf12560f14654683a36290c3c33a00251a8ca0f269fe03e55e7662
|
|
BLAKE2b-256 checksum How to use checksums |
65319e9b0758fe0e4b4c8b022411fdea7c61d8dd6b357ddfbe70dc5e4b155e46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log