Skip to main content

Config reference

A config controls how the CLI and haltsql-server run: where to listen, TLS, who may call (static tokens, OIDC, mTLS), which policies to load (files, inline, signed bundles, or signed bundles pulled from a registry, S3 or HTTPS), audit logging (stdout, files, S3, GCS, Elasticsearch/OpenSearch, Kafka), an optional Open Policy Agent check, the admin API, metrics and OpenTelemetry, and where secrets come from. It is a YAML or JSON document with kind: Config, loaded with the same rules as a policy (see policy-reference.md): unknown and duplicate keys are errors, and errors are located by file, line, and key path.

A config does not change what SQL is allowed. That is the policy's job.

Example​

apiVersion: haltsql.dev/v1
kind: Config
server:
listen: 127.0.0.1:8080 # pod-local; TLS is terminated in front (see below)
auth:
allow_unauthenticated: false
tokens:
- name: analytics-agent
token_sha256: ${env:HALTSQL_ANALYTICS_TOKEN_HASH}
policy: analytics-agent # each token is pinned to one policy
- name: support-bot
token_sha256: ${file:/run/secrets/support-bot.sha256}
policy: support-bot
policies:
- path: ./policies/analytics-agent.yaml
- path: ./policies/support-bot.json # formats can be mixed
default_policy: null
reload:
watch: true
audit:
sinks:
- type: file
path: /var/log/haltsql/audit.jsonl
log_raw_sql: false
telemetry:
metrics:
prometheus: true
logging:
level: info
format: json
limits:
max_request_bytes: 1048576

Top-level keys: apiVersion (haltsql.dev/v1), kind (Config), server, policies, default_policy, reload, audit, telemetry, logging, limits, secrets, opa. Any other key is an error.

A larger example using the enterprise features:

apiVersion: haltsql.dev/v1
kind: Config
server:
listen: 0.0.0.0:8443
tls:
cert: /etc/haltsql/tls/server.crt
key: /etc/haltsql/tls/server.key
client_ca: /etc/haltsql/tls/clients-ca.pem
client_auth: optional # mTLS identities below, OIDC for everyone else
min_version: "1.3"
auth:
oidc:
- name: corp-okta
issuer: https://corp.okta.com/oauth2/default
audiences: [haltsql]
policy_claim: haltsql_policy
allowed_policies: [saas-analytics, support-bot]
context_claims:
tenant_id: https://corp.example/tenant
user_id: sub
required_claims:
scope: sql.verify
mtls:
identities:
- spiffe_id: spiffe://corp.example/ns/agents/sa/analytics
policy: saas-analytics
context: { tenant_id: acme }
policies:
- path: ./policies/support-bot.yaml
- bundle: ./bundles/prod.haltsqlbundle
public_keys: [./keys/release.pub]
audit:
sinks:
- type: s3
bucket: corp-haltsql-audit
region: eu-west-1
object_lock: { mode: COMPLIANCE, retain_days: 365 }
credentials: { source: web_identity }
telemetry:
otlp:
endpoint: http://otel-collector:4318
headers:
authorization: "Bearer ${vault:secret/data/haltsql/otel#token}"
secrets:
vault:
address: https://vault.corp.example:8200
auth: { method: kubernetes, role: haltsql }

Keys​

server​

KeyTypeDefaultMeaning
server.listenstring host:port127.0.0.1:8080Address the HTTP server binds to.
server.tlsmappingnoneBuilt-in TLS (see server.tls). Without it, terminate TLS in front of the server (Envoy, nginx, or a service mesh) and bind to 127.0.0.1 or a pod-local address. See authentication.md.
server.auth.tokenslist[]Bearer tokens (see below).
server.auth.oidclist[]OpenID Connect providers whose JWTs are accepted (see server.auth.oidc).
server.auth.mtlsmappingnoneClient-certificate identities (see server.auth.mtls).
server.auth.allow_unauthenticatedbooleanfalseAccept requests without a token. For local development only.
server.adminmappingdisabledAdmin API (see server.admin).

Each token entry:

