Skip to main content
A secret reference tells GoModel where to read a value instead of holding the value itself:
References work in any string setting of config.yaml, in the environment variables that set those settings, and in provider environment variables:
Single-quote the value in a shell so the shell does not expand it; .env files need no quoting. Docker Compose interpolates ${...} in its own files, so write $${file:...} there; Compose passes ${file:...} on to GoModel.

Syntax

A reference is ${<scheme>:<reference>}. It may sit inside a larger value, and one value may hold several:
The resolved value is used as-is. It may contain newlines, quotes, or any other bytes, so a service-account JSON file works, and it is never scanned for further references.

Schemes

file reads Docker and Kubernetes secret mounts directly, so no entrypoint script is needed to copy them into environment variables. The path must be a regular file or a symlink to one, as those mounts are; a FIFO or device is refused, because GoModel reads the file again on every reload. env differs from the plain ${NAME} form in two ways: it is strict, and it works in environment variables, not only in config.yaml. The plain form is unchanged: ${NAME} and ${NAME:-default} are expanded in config.yaml, and an unset ${NAME} is left in place, which drops a provider whose key it is. External secret managers (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) plug in as further schemes with GoModel Pro.

Failures stop startup

A reference that cannot be resolved stops the gateway with an error naming the setting and the scheme, never the value:
A provider API key set by an environment variable is reported under the variable’s name, for example OPENAI_API_KEY_2: secret reference ${file:...}. A scheme GoModel does not know is not repeated in the error, because it is text taken from the value; check the named setting for a typo. A failed reload keeps the running configuration. GoModel never starts with, or sends upstream, an unresolved reference. Each reference has 30 seconds to resolve. One that takes longer, such as a file on a stalled network mount, fails with context deadline exceeded instead of holding up startup, a reload, or a dashboard save. Only values in use are resolved: a provider value in config.yaml that an environment variable overrides, a provider environment variable that GoModel ignores (for example OPENAI_MODELS when several openai providers exist and none is named openai), or a provider skipped for missing credentials, is not looked up.

Rotation

GoModel does not poll env or file references. After rotating one, reload: gomodel --reload or SIGHUP. Extensions that watch a secret manager, such as GoModel Pro vaults, tell GoModel when a value changes. GoModel then re-resolves every reference and:
  • swaps provider API keys in place when only they changed. Requests in flight finish on the key they started with. Sessions pinned to a removed key move to another one; most other sessions keep their key.
  • reloads the configuration, as SIGHUP does, when anything else in config.yaml or the environment changed (a DSN, the master key, a proxy URL, a service-account JSON). A failed reload keeps the running configuration and is retried on the next change. References saved from the dashboard never reload the configuration; see Dashboard-managed credentials.
  • keeps every current value when a reference cannot be re-resolved, and logs a warning naming the setting.
The log line names the settings that changed, never their values:
Changing the key of the provider used by the semantic cache embedder also reloads, because the embedder keeps its own copy of the keys.

Dashboard-managed credentials

Provider credentials, MCP server headers, and guardrail secret fields saved from the dashboard or the admin API accept references too, in the same syntax:
  • The reference is stored as typed; GoModel resolves it when it builds the provider, MCP connection, or guardrail.
  • A reference is not a secret, so the dashboard and the admin API show a value made only of references unmasked; an MCP header may also start with an auth scheme (Bearer ${env:TOKEN}), and a proxy URL is shown when its password is a reference. Literal secrets, and values mixing literal text with a reference, stay masked, and sending the mask back keeps the stored value.
  • A reference that cannot be resolved is rejected on save with a 400 that names the field and the scheme.
  • Only the master key can add or change a reference. A reference resolves on the gateway host, so it can read any environment variable or file the gateway can, the master key included. Any other credential, such as a managed key with dashboard access, gets a 403 (secret_reference_requires_master_key) naming the field. It can still save literal secrets, send the mask back, and re-save an entity whose references are unchanged. Without a master key, dashboard credentials save references as before.
  • For the same reason, only the master key can change where an entity that holds a reference sends it: an MCP server’s url, a provider’s type, base_url, backend, or proxy_url, or a guardrail’s type or service URL (such as Presidio’s analyzer_url). Other credentials get the same 403 naming the field, and can still edit every other field.
  • Each resolver call carries a field path for auditing, such as provider_credentials.openai.api_keys[0], mcp_servers.github.headers.Authorization, or guardrail_definitions.pii.config.api_key.
  • When a referenced value rotates, only the affected entity is reinstalled: a provider’s keys are swapped in place, any other provider change rebuilds that provider, an MCP server is reconnected, and a guardrail is rebuilt. If the reference cannot be resolved, the entity keeps its current value.
Guardrail rules declared in config.yaml are stored the same way: a reference in a plugin’s secret field is saved as written and resolved when the guardrail is built, so the database never holds the secret. If a stored reference cannot be resolved at startup, the provider credential or MCP server is logged and skipped, and the rest of the gateway starts. On a reload, a provider credential or MCP server that is serving is never dropped this way, nor because a key now resolves to an empty value: the reload fails, naming the entity and field, and the running configuration, that entity included, keeps serving. Fix or delete the entity, then reload again. One that was already skipped is skipped again and does not block the reload. Guardrails load as a set and fail closed: startup fails, a reload keeps the running configuration, and a periodic refresh keeps the guardrails it has. Values saved before references existed are read the same way, so a stored literal containing ${scheme:...} is now treated as a reference. Re-save it with $${ to keep it literal. An extension can register a secret writer. A literal typed in the dashboard is then stored in the secret manager and only the reference is saved, and the writer’s secrets are deleted when their entity or field is. A save that still holds a written reference its entity no longer stores, such as a form opened before someone else replaced that secret, is rejected: reload and save again. Core GoModel ships no writer.

Literal ${

Write $${ for a literal ${. pa$${x} becomes pa${x}.

Limits

  • env and file references are resolved when the configuration is loaded or reloaded; see Rotation.
  • They apply to string settings. A boolean or number setting cannot be a reference, and an environment variable that GoModel parses as a number, boolean, duration, or list (LOGGING_ENABLED, HTTP_TIMEOUT, ENABLED_PASSTHROUGH_PROVIDERS, …) stops startup if it holds one. JSON variables accept references inside their string values.
  • Provider environment variables are the exception: OPENAI_MODELS, OPENAI_MODEL_FILTER_*, and OPENAI_SESSION_STICKY_KEYS are resolved before they are parsed.
  • In dashboard-managed entities, only the fields listed in Dashboard-managed credentials accept references.
Last modified on October 9, 2026