Skip to main content

Hardening baseline

This page is a secure configuration baseline for production deployments of HaltSQL. It is for platform and security engineers who deploy haltsql-server or haltsql-proxy and want one list of settings to review. Each row gives the setting, the recommended value, the reason and the page with the details.

Most defaults are already the safe choice. The rows marked (not default) need an explicit change.

Database​

HaltSQL is one layer of three. A bug in HaltSQL, a policy mistake, or a caller that skips the check is contained only by the database's own controls (defense-in-depth.md).

SettingRecommended valueWhyDetails
Database role for agent queriesA dedicated role: NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS, SELECT granted only on the policy's tables and columnsThe database still enforces the basics if HaltSQL is bypassed or wrongLayer 1
Role session defaultsdefault_transaction_read_only = on, statement_timeout (for example 5s), lock_timeout, idle_in_transaction_session_timeoutHaltSQL does not bound how long an allowed query runsLayer 1
Role search_pathIdentical to the policy's search_pathOtherwise an unqualified name can resolve to a different table than the one HaltSQL checkeddefense-in-depth.md
Row-level securityENABLE and FORCE ROW LEVEL SECURITY on every tenant table, keyed on a setting your application sets per transactionTenant isolation holds even if a query escapes HaltSQLLayer 2
RLS coverage checkRun haltsql rls check against the live database in CI or after migrationsReports where the role, grants and RLS policies in the database have drifted from the HaltSQL policyrls-generation.md
Dangerous functionsRevoke EXECUTE from PUBLIC on functions you do not need (for example pg_sleep)Defense in depth for functions a policy might allow by mistakedefense-in-depth.md

Policies​

SettingRecommended valueWhyDetails
modeenforce (the default). Use shadow only during a rollout.A policy in shadow mode reports violations but allows the querypolicy-reference.md
tenancy.modebound (the default)The tenant value is a bound parameter supplied from the context, never written by the modeltenancy.md
statements.allow[select] (the default) unless writes are requiredRead-only agents should not be able to write, whatever the SQL sayspolicy-reference.md
require_limit.maxSet it (not default), for example 1000Bounds rows per query; every SELECT must end in a constant LIMIT n with n <= maxpolicy-reference.md
limitsKeep the defaults (max_bytes 16384, max_depth 64, max_nodes 20000, max_joins 8) or lowerBounds parsing and analysis cost and query shapepolicy-reference.md
tables.*.columnsExplicit column lists; avoid "*" on tables with sensitive columns"*" allows every current and future columnschema-and-pii.md
allow_explainfalse (the default)Plans expose planner estimates (row counts) that the agent does not need; EXPLAIN ANALYZE is always deniedpolicy-reference.md
Policy testsRun haltsql validate, haltsql lint-policy and haltsql test in CI on every policy changeCatches policies that allow more than intended before they shippolicy-ci.md

Policy delivery​

SettingRecommended valueWhyDetails
Policy sourceLoad policies only from signed bundles: policies[].bundle or policies[].bundle_source (not default)The server verifies the Ed25519 signature before it reads a policy, so a compromised file system or delivery channel cannot change thempolicy-bundles.md
public_keysOnly the current signing key, plus the next one during a rotationAny listed key may signKey management
Signing keyGenerated with haltsql bundle keygen, stored as a protected CI secret, never committedWhoever holds it can authorize any policypolicy-ci.md
bundle_source.ociA digest-pinned reference (repo@sha256:...) for immutable deployments, or a tag with monotonic metadata.versionThe poller refuses rollbacks; a digest pins the exact artifactconfig-reference.md
cache_dirSet, on a writable volumeThe server can restart from the last verified bundle while the source is down; the cache is re-verified on every startpolicy-bundles.md

Note: there is no setting that forbids plain path: or inline: policies. To enforce "signed bundles only", review the config in change control, and check that every policy in GET /v1/admin/status has origin: bundle_source (or bundle_or_file for local bundles).

haltsql-server: transport and callers​

