vault-env-loader 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaylov H
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ prune tests
@@ -0,0 +1,208 @@
1
+ Metadata-Version: 2.4
2
+ Name: vault-env-loader
3
+ Version: 0.1.0
4
+ Summary: Load HashiCorp Vault KV secrets into environment variables before your app starts. Zero dependencies.
5
+ Author: Kaylov H
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kaylovh/vault-env-loader/tree/main/python#readme
8
+ Project-URL: Source, https://github.com/kaylovh/vault-env-loader
9
+ Project-URL: Issues, https://github.com/kaylovh/vault-env-loader/issues
10
+ Keywords: vault,hashicorp,secrets,env,environment,dotenv,kv,config
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Python: >=3.6
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # vault-env-loader (Python)
26
+
27
+ 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.
28
+
29
+ - **Zero dependencies.** One file, standard library only, Python ≥ 3.6. Old projects with pinned `requests` versions are not affected.
30
+ - **Fails fast.** If a secret cannot be loaded, your application does not start (exit code 78), instead of failing later in business code.
31
+ - **Imports like Spring Cloud Vault.** `vault://secret/my-app/prod/database`.
32
+ - **The token stays out of your app.** `VAULT_TOKEN` is removed from the environment your application sees.
33
+ - Works with HashiCorp Vault (KV v1 and v2, auto-detected) and with Vault-compatible servers.
34
+ - Same configuration and behaviour as the [Node.js package](https://github.com/kaylovh/vault-env-loader/tree/main/node).
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ pip install vault-env-loader
40
+ ```
41
+
42
+ Configure it where you deploy (instead of putting secrets in `.env`):
43
+
44
+ ```bash
45
+ VAULT_ADDR=https://vault.example.com
46
+ VAULT_TOKEN=hvs.xxxxx # or VAULT_TOKEN_FILE=/run/secrets/vault-token
47
+ VAULT_ENV_IMPORT="vault://secret/order-service/prod/database, vault://secret/order-service/prod/redis?prefix=REDIS_"
48
+ ```
49
+
50
+ Then pick **one** way to hook it in.
51
+
52
+ ### A. Wrap the start command (recommended — no code change)
53
+
54
+ ```bash
55
+ vault-env-loader -- gunicorn app.wsgi
56
+ vault-env-loader -- uvicorn app.main:app --host 0.0.0.0
57
+ vault-env-loader -- celery -A proj worker
58
+ vault-env-loader -- python manage.py migrate
59
+ python -m vault_env_loader -- python app.py # when the scripts directory is not on PATH
60
+ ```
61
+
62
+ ```dockerfile
63
+ ENTRYPOINT ["vault-env-loader", "--"]
64
+ CMD ["gunicorn", "app.wsgi", "--bind", "0.0.0.0:8000"]
65
+ ```
66
+
67
+ 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.
68
+
69
+ ### B. One line at the top of your entry file
70
+
71
+ ```python
72
+ import vault_env_loader; vault_env_loader.load() # must run before anything reads the configuration
73
+ ```
74
+
75
+ Where "the top" is:
76
+
77
+ | Framework | Put it at the top of |
78
+ |---|---|
79
+ | Django | `manage.py`, `wsgi.py` and `asgi.py` (before `django` is imported), or the first line of `settings.py` |
80
+ | Flask / FastAPI | the module that creates the app, before the config is read |
81
+ | Celery | the module that creates the `Celery()` app |
82
+ | python-dotenv | **before** `load_dotenv()` |
83
+
84
+ `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).
85
+
86
+ ### C. Just get the values
87
+
88
+ ```python
89
+ from vault_env_loader import fetch
90
+ values = fetch() # {'DB_PASSWORD': '...', ...}; os.environ is not touched
91
+ ```
92
+
93
+ ## Configuration
94
+
95
+ 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.
96
+
97
+ | Variable | Flag | Meaning |
98
+ |---|---|---|
99
+ | `VAULT_ADDR` | `--addr` | Vault address. Required. |
100
+ | `VAULT_TOKEN` | — | Token. There is deliberately no `--token` flag: arguments are visible in `ps`. |
101
+ | `VAULT_TOKEN_FILE` | `--token-file` | Read the token from a file instead. |
102
+ | `VAULT_NAMESPACE` | — | Vault Enterprise namespace. |
103
+ | `VAULT_CACERT` / `VAULT_CAPATH` | — | Extra CA certificate file / directory (every `*.pem` and `*.crt` in it). The system trust store is always used too. |
104
+ | `VAULT_ENV_IMPORT` | `--import` | What to load, comma or newline separated (flag repeatable). Required. |
105
+ | `VAULT_ENV_BASE` | `--base` | Base path for relative imports. |
106
+ | `VAULT_ENV_OVERRIDE` | `--override` | `true`: replace variables that are already set. Default: keep them. |
107
+ | `VAULT_ENV_KEEP_TOKEN` | `--keep-token` | `true`: leave `VAULT_TOKEN` in the application's environment. |
108
+ | `VAULT_ENV_TIMEOUT` | `--timeout` | Seconds per request. Default 10. |
109
+ | `VAULT_ENV_RETRIES` | `--retries` | Retries for transient failures. Default 3 (1 s, 2 s, 4 s). |
110
+ | `VAULT_ENV_ALLOW_HTTP` | — | `true` to allow plain `http://` to a host other than localhost. Prints a warning on every start. |
111
+
112
+ **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.
113
+
114
+ **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.)
115
+
116
+ 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.
117
+
118
+ ## Imports
119
+
120
+ ```
121
+ [optional:]<location>[?prefix=<PREFIX>&name=auto|verbatim|upper&kv=1|2]
122
+
123
+ location: vault://<mount>/<path> absolute, as in Spring Cloud Vault
124
+ <path> relative to the base path
125
+ vault:// the base path itself
126
+ ```
127
+
128
+ - **`optional:`** — skip the import if the path does not exist. Permission errors and outages still fail.
129
+ - **`prefix=`** — prepend to every variable name from this import.
130
+ - **`kv=`** — the KV engine version. Without it, vault-env-loader asks Vault (`sys/internal/ui/mounts`, like `vault kv get` does); the token's policy needs `read` on `sys/internal/ui/mounts/*` for that, otherwise version 2 is assumed.
131
+ - When several imports set the same variable, **the later one wins** (with a warning).
132
+
133
+ ### Relative imports and the base path
134
+
135
+ The base path is taken from, in order:
136
+
137
+ 1. `VAULT_ENV_BASE` (e.g. `secret/order-service/prod`);
138
+ 2. the token's metadata: if it has both `application` and `environment`, the base is `secret/<application>/<environment>`. Create such a token with `vault token create -policy=... -metadata=application=order-service -metadata=environment=prod`.
139
+
140
+ With a token-derived base, every environment uses **the same** `VAULT_ENV_IMPORT="database, redis"` and only the token differs.
141
+
142
+ ## Variable names
143
+
144
+ Vault keys may contain characters that are not valid in environment variable names (`db.password`, `spring.datasource.url`). The `name=` option decides:
145
+
146
+ | `name=` | Rule | `DB_PASSWORD` | `db.password` | `spring.datasource.url` | `database_url` |
147
+ |---|---|---|---|---|---|
148
+ | `auto` (default) | Valid names unchanged; others upper-cased with invalid characters → `_` | `DB_PASSWORD` | `DB_PASSWORD` | `SPRING_DATASOURCE_URL` | `database_url` |
149
+ | `verbatim` | Unchanged (`os.environ['db.password']` works; shells cannot use it) | `DB_PASSWORD` | `db.password` | `spring.datasource.url` | `database_url` |
150
+ | `upper` | Always upper-cased, invalid characters → `_` | `DB_PASSWORD` | `DB_PASSWORD` | `SPRING_DATASOURCE_URL` | `DATABASE_URL` |
151
+
152
+ 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.
153
+
154
+ ## Precedence
155
+
156
+ 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.
157
+
158
+ ## Checking a configuration
159
+
160
+ ```bash
161
+ vault-env-loader check
162
+ ```
163
+
164
+ 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**.
165
+
166
+ ## Failures
167
+
168
+ 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`.
169
+
170
+ | Situation | Retried? |
171
+ |---|---|
172
+ | 401 / 403 — invalid, expired or revoked token, or the policy does not allow the path | no |
173
+ | 404 — path does not exist (skipped if `optional:`) | no |
174
+ | 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 |
175
+ | 3xx redirects | not followed (the token would go with them); fails |
176
+
177
+ A token that expires within 14 days produces a warning.
178
+
179
+ ## Things to know
180
+
181
+ - **Rotation needs a restart.** Values are read once, at start — just like editing `.env`. `check` shows each path's version so you can tell whether a restart is due.
182
+ - **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.
183
+ - **Reloaders** (`runserver`, `uvicorn --reload`) start children with the loaded environment and `VAULT_ENV_LOADED`, so they do not read again. Restart the outer command to pick up new values.
184
+ - **One value is limited to 128 KB** (the Linux limit for one environment string); larger values are refused with the key name.
185
+ - **No `.pth` auto-loading.** Loading on every interpreter start (including `pip`) would be invisible and fragile; the wrapper already needs no code change.
186
+ - **Windows** has no `exec`: the wrapper runs the command as a child and passes its exit code through.
187
+ - Only token authentication for now; AppRole and Kubernetes auth may follow.
188
+
189
+ ## Library API
190
+
191
+ ```python
192
+ from vault_env_loader import load, fetch, VaultEnvError
193
+
194
+ load(env=None, **options) # -> names set in os.environ (or in the mapping `env`)
195
+ fetch(env=None, **options) # -> {NAME: value}; does not touch the environment
196
+ ```
197
+
198
+ `options` mirror the variables above: `addr`, `token`, `token_file`, `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.
199
+
200
+ ## Security notes
201
+
202
+ - Secret values are never written to logs, error messages, argv or disk. Unexpected errors print only the exception type, never a traceback.
203
+ - Redirects are not followed, so the token is only ever sent to `VAULT_ADDR`.
204
+ - Requests carry `User-Agent: vault-env-loader/<version> python/<version>`, so server audit logs can tell which client read what.
205
+
206
+ ## License
207
+
208
+ MIT
@@ -0,0 +1,184 @@
1
+ # vault-env-loader (Python)
2
+
3
+ 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.
4
+
5
+ - **Zero dependencies.** One file, standard library only, Python ≥ 3.6. Old projects with pinned `requests` versions are not affected.
6
+ - **Fails fast.** If a secret cannot be loaded, your application does not start (exit code 78), instead of failing later in business code.
7
+ - **Imports like Spring Cloud Vault.** `vault://secret/my-app/prod/database`.
8
+ - **The token stays out of your app.** `VAULT_TOKEN` is removed from the environment your application sees.
9
+ - Works with HashiCorp Vault (KV v1 and v2, auto-detected) and with Vault-compatible servers.
10
+ - Same configuration and behaviour as the [Node.js package](https://github.com/kaylovh/vault-env-loader/tree/main/node).
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ pip install vault-env-loader
16
+ ```
17
+
18
+ Configure it where you deploy (instead of putting secrets in `.env`):
19
+
20
+ ```bash
21
+ VAULT_ADDR=https://vault.example.com
22
+ VAULT_TOKEN=hvs.xxxxx # or VAULT_TOKEN_FILE=/run/secrets/vault-token
23
+ VAULT_ENV_IMPORT="vault://secret/order-service/prod/database, vault://secret/order-service/prod/redis?prefix=REDIS_"
24
+ ```
25
+
26
+ Then pick **one** way to hook it in.
27
+
28
+ ### A. Wrap the start command (recommended — no code change)
29
+
30
+ ```bash
31
+ vault-env-loader -- gunicorn app.wsgi
32
+ vault-env-loader -- uvicorn app.main:app --host 0.0.0.0
33
+ vault-env-loader -- celery -A proj worker
34
+ vault-env-loader -- python manage.py migrate
35
+ python -m vault_env_loader -- python app.py # when the scripts directory is not on PATH
36
+ ```
37
+
38
+ ```dockerfile
39
+ ENTRYPOINT ["vault-env-loader", "--"]
40
+ CMD ["gunicorn", "app.wsgi", "--bind", "0.0.0.0:8000"]
41
+ ```
42
+
43
+ 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.
44
+
45
+ ### B. One line at the top of your entry file
46
+
47
+ ```python
48
+ import vault_env_loader; vault_env_loader.load() # must run before anything reads the configuration
49
+ ```
50
+
51
+ Where "the top" is:
52
+
53
+ | Framework | Put it at the top of |
54
+ |---|---|
55
+ | Django | `manage.py`, `wsgi.py` and `asgi.py` (before `django` is imported), or the first line of `settings.py` |
56
+ | Flask / FastAPI | the module that creates the app, before the config is read |
57
+ | Celery | the module that creates the `Celery()` app |
58
+ | python-dotenv | **before** `load_dotenv()` |
59
+
60
+ `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).
61
+
62
+ ### C. Just get the values
63
+
64
+ ```python
65
+ from vault_env_loader import fetch
66
+ values = fetch() # {'DB_PASSWORD': '...', ...}; os.environ is not touched
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ 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.
72
+
73
+ | Variable | Flag | Meaning |
74
+ |---|---|---|
75
+ | `VAULT_ADDR` | `--addr` | Vault address. Required. |
76
+ | `VAULT_TOKEN` | — | Token. There is deliberately no `--token` flag: arguments are visible in `ps`. |
77
+ | `VAULT_TOKEN_FILE` | `--token-file` | Read the token from a file instead. |
78
+ | `VAULT_NAMESPACE` | — | Vault Enterprise namespace. |
79
+ | `VAULT_CACERT` / `VAULT_CAPATH` | — | Extra CA certificate file / directory (every `*.pem` and `*.crt` in it). The system trust store is always used too. |
80
+ | `VAULT_ENV_IMPORT` | `--import` | What to load, comma or newline separated (flag repeatable). Required. |
81
+ | `VAULT_ENV_BASE` | `--base` | Base path for relative imports. |
82
+ | `VAULT_ENV_OVERRIDE` | `--override` | `true`: replace variables that are already set. Default: keep them. |
83
+ | `VAULT_ENV_KEEP_TOKEN` | `--keep-token` | `true`: leave `VAULT_TOKEN` in the application's environment. |
84
+ | `VAULT_ENV_TIMEOUT` | `--timeout` | Seconds per request. Default 10. |
85
+ | `VAULT_ENV_RETRIES` | `--retries` | Retries for transient failures. Default 3 (1 s, 2 s, 4 s). |
86
+ | `VAULT_ENV_ALLOW_HTTP` | — | `true` to allow plain `http://` to a host other than localhost. Prints a warning on every start. |
87
+
88
+ **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.
89
+
90
+ **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.)
91
+
92
+ 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.
93
+
94
+ ## Imports
95
+
96
+ ```
97
+ [optional:]<location>[?prefix=<PREFIX>&name=auto|verbatim|upper&kv=1|2]
98
+
99
+ location: vault://<mount>/<path> absolute, as in Spring Cloud Vault
100
+ <path> relative to the base path
101
+ vault:// the base path itself
102
+ ```
103
+
104
+ - **`optional:`** — skip the import if the path does not exist. Permission errors and outages still fail.
105
+ - **`prefix=`** — prepend to every variable name from this import.
106
+ - **`kv=`** — the KV engine version. Without it, vault-env-loader asks Vault (`sys/internal/ui/mounts`, like `vault kv get` does); the token's policy needs `read` on `sys/internal/ui/mounts/*` for that, otherwise version 2 is assumed.
107
+ - When several imports set the same variable, **the later one wins** (with a warning).
108
+
109
+ ### Relative imports and the base path
110
+
111
+ The base path is taken from, in order:
112
+
113
+ 1. `VAULT_ENV_BASE` (e.g. `secret/order-service/prod`);
114
+ 2. the token's metadata: if it has both `application` and `environment`, the base is `secret/<application>/<environment>`. Create such a token with `vault token create -policy=... -metadata=application=order-service -metadata=environment=prod`.
115
+
116
+ With a token-derived base, every environment uses **the same** `VAULT_ENV_IMPORT="database, redis"` and only the token differs.
117
+
118
+ ## Variable names
119
+
120
+ Vault keys may contain characters that are not valid in environment variable names (`db.password`, `spring.datasource.url`). The `name=` option decides:
121
+
122
+ | `name=` | Rule | `DB_PASSWORD` | `db.password` | `spring.datasource.url` | `database_url` |
123
+ |---|---|---|---|---|---|
124
+ | `auto` (default) | Valid names unchanged; others upper-cased with invalid characters → `_` | `DB_PASSWORD` | `DB_PASSWORD` | `SPRING_DATASOURCE_URL` | `database_url` |
125
+ | `verbatim` | Unchanged (`os.environ['db.password']` works; shells cannot use it) | `DB_PASSWORD` | `db.password` | `spring.datasource.url` | `database_url` |
126
+ | `upper` | Always upper-cased, invalid characters → `_` | `DB_PASSWORD` | `DB_PASSWORD` | `SPRING_DATASOURCE_URL` | `DATABASE_URL` |
127
+
128
+ 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.
129
+
130
+ ## Precedence
131
+
132
+ 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.
133
+
134
+ ## Checking a configuration
135
+
136
+ ```bash
137
+ vault-env-loader check
138
+ ```
139
+
140
+ 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**.
141
+
142
+ ## Failures
143
+
144
+ 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`.
145
+
146
+ | Situation | Retried? |
147
+ |---|---|
148
+ | 401 / 403 — invalid, expired or revoked token, or the policy does not allow the path | no |
149
+ | 404 — path does not exist (skipped if `optional:`) | no |
150
+ | 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 |
151
+ | 3xx redirects | not followed (the token would go with them); fails |
152
+
153
+ A token that expires within 14 days produces a warning.
154
+
155
+ ## Things to know
156
+
157
+ - **Rotation needs a restart.** Values are read once, at start — just like editing `.env`. `check` shows each path's version so you can tell whether a restart is due.
158
+ - **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.
159
+ - **Reloaders** (`runserver`, `uvicorn --reload`) start children with the loaded environment and `VAULT_ENV_LOADED`, so they do not read again. Restart the outer command to pick up new values.
160
+ - **One value is limited to 128 KB** (the Linux limit for one environment string); larger values are refused with the key name.
161
+ - **No `.pth` auto-loading.** Loading on every interpreter start (including `pip`) would be invisible and fragile; the wrapper already needs no code change.
162
+ - **Windows** has no `exec`: the wrapper runs the command as a child and passes its exit code through.
163
+ - Only token authentication for now; AppRole and Kubernetes auth may follow.
164
+
165
+ ## Library API
166
+
167
+ ```python
168
+ from vault_env_loader import load, fetch, VaultEnvError
169
+
170
+ load(env=None, **options) # -> names set in os.environ (or in the mapping `env`)
171
+ fetch(env=None, **options) # -> {NAME: value}; does not touch the environment
172
+ ```
173
+
174
+ `options` mirror the variables above: `addr`, `token`, `token_file`, `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.
175
+
176
+ ## Security notes
177
+
178
+ - Secret values are never written to logs, error messages, argv or disk. Unexpected errors print only the exception type, never a traceback.
179
+ - Redirects are not followed, so the token is only ever sent to `VAULT_ADDR`.
180
+ - Requests carry `User-Agent: vault-env-loader/<version> python/<version>`, so server audit logs can tell which client read what.
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vault-env-loader"
7
+ dynamic = ["version"]
8
+ description = "Load HashiCorp Vault KV secrets into environment variables before your app starts. Zero dependencies."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Kaylov H" }]
13
+ requires-python = ">=3.6"
14
+ keywords = ["vault", "hashicorp", "secrets", "env", "environment", "dotenv", "kv", "config"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Intended Audience :: System Administrators",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Topic :: Security",
24
+ "Topic :: System :: Systems Administration",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/kaylovh/vault-env-loader/tree/main/python#readme"
29
+ Source = "https://github.com/kaylovh/vault-env-loader"
30
+ Issues = "https://github.com/kaylovh/vault-env-loader/issues"
31
+
32
+ [project.scripts]
33
+ vault-env-loader = "vault_env_loader:_cli"
34
+
35
+ [tool.setuptools]
36
+ py-modules = ["vault_env_loader"]
37
+
38
+ [tool.setuptools.dynamic]
39
+ version = { attr = "vault_env_loader.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+