# 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.

- Documentation: Guides
- Source: https://queryproxy.com/docs/external-secrets/
- Language: en-US
- Author: Muhammet Şafak

---
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.

```text
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:

```bash
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/pay` does not cover
  `vault://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](/docs/configuration/#secrets-from-files).

## Next steps

- [Kubernetes](/docs/kubernetes/) for passing these settings to the pods.
- [Production hardening](/docs/production-hardening/) for the rest of the checklist.
- [Audit chain and SIEM export](/docs/audit-export/) for the `secret.*` events.
