External secret references
Keep target database passwords in HashiCorp Vault, AWS Secrets Manager or Azure Key Vault. QueryProxy resolves the reference when a connection opens.
By default a connection’s password is stored in QueryProxy, encrypted with the application key. If your policy says database passwords live in a secret manager, give the connection a reference instead of the value. QueryProxy reads the secret when it opens the connection, hands it to the database driver and forgets it. Only the reference is stored.
Three kinds of reference are supported:
| Store | Reference |
|---|---|
| HashiCorp Vault | vault://<mount>/<path>#<key> |
| AWS Secrets Manager | aws-sm://<secret-id>[#<json-key>] |
| Azure Key Vault | azure-kv://<vault-name>/<secret-name>[/<version>] |
For Vault, #<key> names the field inside the secret. For AWS, <secret-id> is
the secret’s name or ARN, and #<json-key> picks one key when the secret is a
JSON object; without it the whole secret string is the password. For Azure, the
version is optional and defaults to the current one.
vault://secret/teams/payments/orders-db#password
aws-sm://prod/payments/orders-db#password
azure-kv://payments-vault/orders-db-password
The password of a connection and its separate read password can each be a
reference. A plain password that happens to start with vault://, aws-sm://
or azure-kv:// is now treated as a reference.
Which team may use which references
References are bounded per team, and a team starts with none. Without that bound, anyone who may create a connection could point its host at a server of their own and have QueryProxy send it any secret that the Vault token can read.
A system administrator sets, for each team, the prefixes its connections may reference. Either use Admin → Teams (the Secret paths panel), or the command line:
php artisan queryproxy:team:secret-prefixes payments vault://secret/teams/payments
php artisan queryproxy:team:secret-prefixes payments # show the list
php artisan queryproxy:team:secret-prefixes payments --clear # allow none
The command takes the team’s id or slug. The prefixes you pass are the complete new list. Rules:
- A reference is allowed when it lies under a prefix, compared on whole path
segments:
vault://secret/teams/paydoes not covervault://secret/teams/payments. - A prefix has no wildcard, no
#key, no..or%, and at most 512 characters. A trailing/is dropped. - An AWS secret written as an ARN and the same secret written by name are two different addresses; list the form you use. Neither covers the other.
- A change is written to the audit log as
team.secret_prefixes_changed, with the list before and after. The panel warns when a prefix overlaps another team’s. - A team without prefixes cannot save a connection with a reference, and the connection form disables the external sources for it. Moving a connection to another team checks its reference again.
- Checks run when the connection is saved and again when the secret is resolved, before any cache is read, so another team’s cached value is never handed out.
In the connection form
Open Connections → New connection (or edit one). Credential source offers Stored password, HashiCorp Vault, AWS Secrets Manager and Azure Key Vault. For an external source the password boxes turn into a reference field that shows the expected format and a warning if what you typed does not look right. The warning is only a hint: the server decides. The team’s allowed paths are listed under the field.
The connection list has a Credentials column: the source and, for a reference, the reference itself, never a password. A reference that now falls outside the team’s prefixes is marked Outside team paths. Editing a connection shows its reference; a stored password is never shown.
Test resolves the reference for real. If anything fails, it reports a fixed message that names a reason and never carries the secret.
When resolution fails
Resolution fails closed: the request is marked failed with a generic
message, and no connection is attempted. The detail goes to the application log
(scheme, host or path, reason, HTTP status) and to the audit log as
secret.resolve_failed (the scheme and path, without #key, and the reason).
The value and the tokens used to fetch it are never written anywhere. The reasons
are invalid_reference, not_allowed, not_configured, auth_failed,
access_denied, not_found, key_missing, malformed_response and
unavailable.
Values are cached in the process memory of the web and worker processes, not
in the application cache. QUERYPROXY_SECRET_CACHE_TTL sets the lifetime
(default 300 seconds, 0 turns the cache off). After you rotate a password in
the store, connections pick it up within that time. Each process has its own
cache.
php artisan queryproxy:doctor has a secret_stores check. It makes no network
call: it fails when a store that a saved reference uses is not configured, or when
a saved reference is malformed or outside its team’s prefixes.
Settings
| Variable | Default | Meaning |
|---|---|---|
QUERYPROXY_SECRET_CACHE_TTL |
300 |
Seconds a resolved value stays in process memory; 0 turns the cache off |
QUERYPROXY_SECRET_TIMEOUT |
5 |
Seconds a store may take to connect and to answer |
HashiCorp Vault
| Variable | Default | Meaning |
|---|---|---|
QUERYPROXY_VAULT_ADDR |
none | Address of Vault, such as https://vault.example.com:8200 |
QUERYPROXY_VAULT_NAMESPACE |
none | Vault namespace, when you use them |
QUERYPROXY_VAULT_AUTH |
token |
token, approle or kubernetes |
QUERYPROXY_VAULT_TOKEN |
none | The token, for token |
QUERYPROXY_VAULT_ROLE_ID, QUERYPROXY_VAULT_SECRET_ID |
none | AppRole credentials |
QUERYPROXY_VAULT_APPROLE_MOUNT |
approle |
Mount of the AppRole method |
QUERYPROXY_VAULT_K8S_ROLE |
none | Vault role for the Kubernetes method |
QUERYPROXY_VAULT_K8S_MOUNT |
kubernetes |
Mount of the Kubernetes method |
QUERYPROXY_VAULT_K8S_JWT_PATH |
/var/run/secrets/kubernetes.io/serviceaccount/token |
Service account token to log in with |
QUERYPROXY_VAULT_KV_VERSION |
2 |
2 or 1 |
QUERYPROXY_VAULT_MOUNTS |
none | KV mount paths, comma-separated |
QUERYPROXY_VAULT_CA_FILE |
none | CA bundle for a Vault with a private certificate |
QUERYPROXY_VAULT_MOUNTS is required for KV v1, where nothing else tells a
KV mount from any other Vault API path. It is also needed for mounts whose name
has a slash, such as kv/prod; the longest matching mount is used. Once set it
is an allowlist: only those mounts are read. Whatever the setting, references
into auth, sys, identity and cubbyhole are refused, so a reference cannot
be used to read Vault’s own API paths.
AWS Secrets Manager
QueryProxy uses the AWS SDK’s default credential chain: environment variables, shared credential files, IRSA web identity, ECS task role and instance profile. There are no QueryProxy-specific keys.
| Variable | Default | Meaning |
|---|---|---|
QUERYPROXY_AWS_REGION |
none | Region. AWS_REGION and AWS_DEFAULT_REGION are used if this is not set; an ARN carries its own region |
Azure Key Vault
| Variable | Default | Meaning |
|---|---|---|
QUERYPROXY_AZURE_AUTH |
auto |
auto, client_secret, workload_identity or managed_identity. auto uses a client secret if one is set, else workload identity if a token file is set, else managed identity |
QUERYPROXY_AZURE_TENANT_ID, QUERYPROXY_AZURE_CLIENT_ID |
none | Tenant and application. AZURE_TENANT_ID and AZURE_CLIENT_ID are used if these are not set |
QUERYPROXY_AZURE_CLIENT_SECRET |
none | Client secret, for client_secret |
QUERYPROXY_AZURE_FEDERATED_TOKEN_FILE |
none | Token file for workload identity (AKS injects AZURE_FEDERATED_TOKEN_FILE) |
QUERYPROXY_AZURE_VAULT_DNS_SUFFIX |
vault.azure.net |
DNS suffix the vault host is built from; the host comes from the vault name and this suffix only, so a reference cannot steer a request elsewhere |
QUERYPROXY_VAULT_TOKEN, QUERYPROXY_VAULT_SECRET_ID and
QUERYPROXY_AZURE_CLIENT_SECRET can come from a file with the usual _FILE
suffix; see Secrets from files.
Next steps
- Kubernetes for passing these settings to the pods.
- Production hardening for the rest of the checklist.
- Audit chain and SIEM export for the
secret.*events.