SettingRecommended valueWhyDetails
server.tlsEnabled (not default) whenever traffic leaves the pod or host; min_version: "1.3" if all clients support itWithout it, tokens and SQL cross the network in clear textBuilt-in TLS
server.listen127.0.0.1:8080 (the default) when TLS is terminated elsewhere or the agent is in the same podKeeps the plaintext listener off the network. The container image sets HALTSQL_LISTEN=0.0.0.0:8080, which overrides the config file.config-reference.md
server.tls.client_authrequired with server.auth.mtls identities, for service-to-service callersStrong workload identity; the context comes from config, not the requestmTLS identities
Caller authenticationOIDC or mTLS rather than static tokens when the tenant or user must come from an authenticated identityWith static tokens the request body supplies the whole contextauthentication.md
server.auth.oidc[].context_claimsMap the tenant (and user) fields to claimsThe tenant comes from the signed token; the body cannot override it (403 CONTEXT_NOT_ALLOWED)OIDC
request_context_fields[] (the default), or the smallest list that worksEvery listed field is supplied by the caller, not the identityauthentication.md
server.auth.oidc[].required_claimsA scope or audience-specific claim, for example scope: sql.verifyRejects tokens the IdP issued for other purposesOIDC
policy_from_groupsMost privileged groups first; default: null; on Entra ID prefer app roles (claim: roles)The first match wins; no default means unknown callers are rejected; app roles never overflowPolicies from IdP groups
Static tokensOne token per agent, at least 32 random bytes (openssl rand -hex 32), only token_sha256 in configHaltSQL stores an unsalted SHA-256, which is safe only for high-entropy tokensconfig-reference.md
server.auth.allow_unauthenticatedfalse (the default); leave HALTSQL_ALLOW_UNAUTHENTICATED unsetUnauthenticated callers can name any loaded policy or use default_policyconfig-reference.md
default_policyUnset (the default)Only used for unauthenticated requests and requests that name no policyconfig-reference.md
server.adminDisabled (the default), or callers limited to mTLS or OIDC identities of operatorsThe admin report reveals query patterns, and with log_raw_sql some SQLconfig-reference.md
/metrics, /healthz, /readyzRestrict with a NetworkPolicy, or serve probes on --health-listen and disable Prometheus (telemetry.metrics.prometheus: false) if unusedThese endpoints need no authentication; metrics are labelled with policy names and reason codeshttp-api.md
limits.max_request_bytes1048576 (the default) or lowerBounds request sizeconfig-reference.md
secrets and placeholdersEvery credential through ${vault:...}, ${aws-sm:...}, ${gcp-sm:...}, ${file:...} or ${env:...}; never inlineConfig files and ConfigMaps hold no secretssecrets.md

Audit​

SettingRecommended valueWhyDetails
on_errorfail_closed (the default) on at least one sinkNo verdict is served without its audit recordFailure policy
buffer / dropbuffer only where availability matters more than synchronous auditing; drop only next to another fail_closed sinkdrop discards records with no gap marker in the chainconfig-reference.md
audit.log_raw_sqlfalse (the default)SQL can contain sensitive literals; records always carry sql_sha256 and the fingerprintconfig-reference.md
Durable sinks3 with object_lock: {mode: COMPLIANCE, retain_days: N} and a spool_dir on persistent storage (not default), or gcs with a bucket retention policyWORM storage stops whole-log rewrites; the spool keeps the chain continuous across restartsRestarts and chain continuity
Chain verificationRun haltsql audit verify --expect-head <recorded head> on a schedule and alert on failureA chain proves nothing unless someone checks itaudit-verify.md
Kafka sinktls enabled, sasl with scram-sha-512, acks: all (the default)Without tls the sink is plaintextconfig-reference.md
File sink volumeMount a writable volume at /var/log/haltsql only if you use the file sinkThe rest of the root filesystem stays read-onlydeployment.md
AlertsOn haltsql_audit_errors_total, haltsql_audit_sink_healthy == 0 and haltsql_reasons_total{code="E_INTERNAL"}Detects audit loss and internal errors earlyhttp-api.md

OPA​