KeyTypeMeaning
namestringLabel used in logs and audit events. Never the secret. Must be unique.
token_sha256hex stringSHA-256 of the bearer token as 64 lower-case hex characters. The token itself is never stored. Must be unique.
policystringName (metadata.name) of the one policy this token may use.
contextmappingOptional. Context values bound to this token (for example tenant_id: acme). The request body cannot supply or override them.
request_context_fieldslistOptional. Context fields the request body may still supply. Setting this or context binds the token: any other body field gets 403 CONTEXT_NOT_ALLOWED. Without either key the body supplies the whole context (see authentication.md).

A token can only use its pinned policy, so a compromised agent cannot pick a looser one. Generate a token and its hash with, for example:

token="$(openssl rand -hex 32)"
printf '%s' "$token" | sha256sum | cut -d' ' -f1 # goes in token_sha256

server.tls​

KeyTypeDefaultMeaning
certpath or PEMnoneServer certificate chain. Required together with key.
keypath or PEMnonePrivate key. Required together with cert. Always shown as <redacted> by config show.
client_capath or PEMnoneCA bundle used to verify client certificates (enables mTLS).
client_authnone, optional, requirednoneWhether client certificates are requested. optional and required need client_ca.
min_version"1.2", "1.3""1.2"Minimum TLS version.

Relative paths are resolved against the config file's directory. A value that starts with -----BEGIN (for example from ${file:...} or a secret provider) is used as inline PEM. Setting client_ca, client_auth or min_version without cert and key is an error.

server.auth.oidc​

Each entry describes one OpenID Connect provider. A request whose bearer token is a JWT signed by the provider is authenticated by it.

