FHIR Extensions as a Bridge to External Secret Management

Andrey Listopadov, Ivan Bagrov
March 9, 2026
15 minutes

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.

How it works

The mechanism is straightforward. There are three pieces:

  1. Secret files on the filesystem. Your vault or orchestrator places secret values as files inside the Aidbox Pod. This is the only contract — a file at a known path.
  2. A vault config file. A JSON file maps logical secret names to filesystem paths and declares which resources are allowed to use each secret. You point Aidbox to this file with the BOX_VAULT_CONFIG environment variable.
  3. FHIR extensions on resources. Instead of storing a secret value directly, a resource marks the field as 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

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" }
    }
  }
}

The scope field controls access. You can restrict a secret to a specific resource type, or narrow it further to a specific resource ID. If a resource that isn't in scope tries to resolve a secret, Aidbox returns an unauthorized error.

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.

Referencing secrets in resources

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/Patient

What resources support external secrets

External secrets aren't limited to client credentials. Here's the full list of supported resources and fields:

Resource Field Use case
Client secret Client authentication
IdentityProvider client.secret Symmetric authentication
IdentityProvider client.private-key Asymmetric key material
IdentityProvider client.certificate Certificate for asymmetric auth
TokenIntrospector jwt.secret JWT verification key
TokenIntrospector jwt.keys.k Symmetric validation key
TokenIntrospector introspection_endpoint.authorization Bearer token / auth header
AidboxTopicDestination parameter.saslJaasConfig Kafka SASL configuration
AidboxTopicDestination parameter.sslKeystoreKey Kafka SSL key

This covers the most common cases: authentication credentials, identity provider keys, token verification, and messaging infrastructure secrets.

Secret rotation

One of the key benefits of external secrets is rotation without downtime. The flow depends on how secrets are delivered to the filesystem:

  • Secrets Store CSI Driver: The CSI driver polls the vault for changes. When a secret is updated in Azure Key Vault (or another vault), the driver writes the new value to the mounted file. Aidbox detects the file modification and picks up the new value on next access — no restart required.
  • Kubernetes Secrets: When the Secret object is updated, Kubernetes propagates the change to mounted volumes. Aidbox sees the updated file and invalidates its cache.
  • Docker Secrets / bind-mounts: Update the file on the host. Aidbox will pick up the change based on file modification time.

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.

Deploying with HashiCorp Vault

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:

1. Deploy HashiCorp Vault and store your secrets.

2. Install the Secrets Store CSI Driver and the Vault provider on your cluster.

3. Configure a Vault role and policy granting read access to the secrets your Aidbox resources need.

4. Create a SecretProviderClass that tells the CSI driver which secrets to fetch from Vault.

5. Create a vault config as a ConfigMap, mapping logical secret names to the mounted file paths.

6. Deploy Aidbox with two volume mounts: the CSI volume for secrets and the ConfigMap for the vault config. Set BOX_VAULT_CONFIG to point to the config file.

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.

What's next

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.

contact us

Get in touch with us today!

By submitting the form you agree to Privacy Policy and Cookie Policy.
Thank you!
We’ll be in touch soon.

In the meantime, you can:
Oops! Something went wrong while submitting the form.

Never miss a thing
Subscribe for more content!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By clicking “Subscribe” you agree to Health Samurai Privacy Policy and consent to Health Samurai using your contact data for newsletter purposes