DEXUS VAULT
dexus-vault - synchronizer of Dex clients with secrets in Vault or AWS SSM
🚩 Table of Contents
- About the project
- Installation
- Usage
- Configuration
- Vault secret structure
- Pruning
- Metrics
- Running in Kubernetes
- Local testing
- Development
- Upgrading from 0.3
- Thanks
🚀 About the project
Dexus Vault keeps the OAuth2 clients of Dex in sync with secrets stored in HashiCorp Vault or in AWS Systems Manager Parameter Store. The secret backend is the source of truth: every secret under a path describes one Dex client.
How it works
Every SYNC_INTERVAL seconds dexus-vault:
- lists and reads the secrets under
VAULT_MOUNT_POINT/VAULT_CLIENTS_PATH(or the parameters underSSM_PATH), and validates each one as a client; - fetches each client from Dex over gRPC and compares them;
- applies the difference:
- missing clients are created;
- changed clients are updated in place with
UpdateClient; - clients are recreated (deleted and created again) only when Dex cannot update the change in place: a new
secret, a change ofpublic, or clearing a list/name/logo; - with pruning enabled, clients that are no longer in the backend are deleted.
Secrets that are invalid, or that define the same client id twice, are reported and their clients are left untouched.
💾 Installation
pip install dexus-vault # Vault backend
pip install 'dexus-vault[aws]' # adds the AWS SSM backend (boto3)
or with Docker:
docker run --env-file .env ifurs/dexus-vault
The image includes both backends. See the image README.
📙 Usage
dexus-vault # sync continuously
dexus-vault --once # one sync, exit code 1 if anything failed
dexus-vault --dry-run # log what would change, change nothing
dexus-vault --prune # also delete clients that are not in Vault
python3 -m dexus_vault # same as dexus-vault
All options:
--once run one sync and exit, non-zero on errors (SYNC_ONCE)
--dry-run log changes without applying them (SYNC_DRY_RUN)
--prune, --no-prune delete Dex clients missing in the source (SYNC_PRUNE)
--backend {vault,aws-ssm} where clients are read from (SECRETS_BACKEND)
--interval INTERVAL seconds between syncs (SYNC_INTERVAL)
--log-level LOG_LEVEL log level (LOG_LEVEL)
--log-format {text,json} log format (LOG_FORMAT)
--env-file ENV_FILE dotenv file to read (default: .env)
Exit codes: 0 success or clean shutdown (SIGTERM/SIGINT), 1 Dex or Vault unreachable or a failed --once sync, 2 invalid configuration.
🔧 Configuration
Settings are read from environment variables and from a .env file in the working directory (or --env-file). Command line flags win over both.
General and sync
| variable | default | description |
|---|---|---|
| SECRETS_BACKEND | vault | vault or aws-ssm |
| SYNC_INTERVAL | 60 | Seconds between syncs |
| SYNC_ONCE | false | Run one sync and exit |
| SYNC_DRY_RUN | false | Log planned changes without applying them |
| SYNC_PRUNE | false | Delete Dex clients that are not defined in the backend, see Pruning |
| SYNC_PRUNE_PREFIX | - | Only prune clients whose id starts with this prefix |
| SYNC_PRUNE_EXCLUDE | - | Comma-separated client ids that are never pruned |
| LOG_LEVEL | INFO | DEBUG, INFO, WARNING, ERROR or CRITICAL |
| LOG_FORMAT | text | text or json (one JSON object per line) |
Metrics and health
| variable | default | description |
|---|---|---|
| METRICS_ENABLE | true | Serve /metrics, /healthz and /readyz (not in --once mode) |
| METRICS_ADDR | 0.0.0.0 | Listen address |
| METRICS_PORT | 8000 | Listen port |
| INTERNAL_METRICS | false | Also export Python process, platform and GC metrics |
Dex connection
| variable | default | description |
|---|---|---|
| DEX_GRPC_URL | 127.0.0.1:5557 | Dex gRPC address |
| DEX_CA_CRT | - | CA that signed the Dex gRPC server certificate (enables TLS) |
| DEX_CLIENT_CRT | - | Client certificate for mutual TLS |
| DEX_CLIENT_KEY | - | Client certificate key for mutual TLS |
| DEX_TIMEOUT | 10 | Deadline in seconds for each gRPC call |
| DEX_MAX_RETRIES | 20 | Startup retries while Dex is unreachable |
| DEX_RETRY_WAIT | 3 | Seconds between startup retries |
CA_CRT, CLIENT_CRT and CLIENT_KEY are still accepted as aliases.
Without any certificate the connection is not encrypted and a warning is logged. Dex's gRPC API gives full administrative access, so configure grpc.tlsClientCA in Dex and use mutual TLS. Setting only DEX_CA_CRT gives server-authenticated TLS; without it the system CA store is used to verify Dex.
Dex version notes: dexus-vault works with any Dex v2 release, but allowed_connectors, sso_shared_with, backchannel_logout_uri, post_logout_redirect_uris and refresh_token_lifetime need Dex v2.46.0 (they are ignored, with a warning, on older servers), and pruning needs Dex v2.44.0 (ListClients).
Vault connection
| variable | required | default | description |
|---|---|---|---|
| VAULT_ADDR | - | http://127.0.0.1:8200 | Vault address |
| VAULT_MOUNT_POINT | yes | - | KV engine mount point |
| VAULT_CLIENTS_PATH | yes | - | Path inside the engine that holds one secret per client |
| VAULT_ENGINE | - | v2 | KV engine version, v1 or v2 |
| VAULT_NAMESPACE | - | - | Vault Enterprise / HCP namespace |
| VAULT_CERT_CA | - | true | TLS verification: true (system CA store), false, or a path to a CA bundle |
| VAULT_REQUEST_TIMEOUT | - | 10 | Seconds per Vault request |
| VAULT_ALLOW_REDIRECT | - | false | Follow HTTP redirects |
| VAULT_PROXIES | - | - | JSON, e.g. {"https": "http://proxy:3128"} |
| VAULT_MAX_RETRIES | - | 20 | Startup retries while Vault is unreachable or sealed |
| VAULT_RETRY_WAIT | - | 3 | Seconds between startup retries |
| VAULT_AUTH_METHOD | - | auto | auto, token, approle, kubernetes, ldap or cert |
Vault auth
With VAULT_AUTH_METHOD=auto the first configured method in this order is used: AppRole, Kubernetes, LDAP, certificate, token.
| method | variables |
|---|---|
| Token | VAULT_TOKEN |
| AppRole | VAULT_APPROLE_ROLE_ID and either VAULT_APPROLE_SECRET_ID or VAULT_APPROLE_SECRET_PATH (file with the secret id, e.g. written by Vault Agent); VAULT_APPROLE_MOUNT_POINT (default approle) |
| Kubernetes | VAULT_KUBERNETES_ROLE; VAULT_KUBERNETES_JWT_PATH (default /var/run/secrets/kubernetes.io/serviceaccount/token); VAULT_KUBERNETES_MOUNT_POINT (default kubernetes) |
| LDAP | VAULT_LDAP_USERNAME, VAULT_LDAP_PASSWORD; VAULT_LDAP_MOUNT_POINT (default ldap) |
| Certificate | VAULT_CERT, VAULT_CERT_KEY; VAULT_CERT_ROLE (optional role name); VAULT_CERT_MOUNT_POINT (default cert). Requires TLS verification |
Tokens obtained by logging in (all methods except token) are renewed before they expire, replaced by a new login when renewal is not possible, and revoked on shutdown.
Vault policy
# KV v2 mounted at "kv", clients under "dex/"
path "kv/metadata/dex/*" {
capabilities = ["list"]
}
path "kv/data/dex/*" {
capabilities = ["read"]
}
For KV v1 grant list and read on kv/dex/*.
AWS SSM Parameter Store
Set SECRETS_BACKEND=aws-ssm (or --backend aws-ssm) and install the aws extra. Every parameter directly under SSM_PATH is one client; its value is the same JSON object as a Vault secret, and the parameter name below the path is the default client id. Use SecureString parameters, they are decrypted on read.
| variable | required | default | description |
|---|---|---|---|
| SSM_PATH | yes | - | Parameter path, e.g. /dex/clients |
| AWS_REGION | - | from the AWS config | Region of the parameters |
| AWS_PROFILE | - | - | Shared config / SSO profile |
| SSM_ENDPOINT_URL | - | - | Custom endpoint, e.g. a VPC endpoint or a local emulator |
| SSM_TIMEOUT | - | 10 | Connect and read timeout in seconds |
| SSM_MAX_RETRIES | - | 5 | Attempts per request, throttling is retried |
| AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN | - | - | Static credentials, normally not needed |
Credentials come from the standard AWS chain: environment variables, shared config and SSO profiles, web identity (EKS IRSA), EKS Pod Identity, or the ECS/EC2 instance role. The explicit AWS_* key settings exist so credentials can also come from a .env file.
aws ssm put-parameter --type SecureString --name /dex/clients/my-app \
--value '{"secret": "a-long-random-secret", "redirect_uris": ["https://app.example.com/callback"]}'
Minimal IAM policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ssm:GetParametersByPath",
"Resource": "arn:aws:ssm:eu-west-1:123456789012:parameter/dex/clients"
}
]
}
The default aws/ssm KMS key lets every principal in the account decrypt. With a customer managed key, also allow kms:Decrypt on that key. Parameters whose value is not a JSON object are reported, and pruning is skipped until they are fixed. Nested parameters (/dex/clients/team/app) are ignored.
🔒 Vault secret structure
Each secret under VAULT_CLIENTS_PATH (or parameter under SSM_PATH) is one client. All fields of the Dex client API are supported:
{
"id": "my-first-client",
"secret": "a-long-random-secret",
"name": "My First Client",
"logo_url": "https://example.com/logo.png",
"public": false,
"redirect_uris": ["https://app.example.com/callback"],
"trusted_peers": ["my-second-client"],
"allowed_connectors": ["github"],
"sso_shared_with": ["my-second-client"],
"backchannel_logout_uri": "https://app.example.com/backchannel-logout",
"post_logout_redirect_uris": ["https://app.example.com/"],
"refresh_token_lifetime": "standalone"
}
iddefaults to the secret name.secretis required for confidential clients. Public clients ("public": true) may leave it out, Dex then keeps managing it.refresh_token_lifetimeisstandalone(default) orsession.- Lists can be native JSON lists, JSON-encoded strings (
"[\"a\", \"b\"]") or comma-separated strings ("a,b"), so they work with the key/value view of the Vault UI too. The same goes for"public": "true". - camelCase keys (
redirectUris) are accepted. Unknown keys are ignored with a warning, which catches typos such asredirect_uri. - Sub-paths (folders) under
VAULT_CLIENTS_PATHare skipped. Soft-deleted KV v2 secrets count as removed.
✂️ Pruning
With SYNC_PRUNE=true (or --prune) clients that exist in Dex but not in the backend are deleted. Dex clients carry no owner label, so limit the scope:
SYNC_PRUNE_PREFIX=vault-only touches clients whose id starts withvault-;SYNC_PRUNE_EXCLUDE=admin-ui,clinever touches the listed ids.
Safety rules, a sync never prunes when:
- some secret could not be read or some client could not be checked in Dex, since the picture is incomplete;
- the backend returned no clients at all (for example a wrong
VAULT_CLIENTS_PATHorSSM_PATH); - the client belongs to an invalid or duplicated secret.
Static clients from the Dex config file cannot be deleted through the API; the first attempt is logged and they are skipped afterwards. Try --dry-run --prune --once first to see what would be deleted.
📈 Metrics
Served at http://<METRICS_ADDR>:<METRICS_PORT>/metrics:
| metric | type | description |
|---|---|---|
dexus_vault_client_operations_total{operation, status} |
counter | operation: create, update, recreate, delete; status: ok, failed, planned (dry-run) |
dexus_vault_syncs_total{result} |
counter | Sync runs, result: success or failure |
dexus_vault_sync_duration_seconds |
histogram | Duration of a sync run |
dexus_vault_last_sync_timestamp_seconds |
gauge | Time of the last sync run |
dexus_vault_last_success_timestamp_seconds |
gauge | Time of the last sync without errors |
dexus_vault_clients |
gauge | Valid clients defined in the backend |
dexus_vault_invalid_secrets |
gauge | Secrets that are not valid client definitions |
dexus_vault_build_info{version, backend} |
gauge | Build information and the active backend |
Example alert: time() - dexus_vault_last_success_timestamp_seconds > 600.
Health endpoints on the same port:
/healthz: 200 while the process runs (liveness);/readyz: 200 once a sync succeeded and successes keep coming within3 × SYNC_INTERVAL + 60s, 503 otherwise (readiness).
☸️ Running in Kubernetes
containers:
- name: dexus-vault
image: ifurs/dexus-vault:0.4.0
env:
- { name: DEX_GRPC_URL, value: "dex.dex.svc:5557" }
- { name: VAULT_ADDR, value: "https://vault.vault.svc:8200" }
- { name: VAULT_MOUNT_POINT, value: "kv" }
- { name: VAULT_CLIENTS_PATH, value: "dex" }
- { name: VAULT_KUBERNETES_ROLE, value: "dexus-vault" }
- { name: LOG_FORMAT, value: "json" }
ports:
- { name: http, containerPort: 8000 }
livenessProbe:
httpGet: { path: /healthz, port: http }
readinessProbe:
httpGet: { path: /readyz, port: http }
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
For a CronJob, add args: ["--once"], the pod then fails when the sync fails.
On EKS with the SSM backend, replace the Vault variables with SECRETS_BACKEND=aws-ssm, SSM_PATH and AWS_REGION, and give the service account an IAM role (IRSA or Pod Identity) with the policy above.
💻 Local testing
docker/tests has a Docker Compose stack with Dex, a Vault dev server and an AWS SSM emulator (moto), sample clients and a restricted Vault policy:
docker/tests/start.sh up
dexus-vault --env-file docker/tests/.env # Vault
dexus-vault --env-file docker/tests/.env.ssm # AWS SSM
docker/tests/start.sh down
It is not suitable for production.
🛠 Development
pip install -r requirements-dev.txt -e ".[aws]"
pytest # unit tests
docker/tests/start.sh up && DEXUS_INTEGRATION=1 pytest tests/integration
black . && ruff check .
The project uses pre-commit.
For Claude Code, CLAUDE.md holds the project rules, and .claude/ defines project agents and skills:
| agent | model | role |
|---|---|---|
dexus-researcher |
opus | read-only; verifies upstream Dex, Vault/hvac, AWS and dependency behavior against tagged sources, keeps verified facts in project memory |
dexus-developer |
opus | implements changes test-first along the existing seams; a Stop hook blocks it from finishing while lint or unit tests fail |
dexus-tester |
sonnet | runs unit and integration suites against local endpoints only, reproduces bugs as failing tests; may edit test code only |
dexus-reviewer |
opus | independent review of a diff in a fresh context, re-runs the checks, reports only correctness, security and requirement gaps |
| skill | use |
|---|---|
dex-api |
verified Dex RPC semantics and version matrix, preloaded into the agents |
dexus-verify |
the verification gate: which checks to run for which change, and how to report evidence |
/upgrade-dex-api <tag> |
step-by-step upgrade of the vendored Dex API |
The hooks used by the agents are in scripts/agents/guard.py, tested in tests/test_agent_guard.py.
The Dex gRPC stubs in dexus_vault/grpc_dexidp are generated from api.proto. To update them to another Dex release:
scripts/generate_proto.sh v2.46.0
⬆️ Upgrading from 0.3
- Python 3.11 or newer is required.
VAULT_MOUNT_POINTandVAULT_CLIENTS_PATHare now validated as required, as documented before.- Metrics were renamed and are now counters with a
dexus_vault_prefix, see Metrics. The oldclient_create,client_delete,client_update,client_skipandincorrect_secretsgauges are gone. - Confidential clients with an empty
secretare rejected. Before, Dex replaced the empty secret with a random one, and dexus-vault recreated the client on every sync. - Vault TLS is verified by default for every auth method (
VAULT_CERT_CA=true). SetVAULT_CERT_CA=falseto turn it off, or point it at your CA bundle. - Changes are applied in place with
UpdateClientwhere possible, so clients are no longer deleted and recreated for every change. - The Docker image runs as an unprivileged user (uid 65532).
Roadmap
- Redesign metrics concept to make it more Prometheus friendly
- Switch to pydantic
- Track clients in Dex and prune the ones removed from Vault
- CLI with parameters
- Structured (JSON) logs
- Generate missing client secrets and write them back to Vault
- Read clients from several Vault paths, recursively
- AWS SSM Parameter Store backend
- AWS Secrets Manager and other secret backends
🔥 Thanks
- Hurlenko for references copied from your repos
Metadata
Release files for dexus-vault 0.4.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 | |
|---|---|---|---|---|
| dexus_vault-0.4.0.tar.gz | 53.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dexus_vault-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 96.0 kB
Release files / dexus_vault-0.4.0.tar.gz
| Download URL | dexus_vault-0.4.0.tar.gz |
|---|---|
| Size | 53.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
358d55ea9ba495304d9c9a796339dab8893c57c31f4f696ef619776bc18ba81f
|
|
BLAKE2b-256 checksum How to use checksums |
9ea320204edde13ddfb914630a1f6f46d6e52074c4d64c53895a0de6f0274454
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 21, 2026.
Transparency logRelease files / dexus_vault-0.4.0-py3-none-any.whl
| Download URL | dexus_vault-0.4.0-py3-none-any.whl |
|---|---|
| Size | 42.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
caac5a7af8551cbc6ef90edf380a26c0103560a9d59535be0ab85b85d1bf43a6
|
|
BLAKE2b-256 checksum How to use checksums |
466af7c8567ee5660c3faaaff3a6bbadc5cda1c9a6454cf4004def74732dc874
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 21, 2026.
Transparency logSecurity for dexus-vault 0.4.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 dexus_vault-0.4.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 21, 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.