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.
- vault_env_loader-0.1.0/LICENSE +21 -0
- vault_env_loader-0.1.0/MANIFEST.in +1 -0
- vault_env_loader-0.1.0/PKG-INFO +208 -0
- vault_env_loader-0.1.0/README.md +184 -0
- vault_env_loader-0.1.0/pyproject.toml +39 -0
- vault_env_loader-0.1.0/setup.cfg +4 -0
- vault_env_loader-0.1.0/vault_env_loader.egg-info/PKG-INFO +208 -0
- vault_env_loader-0.1.0/vault_env_loader.egg-info/SOURCES.txt +10 -0
- vault_env_loader-0.1.0/vault_env_loader.egg-info/dependency_links.txt +1 -0
- vault_env_loader-0.1.0/vault_env_loader.egg-info/entry_points.txt +2 -0
- vault_env_loader-0.1.0/vault_env_loader.egg-info/top_level.txt +1 -0
- vault_env_loader-0.1.0/vault_env_loader.py +844 -0
|
@@ -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__" }
|