We wanted Aidbox to be fully dynamic — configurable at runtime, not just at deploy time. So we modeled Clients, IdentityProviders, TokenIntrospectors, and AidboxTopicDestinations as FHIR resources. You can create a new Kafka destination or register an identity provider through the standard FHIR API, no restart required.
However, FHIR resources get persisted in the database. And some of these resources carry secrets — client credentials, private keys, SASL configs for Kafka. Static secrets like database passwords or license keys live safely in environment variables and never touch the database. But a dynamically created Client or IdentityProvider? Its secret is part of the resource, and the resource is stored in the database.
We needed a way to have secrets inside FHIR resources without actually storing the secret values.
Starting with version 2602, Aidbox solves this with FHIR primitive extensions. A resource doesn't contain the secret value — it carries a data-absent-reason extension marked masked and a reference to a logical secret name. Aidbox resolves that name to a file on the filesystem at runtime, reads the value, and uses it. The actual secret never reaches the database or the API.
For the filesystem layer, we didn't want to build a dedicated connector for each vault — that's a maintenance burden that scales poorly and locks teams into specific providers. Instead, we chose the simplest possible interface: a file path. Many popular vaults — Azure Key Vault, HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager — already have Secrets Store CSI Driver providers that mount secrets as files in Kubernetes Pods. Others can be integrated through external tools or scripts that write secret values to files. Kubernetes Secrets, Docker Secrets, and plain bind-mounts work too. As long as the secret ends up in a file, Aidbox can use it — no plugins, no vendor SDKs, no custom integration code.
The mechanism is straightforward. There are three pieces:
BOX_VAULT_CONFIG environment variable.masked and includes a reference to the logical secret name. Aidbox resolves it at runtime.When Aidbox needs a secret — say, to authenticate a client — it reads the secret name from the resource, looks up the file path in the vault config, checks that the resource is allowed to access it (scope enforcement), reads the file, and uses the value. The secret value is never returned through the API: GET responses contain only the reference.
The vault config is a JSON file with a secret object. Each key is a logical name, and the value specifies where the file lives and who can use it:
{
"secret": {
"my-client-secret": {
"path": "/run/secrets/my-client-secret",
"scope": { "resource_type": "Client", "id": "my-client" }
},
"idp-private-key": {
"path": "/run/secrets/idp-key",
"scope": { "resource_type": "IdentityProvider" }
}
}
}Aidbox loads this file once at startup. If you change the vault config, you need to restart Aidbox. However, the secret files themselves are cached with a short TTL and validated against file modification time — so secret rotation works without restarts.
Resources use FHIR primitive extensions to reference secrets. Here's a Client with a vault-backed secret:
PUT /fhir/Client/my-client{
"resourceType": "Client",
"id": "my-client",
"_secret": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "masked"
},
{
"url": "http://health-samurai.io/fhir/secret-reference",
"valueString": "my-client-secret"
}
]
},
"grant_types": ["client_credentials", "basic"]
}Reading it back returns exactly the same structure — the extension with the secret name, never the value:
GET /fhir/Client/my-client{
"resourceType": "Client",
"id": "my-client",
"_secret": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "masked"
},
{
"url": "http://health-samurai.io/fhir/secret-reference",
"valueString": "my-client-secret"
}
]
},
"grant_types": ["client_credentials", "basic"]
}But authentication works as expected — Aidbox reads the secret from the mounted file and uses it to validate credentials:
# Works — Aidbox resolves the secret from the filesystem
curl -u my-client:the-actual-secret-value http://aidbox/fhir/Patient
# Fails — wrong password
curl -u my-client:wrong-password http://aidbox/fhir/PatientExternal secrets aren't limited to client credentials. Here's the full list of supported resources and fields:
This covers the most common cases: authentication credentials, identity provider keys, token verification, and messaging infrastructure secrets.
One of the key benefits of external secrets is rotation without downtime. The flow depends on how secrets are delivered to the filesystem:
In all cases, the important distinction is: the vault config file (pointed to by BOX_VAULT_CONFIG) is loaded once at startup and needs a restart to change. The secret files it points to are cached with a short TTL and validated against modification timestamps, so they rotate automatically.
We have a complete step-by-step tutorial that walks through a HashiCorp Vault deployment on Kubernetes from scratch. Here's the high-level flow:
Once deployed, secrets from HashiCorp Vault appear as files inside the Pod. Aidbox reads them at runtime — and when you rotate a secret in the vault, the CSI driver updates the file automatically with no restart needed.
The same pattern applies to other vaults with CSI providers (Azure Key Vault, AWS Secrets Manager, GCP Secret Manager). For the full vault config reference and extension format, see the External Secrets documentation.
External Secrets is available now in Aidbox 2602 and later. If you're running Aidbox on Kubernetes and managing secrets through a vault, this integration removes the need to store sensitive values in the database. Your vault keeps doing what it does best — rotation, auditing, access control — and Aidbox resolves secrets at runtime from the filesystem.
We'd love to hear how you're managing secrets in your FHIR deployments. What vaults are you using? What's been painful? Join Zulip to explore implementation details with our team.
Get in touch with us today!
