config-to-vault
Replicate a folder of config files from git into HashiCorp Vault KV v2, one secret per file.
Config usually lives in git as Spring-style YAML or .properties files. Some consumers can only read Vault. One example is NiFi's HashiCorp Vault parameter provider, which reads one secret per parameter context. config-to-vault resolves each file for a profile, flattens it to key/value pairs and writes it to <base>/<profile>/<name>.
- Dry run by default.
--applywrites. - Unchanged secrets are not rewritten. The plan lists changed key names, never values (unless
--show-values). --pruneremoves secrets whose file was deleted.
flowchart LR
subgraph git["Config in git"]
files["orders.yml<br/>billing.properties<br/>application.yml"]
end
cs["Spring Cloud<br/>Config Server<br/>(optional)"]
resolve["Resolve for --profile<br/>merge, flatten to keys"]
plan["Plan against Vault<br/>create / update / unchanged / prune"]
vault[("Vault KV v2<br/>base/profile/name")]
files --> resolve
files -. "names" .-> cs -. "resolved" .-> plan
resolve --> plan
plan -- "--apply" --> vault
Install
uvx config-to-vault --help # run without installing
uv tool install config-to-vault # or: pip install config-to-vault
docker run --rm ghcr.io/arogan178/config-to-vault --help
Python 3.11 or newer.
Quick start
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=... # see "Vault authentication"
# Plan only
config-to-vault --source-dir examples/config --shared-base application \
--profile prod --vault-base secret/my-app
# Write
config-to-vault --source-dir examples/config --shared-base application \
--profile prod --vault-base secret/my-app --apply
Example output:
Mode: dry run (no changes written, use --apply)
created billing -> secret/my-app/prod/billing
4 keys
updated orders-service -> secret/my-app/prod/orders-service
changed: orders.retries
Summary: 1 created, 1 updated, 2 unchanged, 0 failed
The exit code is 1 if any file fails, 2 for usage or configuration errors. Before reading anything, the target mount is checked to be KV v2, so a KV v1 mount is refused instead of getting data/ folders.
Sources
Choose where files come from.
- Local folder (default):
--source-dir PATH. Use this in CI after checking the repo out, with any git host. Reads*.yml,*.yaml,*.propertiesand*.jsonrecursively. The secret name is the file name without extension, so names must be unique. - Config Server:
--config-server 'https://cs.example/{name}/{profile}'. Each name is resolved by a Spring Cloud Config Server instead of locally. SetCONFIG_SERVER_TOKENfor a bearer token. Names come from--source-diror--bitbucket-url. - Bitbucket Cloud listing:
--bitbucket-url https://bitbucket.org/<ws>/<repo>/src/<branch>/<path>. Lists the YAML file names in that folder through the API (BITBUCKET_EMAILandBITBUCKET_API_TOKEN) and needs--config-serverto resolve them.
Local resolution rules
- YAML files may hold several documents, and
.propertiesfiles may be split with#---or!---lines. As in Spring Boot 2.4+, documents apply in file order and later ones win. A document withspring.config.activate.on-profile(or legacyspring.profiles) applies only when it matches--profile. The value can be a comma-separated list or a YAML list of profile expressions (prod,!dev,prod | eu,(prod | eu) & !dev). - Nested YAML is flattened to dotted keys (
a.b.c, lists asa[0]). Scalars keep their type.nulland empty lists become"". .propertiesfiles followjava.util.Propertiesrules, including\escapes and line continuations.spring.profile*andspring.config.activate*keys are removed from the output.- With
--shared-base application,application.yml(or.yaml,.properties,.json) in the root of--source-diris merged first and not written as a secret itself. - A list set by a later document (or by the file over the shared base) replaces the earlier list as a whole, as Spring's binder does.
Config Server merge
The Config Server lists propertySources highest precedence first, so earlier sources win. Lists are replaced as a whole and profile keys are removed. A response without property sources fails that file instead of emptying the secret.
Flags
| Flag | Meaning |
|---|---|
--profile |
Required. Profile to resolve and the {profile} path value. |
--vault-base |
Required. Base path. The first segment is the KV v2 mount. |
--vault-path-template |
Default {base}/{profile}/{name}. Must contain {name}; only {base}, {profile} and {name} are allowed. |
--source-dir |
Folder with config files. Default: current directory. |
--shared-base NAME |
Shared base file merged first (local resolution only). |
--config-server URL_TEMPLATE |
Resolve through a Config Server. URL contains {name} and optionally {profile}. |
--bitbucket-url |
List names from Bitbucket Cloud. Needs --config-server. |
--apply |
Write to Vault. |
--prune |
Delete secrets in the target folder that have no source file. Needs a template ending in /{name}, with {name} used once. |
--show-values |
Print values in the plan. Off by default. |
--workers |
Thread pool size. Default 5. |
Environment
| Variable | Meaning |
|---|---|
VAULT_ADDR |
Required. |
VAULT_TOKEN |
Token auth. |
VAULT_ROLE_ID, VAULT_SECRET_ID |
AppRole auth (mount approle). |
VAULT_K8S_ROLE, VAULT_K8S_MOUNT |
Kubernetes auth. Mount defaults to kubernetes. |
VAULT_NAMESPACE |
Sent as X-Vault-Namespace. |
CONFIG_SERVER_TOKEN |
Bearer token for the Config Server. |
BITBUCKET_EMAIL, BITBUCKET_API_TOKEN |
Bitbucket Cloud API credentials. |
Auth order: token, then AppRole, then Kubernetes. A dry run still needs read access to Vault to compute the plan.
Vault policy
Least privilege for --vault-base secret/my-app and profile prod. Replace the mount and path with yours. KV v2 policies use data/ and metadata/ paths.
# Dry run and --apply
path "secret/data/my-app/prod/*" {
capabilities = ["read", "create", "update"]
}
# Only for --prune
path "secret/metadata/my-app/prod" {
capabilities = ["list"]
}
path "secret/metadata/my-app/prod/*" {
capabilities = ["delete"]
}
--prune deletes the secret metadata, which removes all versions.
CI
GitHub Actions
name: config-to-vault
on:
push:
branches: [main]
paths: ["config/**"]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv tool install . # or install from your package index
- run: config-to-vault --source-dir config --profile prod --vault-base secret/my-app --apply
env:
VAULT_ADDR: ${{ vars.VAULT_ADDR }}
VAULT_ROLE_ID: ${{ secrets.VAULT_ROLE_ID }}
VAULT_SECRET_ID: ${{ secrets.VAULT_SECRET_ID }}
Bitbucket Pipelines
pipelines:
branches:
main:
- step:
image: python:3.12-slim
script:
- pip install config-to-vault
- config-to-vault --source-dir config --profile prod --vault-base secret/my-app --apply
# Set VAULT_ADDR, VAULT_ROLE_ID and VAULT_SECRET_ID as secured repository variables.
Run without --apply on pull requests to review the plan.
Development
uv sync
uv run ruff check && uv run ruff format --check
uv run pytest
Integration test against a Vault dev server:
docker run -d --name ctv-vault -p 18200:8200 -e VAULT_DEV_ROOT_TOKEN_ID=root hashicorp/vault
CONFIG_TO_VAULT_VAULT=1 uv run pytest
docker rm -f ctv-vault
License
Apache-2.0
Metadata
Release files for config-to-vault 0.1.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 | Provenance | |
|---|---|---|---|---|
| config_to_vault-0.1.0.tar.gz | 46.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| config_to_vault-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 65.6 kB
Release files / config_to_vault-0.1.0.tar.gz
| Download URL | config_to_vault-0.1.0.tar.gz |
|---|---|
| Size | 46.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0c30a7a79400d5e6332c9830ea9069f6193e5a78bc60a53b4c13a05e906d7cb
|
|
BLAKE2b-256 checksum How to use checksums |
c043bc0cc7578a4730a2c2bf0f0f31e04dcf7ad3c842ed09dbc159c8ec2deb5c
|
| 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 19, 2026.
Transparency logRelease files / config_to_vault-0.1.0-py3-none-any.whl
| Download URL | config_to_vault-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3e8f632fdfed362bc6a58d3be0721cb60573fbdf82b5e3bae2b249a3f82c6ab3
|
|
BLAKE2b-256 checksum How to use checksums |
b81cfd02f3a800a9a85d82ead6139d105148a208bd2e177a6574a69b6a3a76aa
|
| 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 19, 2026.
Transparency logSecurity for config-to-vault 0.1.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for config_to_vault-0.1.0.tar.gz
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 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.