SettingRecommended valueWhyDetails
opa.on_errordeny (the default)An unreachable or misconfigured OPA must not allow queriesopa.md
opa.urlA loopback sidecar, or https with tls.ca_filePlain http is accepted only for loopbackconfig-reference.md
opa.headersA bearer token from a secret placeholder, with OPA's own authentication enabledStops other workloads from changing OPA's answersopa.md

OPA can only add denials: a query HaltSQL denies is never sent to OPA, and no OPA answer turns a denial into an allow. There is nothing to configure for this.

haltsql-proxy​

SettingRecommended valueWhyDetails
tlsConfigured (not default); required: true (the default once tls is set)Without a tls section the proxy accepts plaintext clientsproxy.md
auth.methodcert with tls.client_auth: require, or scram with long random passwordsThe identity selects the policy and the contextproxy.md
upstream.tls.modeverify-full with upstream.tls.ca (not default; the default is disable)require encrypts without verifying the serverproxy.md
upstream.userA least-privilege, read-only role, as in DatabaseThe proxy's credentials are the only ones clients useproxy.md
upstream.passwordA ${env:...} or ${file:...} placeholderNever in the fileproxy.md
limitsSet max_connections, max_message_bytes, idle_timeout_seconds for your loadBounds what one client can consumeproxy.md
auditA sink with on_error: fail_closed (the default)No statement reaches the database without its audit recordproxy.md
NetworkOnly the proxy can reach the database (security groups, NetworkPolicy, pg_hba.conf)A client that reaches PostgreSQL directly bypasses the proxydeploy-aws.md

Runtime​

SettingRecommended valueWhyDetails
Imageghcr.io/haltsql/haltsql pinned by digest, signature verified with cosign verifyEnsures you run the artifact the release workflow builtsupply-chain.md
Base imageThe default distroless/static-debian12:nonroot runtime stageNo shell, no libc, no package managerdeployment.md
UserNon-root, uid/gid 65532 (the image default)Limits the impact of a process compromiseDockerfile
Root filesystemreadOnlyRootFilesystem: true / docker run --read-onlyNothing in the image writes to the root filesystemDockerfile
Container security contextallowPrivilegeEscalation: false, capabilities.drop: [ALL], seccompProfile: RuntimeDefault, runAsNonRoot: trueSet by the Helm chart's defaultsvalues.yaml
Service account tokenautomountServiceAccountToken: false (the chart default) unless Vault Kubernetes auth needs itHaltSQL does not call the Kubernetes APIvalues.yaml
NetworkPolicynetworkPolicy.enabled: true (the chart default) with ingressFrom set to the agent pods (an empty list allows any pod in the namespace); egress limited with extraRules to the endpoints you configuredEgress is denied by default because verification needs no network accessvalues.yaml
Sidecar topologyOne haltsql-server per agent pod, reached on 127.0.0.1Verification traffic never leaves the poddeployment.md
systemdThe shipped unit (DynamicUser, ProtectSystem=strict, NoNewPrivileges, empty CapabilityBoundingSet, and more)Same isolation outside containers; check with systemd-analyze securitydeployment.md

SDKs and the MCP server​

SettingRecommended valueWhyDetails
Execution pathOnly the SDK's guarded-execute helperIt checks sql_sha256, binds bind_params, and treats any error as a denialdeployment-modes.md
statement_timeout_ms, max_rows (Python SDK)Set bothHaltSQL does not limit run timesdk-python.md
haltsql-mcp row cap and timeoutKeep --row-cap (default 200) and --statement-timeout-ms (default 5000)Bounds what one tool call can return and how long it runsmcp.md
haltsql-mcp HTTP transportA tokens file, and a TLS proxy in frontThe MCP server has no built-in TLSmcp.md

Checking a deployment​

  • haltsql config show --config FILE prints the effective configuration (flags, HALTSQL_* environment and file merged) with secrets redacted.
  • GET /v1/admin/status shows loaded policies, their origin and mode, bundle signer key ids, audit sink health and the OPA settings.
  • haltsql-proxy validate -c FILE --json prints the proxy's effective config with secrets redacted.