KeyTypeDefaultMeaning
namestringrequiredUnique provider name, used in logs and audit events.
issuerURLrequiredMust equal the token's iss. https only (no credentials, query or fragment); http only with allow_insecure_http: true.
audienceslistrequiredNon-empty; the token's aud must contain one of them.
jwks_uriURLdiscoveredDefault: jwks_uri from <issuer>/.well-known/openid-configuration.
algorithmslist[RS256, ES256]Subset of RS256 RS384 RS512 PS256 ES256 ES384 EdDSA. none and HS* are rejected.
leeway_secondsinteger 0-30060Clock skew allowed for exp, nbf, iat.
jwks_cache_secondsinteger 10-86400300How long fetched keys are cached.
policystringnonePin every token from this provider to one policy.
policy_claimclaim pathnoneInstead of policy: the claim whose value names the policy. Needs allowed_policies.
allowed_policieslistnoneThe only policies policy_claim may select (non-empty).
policy_from_groupsmappingnoneInstead of policy / policy_claim: pick the policy from the token's groups (see policy_from_groups).
context_claimsmap{}Context field -> claim path. The path is first tried as one claim name (so https://corp.example/tenant works), then as a dot path (org.id).
required_claimsmap{}Claim -> string or list. A string must equal the claim, or be contained in it when the claim is an array or a space-separated string such as scope. A list requires every value.
request_context_fieldslist[]Context fields the request body may still supply. They must not also appear in context_claims.
allow_insecure_httpbooleanfalseAllow http issuer and JWKS URLs. For tests only.

Exactly one of policy, policy_claim or policy_from_groups is required. Every referenced policy must be loaded (see policies for bundles).

policy_from_groups​

Maps IdP groups (or roles) to policies, so one provider can serve several kinds of caller without a custom claim:

server:
auth:
oidc:
- name: corp-keycloak
issuer: https://sso.corp.example/realms/corp
audiences: [haltsql]
policy_from_groups:
claim: groups # default; dotted paths like realm_access.roles work
map: # ordered: the first entry the token matches wins
- { group: data-platform-admins, policy: admin-readonly }
- { group: analysts, policy: saas-analytics }
- { group: support, policy: support-bot }
default: null # no match: the token is rejected (401)
KeyTypeDefaultMeaning
claimclaim pathgroupsClaim holding the groups. Looked up like context_claims paths: first as one claim name, then as a dot path (realm_access.roles).
maplistrequiredNon-empty, ordered list of {group, policy}. Groups are compared exactly (case-sensitive) and must be unique.
defaultstringnonePolicy for a token that matches no entry. Without it such a token is rejected.

The claim may be a string (one group; it is never split on spaces) or an array of strings. A missing claim, or a claim of any other shape (a number, an object, an array containing a non-string), rejects the token. The map is walked in order and the first entry whose group the token has wins, so put the most privileged or most specific groups first.

policy_from_groups cannot be combined with policy, policy_claim or allowed_policies. Every policy in map and default must resolve, with the same rule as other references (bundles with name_prefix, see policies).

server.auth.mtls​

server:
auth:
mtls:
identities:
- spiffe_id: spiffe://corp.example/ns/agents/sa/analytics # exact URI SAN
policy: saas-analytics
context: { tenant_id: acme, region: [eu-1, eu-2] }
request_context_fields: [user_id]
- subject_cn: support-bot # exact subject CN
policy: support-bot

Each identity has exactly one of spiffe_id (a valid spiffe:// URI with a lower-case trust domain) or subject_cn, a policy, an optional static context (strings, integers or non-empty lists of those) and request_context_fields (default [], must not repeat a static context field). Identities must be unique. Non-empty identities require server.tls.client_ca and server.tls.client_auth set to optional or required.

policies​

A list of policies to load. Each entry is one of:

FormMeaning
{ path: FILE }Load a policy file (YAML or JSON). Relative paths are resolved against the config file's directory.
{ inline: { ...policy document... } }Embed a whole policy document, so a small deployment can be one file.
{ bundle: FILE, public_keys: [...], name_prefix: STR }A signed .haltsqlbundle. public_keys (non-empty) lists trusted ed25519 keys, each a file path, inline PEM (-----BEGIN PUBLIC KEY-----) or the base64 of the 32 raw key bytes. name_prefix (optional) is prepended to the names of the bundle's policies.
{ bundle_source: {...}, public_keys: [...], ... }A signed bundle fetched from an OCI registry, S3 (or an S3-compatible store) or HTTPS, and polled for updates. See bundle_source.

Policy names (metadata.name) must be unique. Every token's policy must name a loaded policy.

bundle_source​

policies:
- bundle_source:
oci: ghcr.io/corp/haltsql-policies:prod # or registry/repo@sha256:<digest>
public_keys: [./keys/release.pub] # mandatory
name_prefix: prod.
poll_interval_s: 60
cache_dir: /var/cache/haltsql/bundles
auth: { username: robot$haltsql, password: "${vault:secret/data/haltsql/registry#password}" }
- bundle_source:
s3:
bucket: corp-haltsql-bundles
key: prod/policies.haltsqlbundle
region: us-east-1
endpoint: http://127.0.0.1:9000 # S3-compatible store such as MinIO
credentials: { source: static, access_key_id: "${env:BUNDLE_AK}", secret_access_key: "${env:BUNDLE_SK}" }
public_keys: [./keys/release.pub]
- bundle_source:
https: https://bundles.corp.example/prod.haltsqlbundle
public_keys: [./keys/release.pub]
auth: { token: "${env:BUNDLE_TOKEN}" }
KeyTypeDefaultMeaning
bundle_sourcemappingrequiredExactly one of oci, s3 or https (below).
public_keyslistrequiredTrusted ed25519 keys, as for bundle. Signature verification is mandatory: a source without keys is a config error.
name_prefixstringnonePrepended to the names of the bundle's policies (1-64 of letters, digits, ., _, -, starting with a letter or digit).
poll_interval_sinteger 10-8640060Seconds between update checks.
cache_dirpathnoneDirectory holding the last verified bundle (relative to the config file).
authmappingnoneoci and https only: {token} (bearer token) or {username, password}. Not allowed for s3, which uses s3.credentials.

bundle_source forms:

FormMeaning
oci: REFAn OCI artifact. REF is registry[:port]/repository:tag, registry[:port]/repository@sha256:<64 hex> (or sha512:<128 hex>), or both (:tag@digest). The registry host is mandatory (there is no implicit Docker Hub): it must contain a . or a : or be localhost. Repository components are lower-case letters, digits, ., _ and -. Pin a digest for immutable deployments.
plain_http: trueWith oci only: talk plain HTTP to the registry. Allowed only for loopback registries (localhost, 127.x, [::1]), for local tests. Default false.
s3: {bucket, key, region, endpoint, credentials}An S3 object. bucket, key and region are required; key must not start with /. endpoint is an S3-compatible URL (MinIO). credentials has the same shape as for S3 audit sinks (env by default).
https: URLAn https URL (plain http only for loopback addresses). No credentials, query or fragment in the URL; use auth.

Two entries with the same source are an error. poll_interval_s, cache_dir and auth are only allowed with bundle_source; public_keys and name_prefix only with bundle or bundle_source.

Bundles and bundle sources are parsed and checked when the config loads, but they are fetched, verified and loaded by the server afterwards. So when a config lists bundles (of either form), a policy name used by a token, OIDC provider (including policy_from_groups map entries and default), mTLS identity or default_policy that is not one of the plain policies is accepted at config load time. If every bundle and bundle source has a name_prefix, such a name must start with one of them. The server re-validates every reference after loading the bundles and refuses to start (or rejects the reload) if one is still missing. If any policy fails to load, the whole config fails to load (at startup) or the reload is rejected and the last good set stays active (at runtime).

default_policy​

Name of the policy used for unauthenticated requests when allow_unauthenticated is true, and for requests that name no policy. Must name a loaded policy. Default: none.

reload​

KeyTypeDefaultMeaning
reload.watchbooleanfalseWatch the config and policy files (polled every 2 seconds) and reload on change. The server also reloads on SIGHUP. A reload is atomic: either all policies are swapped in, or none are.

What a reload (file change, SIGHUP, or a new bundle from a bundle source) applies, and what needs a restart:

Applied on reloadRestart only
policies (paths, inline policies, local bundle files and their signatures) and default_policyserver.listen, the health listener
server.auth.tokens, allow_unauthenticatedenabling or disabling server.tls (certificates and keys themselves reload)
server.auth.oidc (including policy_from_groups; key caches of unchanged providers are kept) and server.auth.mtlsaudit (all sinks, log_raw_sql): two writers on one chain would fork it. A reload that changes audit.sinks keeps the running sinks, logs a warning and sets audit_restart_pending in GET /v1/admin/status
server.tls certificate, key and client CA filesthe list of bundle_source entries: adding, removing or changing an entry needs a restart (the running pollers keep their settings; new bundles they fetch are applied as reloads)
opa (url, decision path, timeout, on_error, headers)limits, logging, telemetry.metrics.prometheus
server.admin (enabling, callers, report_window, max_tracked_queries; disabling it drops the report window)
telemetry.otlp

A reload that fails (any load, validation, reference or TLS error) changes nothing: the last good configuration stays active and the failure is logged and counted in haltsql_policy_reload_total{result="error"}.

audit​

KeyTypeDefaultMeaning
audit.sinkslist[{type: stdout}]Where audit events go: stdout, file, s3, gcs, elasticsearch or kafka (below).
audit.log_raw_sqlbooleanfalseInclude the raw SQL in audit events. Off by default because SQL can contain sensitive literals; events always include sql_sha256 and the fingerprint.

Every sink accepts on_error: fail_closed (default; if the sink cannot be written, requests are refused with HTTP 503) or buffer (records are kept in memory and retried).

SinkKeys
stdouttype, on_error
filetype, path (relative to the config file), on_error
s3type, bucket, region (required); prefix (default haltsql/audit/), endpoint (S3-compatible URL, e.g. MinIO), object_lock: {mode: GOVERNANCE|COMPLIANCE, retain_days: 1-36500}, batch, credentials, spool_dir, on_error
gcstype, bucket (required); prefix (default haltsql/audit/), batch, credentials, spool_dir, on_error

batch: max_records (1-100000, default 500) and max_seconds (1-3600, default 10).

spool_dir (s3/gcs, optional, relative to the config file; one directory per sink, on persistent storage): records are spooled there before upload, and the hash-chain head and object sequence number are persisted, so after a restart or crash the chain continues instead of starting again at genesis, and a batch uploaded just before a crash is not stored twice. Without it, each process starts a new chain (see Audit sinks).

S3 credentials.source: env (default; the standard AWS_* variables), static (needs access_key_id and secret_access_key, optional session_token; use secret placeholders and never write keys in the file), web_identity (IRSA / OIDC federation) or instance (instance profile). The static keys are only allowed with source: static.

GCS credentials.source: metadata (default) or service_account_file (needs file, relative to the config file).

Bucket names are validated (3-63 characters; S3: lower-case letters, digits, . and -; GCS also _). A prefix must not start with / or contain ...

Streaming sinks: elasticsearch and kafka​

The elasticsearch and kafka sinks stream records as they are written instead of uploading objects. They accept a third on_error value and their own batch shape:

KeyTypeDefaultMeaning
on_errorfail_closed, buffer, dropfail_closedfail_closed refuses requests (HTTP 503) when records cannot be delivered. buffer keeps up to max_buffer_records in memory and retries; when the buffer is full the sink fails closed. drop discards records that cannot be delivered (they are counted and logged); requests keep being served without an audit trail in this sink, so use it only next to another fail-closed sink.
max_buffer_recordsinteger 1-100000010000 (or batch.max_records if larger)Only with on_error: buffer. Must be at least batch.max_records.
batch.max_recordsinteger 1-100000500Records per request at most.
batch.max_bytesinteger 1024-1048576005242880 (5 MiB)Request body size at most.
batch.flush_interval_msinteger 10-600001000Milliseconds before a partial batch is sent.

elasticsearch (Elasticsearch or OpenSearch, bulk API):

audit:
sinks:
- type: elasticsearch
urls: [https://es-1.corp.example:9200, https://es-2.corp.example:9200]
data_stream: logs-haltsql-prod # or index: haltsql-audit
auth: { api_key: "${vault:secret/data/haltsql/es#api_key}" }
pipeline: haltsql-enrich
tls: { ca_file: ./certs/es-ca.pem }
timeout_ms: 10000
batch: { max_records: 500, max_bytes: 5242880, flush_interval_ms: 1000 }
on_error: buffer
max_buffer_records: 20000
KeyTypeDefaultMeaning
url / urlsURL / listrequiredExactly one of them. Node URLs: https, or plain http only for loopback addresses. No credentials, query or fragment (a path prefix is allowed). urls must be non-empty and unique.
index / data_streamstringrequiredExactly one of them. Lower-case letters, digits, ., -, _, +; at most 255 characters; not starting with -, _ or +; not . or ...
authmappingnone{api_key} (the encoded key, sent as Authorization: ApiKey <value>) or {username, password} (basic auth; the user name must not contain :). Use secret placeholders.
pipelinestringnoneIngest pipeline run on every record (letters, digits, ., _, -).
tls.ca_filepath or PEMsystem rootsCA bundle for the cluster certificate. Only allowed when a URL is https.
timeout_msinteger 100-12000010000Request timeout.

kafka (one message per audit record):

audit:
sinks:
- type: kafka
brokers: [kafka-1.corp.example:9093, kafka-2.corp.example:9093]
topic: haltsql.audit
client_id: haltsql-prod
acks: all
tls: { ca_file: /etc/haltsql/kafka/ca.pem }
sasl: { mechanism: scram-sha-512, username: haltsql, password: "${env:KAFKA_PASSWORD}" }
on_error: fail_closed
KeyTypeDefaultMeaning
brokerslistrequiredNon-empty, unique host:port bootstrap brokers (DNS name, IPv4, or [IPv6]).
topicstringrequired1-249 of letters, digits, ., _, -; not . or ...
client_idstringhaltsqlKafka client id (1-255 of letters, digits, ., _, -).
acksall, 1allAcknowledgements required per batch. 0 (fire and forget) is rejected: audit records must be acknowledged.
tlsmappingnone (plain text)enabled (default true when tls is given), ca_file, and a client certificate as cert_file + key_file (both or neither). The other keys are only allowed when enabled is true. Values are paths (relative to the config file) or inline PEM.
saslmappingnonemechanism (plain, scram-sha-256, scram-sha-512), username and password (all required). plain without tls is rejected unless every broker is a loopback address, because it sends the password in clear text.

The streaming sinks sit in the same audit.sinks list as the others and can be mixed with them; listing only streaming sinks means no stdout sink.

opa​

An optional Open Policy Agent check that runs after HaltSQL has allowed a query:

opa:
url: https://opa.corp.example:8181
decision_path: haltsql/allow
timeout_ms: 500
on_error: deny
headers:
Authorization: "Bearer ${vault:secret/data/haltsql/opa#token}"
tls: { ca_file: ./certs/opa-ca.pem }
KeyTypeDefaultMeaning
urlURLrequiredOPA base URL. https, or plain http only for loopback addresses (an OPA sidecar on 127.0.0.1). No credentials, query or fragment.
decision_pathstringrequiredThe decision below /v1/data, as Rego identifiers separated by /, without a leading /: haltsql/allow queries POST <url>/v1/data/haltsql/allow.
timeout_msinteger 10-5000500Decision request timeout.
on_errordeny, allow_haltsql_verdictdenyWhat happens when OPA cannot be reached, times out, or answers with an error or an undefined/non-boolean decision. deny refuses the query; allow_haltsql_verdict keeps HaltSQL's verdict.
headersmap{}Extra HTTP headers (for example a bearer token). Values may use secret placeholders, must not contain CR/LF, and are redacted by config show.
tls.ca_filepath or PEMsystem rootsCA bundle for the OPA certificate. Only allowed with an https url.

Semantics: OPA can only further deny. A query HaltSQL denies is denied without asking OPA, and OPA's answer can never turn a HaltSQL denial into an allow. A query HaltSQL allows is allowed only if OPA's decision is true (or, with on_error: allow_haltsql_verdict, if OPA could not give a decision). An OPA denial is reported with its own reason code so it can be told apart from HaltSQL's own reasons.

server.admin​

The admin API serves GET /v1/admin/report (shadow-mode analytics: which queries would have been denied, by reason and policy, over the report window) and GET /v1/admin/status. It is off by default.

server:
admin:
enabled: true
callers:
- token:ops-dashboard # a server.auth.tokens name
- oidc:corp-okta # any token verified by this provider
- mtls:spiffe://corp.example/ns/ops/sa/console # a configured mTLS spiffe_id
- mtls:ops-console # a configured mTLS subject_cn
report_window: 24h
max_tracked_queries: 10000
KeyTypeDefaultMeaning
enabledbooleanfalseServe the admin endpoints.
callerslist[]Who may call them. Required and non-empty when enabled is true. Each entry is token:<name>, oidc:<provider> or mtls:<spiffe-id or subject CN> and must name a configured token, OIDC provider or mTLS identity. Unauthenticated callers can never use the admin API.
report_windowduration24hHow far back the report looks: a whole number followed by s, m, h or d, between 1m and 30d.
max_tracked_queriesinteger 1-100000010000Distinct query fingerprints tracked for the report; the least recently seen are evicted first.

telemetry​

KeyTypeDefaultMeaning
telemetry.metrics.prometheusbooleantrueServe Prometheus metrics at GET /metrics.
telemetry.otlpmappingnoneOpenTelemetry (OTLP/HTTP) export, below.
telemetry.otlp_endpointURLnoneLegacy shorthand for telemetry.otlp.endpoint with all other defaults. Cannot be combined with telemetry.otlp. Must be an http(s) URL.

telemetry.otlp:

KeyTypeDefaultMeaning
endpointURLrequiredCollector base URL, e.g. http://collector:4318.
headersmap{}Extra HTTP headers. Values may use secret placeholders, must not contain CR/LF, and are redacted by config show.
service_namestringhaltsqlservice.name resource attribute.
tracesbooleantrueExport traces.
metricsbooleantrueExport metrics.
export_interval_secondsinteger 1-360015Metric export interval.
timeout_secondsinteger 1-3005Export request timeout.

logging​

KeyTypeDefaultMeaning
logging.leveldebug, info, warn, errorinfoMinimum log level.
logging.formatjson, textjsonLog line format. Logs go to stderr.

limits​

KeyTypeDefaultMeaning
limits.max_request_bytesinteger (1024 to 67108864)1048576Largest accepted HTTP request body. Larger requests get HTTP 413.

secrets​

Configures the secret providers used by the ${vault:...}, ${aws-sm:...} and ${gcp-sm:...} placeholders. Values in this section may only use ${env:...} and ${file:...} (a provider cannot configure itself).

KeyMeaning
secrets.vault.addressVault URL. https, or http for loopback addresses only.
secrets.vault.namespaceVault Enterprise namespace (optional).
secrets.vault.auth.methodtoken (needs token), kubernetes (needs role; jwt_path defaults to /var/run/secrets/kubernetes.io/serviceaccount/token, mount to kubernetes) or approle (needs role_id and secret_id; mount defaults to approle). Fields of other methods are errors.
secrets.vault.ca_fileCA bundle for the Vault server certificate (optional).
secrets.aws.region, secrets.aws.endpointAWS Secrets Manager region (required) and optional endpoint (tests).
secrets.gcp.project, secrets.gcp.endpointDefault project for short secret names, and optional endpoint (tests).

Vault tokens, AppRole role and secret ids are redacted by config show.

config show also redacts: Elasticsearch API keys and passwords, the Kafka SASL password and TLS client key, bundle source passwords and tokens, static S3 credentials (sinks and bundle sources), OPA and OTLP header values, the server TLS key and token hashes. Inline PEM values are shown as <inline PEM>.

Secrets: placeholders​

Config values can load secrets without writing them into the file:

PlaceholderReplaced by
${env:NAME}The value of environment variable NAME.
${file:/path}The contents of the file (at most 1 MiB), with one trailing newline (\n or \r\n) removed. Relative paths are resolved against the config file's directory.
${vault:<path>#<field>}Field <field> of the Vault secret at <path> (e.g. secret/data/haltsql/otel#token).
${aws-sm:<secret-id>[#<json-key>]}An AWS Secrets Manager secret, or one key of its JSON value.
${gcp-sm:<name>[#<json-key>]}A Google Secret Manager secret (projects/P/secrets/S/versions/V or a short name in secrets.gcp.project), or one key of its JSON value.
$${A literal ${.

The provider placeholders are resolved by the program loading the config (the server and CLI build a resolver from the secrets section first, then load the rest). A provider placeholder in a program or context with no resolver is a load error ("secret provider not configured"). Each distinct reference is fetched once per load.

Rules:

  • A placeholder that cannot be resolved (unset variable, unreadable file, missing secret) is a load error. HaltSQL never substitutes an empty string.
  • Placeholders are allowed only in Config documents. They are rejected in policies, which may only use ${ctx.*}.
  • Resolved secrets are never printed. haltsql config show replaces them with "<redacted>".

Precedence​

Runtime settings come from four places. Higher rows win:

PrioritySourceExample
1 (highest)Command-line flagshaltsql-server --listen 0.0.0.0:9000
2HALTSQL_* environment variablesHALTSQL_LISTEN=0.0.0.0:9000
3Config fileserver.listen: 0.0.0.0:8443
4 (lowest)Built-in defaults127.0.0.1:8080

Environment variables recognized in 0.0.1:

VariableOverrides
HALTSQL_LISTENserver.listen
HALTSQL_LOG_LEVELlogging.level
HALTSQL_LOG_FORMATlogging.format
HALTSQL_ALLOW_UNAUTHENTICATEDserver.auth.allow_unauthenticated (true/false, 1/0, yes/no, on/off)
HALTSQL_DEFAULT_POLICYdefault_policy

An invalid value in one of these variables is a load error, not ignored.

Use haltsql config show --config FILE to print the effective, merged config with secrets redacted. Run it after every change; it is the fastest way to see what the server will actually do.

Quick start without a config file​

For local experiments, the server accepts a policy directly:

haltsql-server --policy policy.yaml --listen 127.0.0.1:8080 --allow-unauthenticated

--policy is repeatable and adds to (or, by name, replaces) the config's policies. See cli.md for all server flags. Use a config file with tokens for anything shared or deployed.