Skip to content

Production Hardening

Recent authorization changes also protect /metrics with Admin permission or the configured scrape identity, and protect shared-reference-realm writes with Admin permission. Give monitoring its intended identity instead of making the endpoint anonymous. Native machine credentials use canonical tenant bindings; request headers cannot supply a substitute tenant.

Configure exclusive writers and machine defaults for service-owned graphs. Governed status and release-manifest writes have additional native guards; ordinary graph Write permission does not bypass them.

By default an IndentiaDB server starts fully open — anonymous access, allow-all authorization. That is convenient for development and a compliance failure for anything else. Setting IG_PRODUCTION=true turns startup into a fail-closed gate: the server refuses to boot unless the configuration meets a production security baseline.

IG_PRODUCTION accepts true/false, yes/no, on/off, or 1/0 (case-insensitive). Any other value refuses startup, so a typo can never silently switch the gate off.


The IG_PRODUCTION Boot Gate

With IG_PRODUCTION=true, the server validates the loaded security configuration at startup and refuses to boot unless all of the following hold:

Requirement Why
authentication.enabled = true The core data plane (/sparql, /update, /sql, /graphql), the control plane (/cluster/*), and the :9200 Elasticsearch mirror are always mounted and would otherwise serve anonymously
authentication.allow_anonymous = false No anonymous fallback principal
At least one enabled authentication provider LDAP, OIDC, or a service token
acl.require_tenant_scope = true Every request is evaluated within the caller's tenant
acl.default_access is not allow_all Fail-closed authorization default
audit.enabled = true Security events are always recorded
[authorization.roles] maps admin to "admin" The reserved admin role keeps Admin permission
Provider transport is secure OIDC issuer, discovery, and JWKS URLs must be HTTPS; the loopback-HTTP development override (IG_ALLOW_INSECURE_LOOPBACK_OIDC_HTTP) and the in-cluster allow_insecure_cluster_http provider option are refused. LDAP must use ldaps:// or start_tls = true
OIDC roles_claim has a role_mapping Raw claim values are never used as roles in production
IG_AUDIT_LOG_FILE is an absolute path See Durable Audit Log
IG_CLUSTER_ID is a valid UUID Production never generates a temporary cluster identity; keep the value stable across restarts and record it with your recovery secrets

A config file with enabled = false does not count as authentication

The gate evaluates the loaded configuration values, not the mere presence of a security file. Mounting a security.toml that contains authentication.enabled = false still fails the gate.

Additional refusal conditions

IG_PRODUCTION=true also refuses to boot when:

  • IG_ACL_BYPASS is set (admin ACL bypass), or IG_ACL_ENFORCE=false (global ACL off-switch).
  • The Elasticsearch mirror is enabled without ACL enforcement (see below), or IG_ES_ALLOW_UNREPLICATED_WRITES is enabled.
  • IG_CLUSTER_BOOTSTRAP_ARMED is not set explicitly to true or false (true only for the first bootstrap of a new, empty cluster).
  • A standalone node has no durable RDF write-ahead log (IG_RDF_WAL_DIR); production does not acknowledge writes held only in memory.
  • Cluster mode is configured with cluster.allow_plaintext, without security.mtls_enabled = true, or without security.tls_enabled = true for the HTTP API.
  • Cluster mode disables join-identity checks (IG_REQUIRE_JOIN_IDENTITY=false) or the shard reconciler (IG_ALLOW_DISABLED_RECONCILER=true). See Cluster Posture.
  • Cluster mode enables the Elasticsearch-compatible API without IG_ES_RDF_SOT=true. Without the replicated RDF write path, every :9200 document write would land only on the receiving node.
  • SPARQL federation is enabled with an empty IG_SPARQL_FEDERATION_ALLOWLIST, or with an allowlist entry that is not an explicit HTTPS URL (wildcards and credentials in the URL are refused).
  • The legacy raw SurrealDB proxy (IG_ENABLE_SURREAL_PROXY) is enabled alongside the Elasticsearch-compatible API.
  • Encryption at rest is misconfigured — production implies IG_ENCRYPTION_REQUIRED unless explicitly opted out, and a Vault trust root (IG_CRYPTO_VAULT_CA_FILE) is mandatory.
  • Experimental features that are not production-safe are enabled (e.g. IG_D5_SPREAD_EXPERIMENTAL).
  • A dangerous operator escape hatch is set (see the next section).

There is no unauthenticated escape hatch: the production authentication gate is a hard bail, and the former IG_ALLOW_UNAUTHENTICATED flag is itself refused. For network-fenced development instances, simply leave IG_PRODUCTION unset.


Escape-Hatch Boot Audit

IndentiaDB carries a small number of deliberate escape hatches for recovery and migration — ignore modes that boot past corrupt state, relaxed fsync policies, SSRF-guard opt-outs. Each is safe by default, but in production a set hatch is usually a leftover from an incident, a stale manifest, or a typo.

Under IG_PRODUCTION=true the server evaluates every escape hatch in one audit, before any data, listener, or security file is touched. If anything is refused, startup fails with a single error that lists all violations at once, so you fix the manifest in one pass instead of discovering problems one restart at a time.

Values are matched generously so that intent is never missed: tokens are trimmed and case-insensitive (IG_WAL_RECOVERY=Truncate is refused even if a consumer would ignore that casing), truthy means 1/true/yes/on/y/t/enable/enabled, and explicit false means 0/false/no/off.

Refused settings

Setting Why it is refused Acknowledgeable
IG_RAFT_SNAPSHOT_RECOVERY=ignore Boots an HA node without its persisted Raft snapshot while the log is purged, permanently losing the purged prefix yes
IG_WAL_RECOVERY=ignore Boots with empty replayed state, losing the local WAL history yes
IG_WAL_RECOVERY=truncate Truncates at the first bad record without a committed-watermark bound; can drop acknowledged writes yes
IG_WAL_RECOVERY=skip-undecodable Silently drops undecodable metadata WAL records, which can desynchronise tenant-to-shard routing yes
IG_RDF_CHECKPOINT_RECOVERY=ignore Boots despite a corrupt RDF checkpoint, losing the covered write history yes
IG_META_CHECKPOINT_RECOVERY=ignore Boots despite a corrupt metadata checkpoint, losing covered control-plane history yes
IG_SHARD_WAL_RETIRE=ignore Boots past legacy per-shard WALs whose writes are no longer served yes
IG_CLUSTER_BACKUP_ALLOW_UNCOVERED_WAL (truthy) Cluster backups would omit writes still held in shard WALs yes
IG_CLUSTER_BACKUP_ALLOW_UNFENCED (truthy) Cluster backups without process-incarnation fencing; a stale leader could overwrite a newer backup yes
IG_RAFT_LOG_SYNC=range Raft acknowledgements without a device flush can be lost on power failure no
IG_WAL_SYNC=never Acknowledged writes are lost on a crash or power loss no
IG_META_WAL_APPEND=fail-open Control-plane writes would continue in memory after a failed durable append no
IG_BACKUP_MANIFEST_ALLOW_UNSIGNED_LEGACY (truthy) Accepts unsigned backup manifests; during a signing migration set it only on the backup/restore CLI invocation, never on the server no
IG_ALLOW_UNAUTHENTICATED (truthy) Removed flag; authentication is mandatory in production no
IG_ALLOW_INSECURE_LOOPBACK_VAULT (truthy) Plaintext loopback Vault Transit endpoint no
IG_ALLOW_INSECURE_LOOPBACK_LEADER_FORWARD (truthy) Plaintext loopback leader forwarding no
IG_RECONCILE_REQUIRE_HTTPS=false Cluster reconciliation must use verified HTTPS no
IG_SPARQL_LOAD_ALLOW_LOCALHOST (truthy) Disables the SPARQL LOAD SSRF guard (loopback, private networks, cloud metadata) no
IG_SPARQL_LOAD_ENABLED (truthy) with an empty IG_SPARQL_LOAD_ALLOWLIST LOAD would fetch arbitrary external URLs no

Configuration-file options with the same risk profile are refused by the security gate above rather than by this audit — notably OIDC allow_insecure_cluster_http, cluster.allow_plaintext, and IG_REQUIRE_JOIN_IDENTITY=false. None of them can be acknowledged.

Acknowledging a recovery boot

The settings marked acknowledgeable can be legitimately needed to bring a damaged production node back or to take an emergency backup. Acknowledge them explicitly by exact flag name:

IG_WAL_RECOVERY=ignore
IG_PRODUCTION_RECOVERY_ACK=IG_WAL_RECOVERY

An acknowledged hatch boots with a loud warning on the audit log target, while the rest of the production posture stays in force. The acknowledgement cannot linger: naming a flag that is not acknowledgeable, an unknown flag, or a flag that is not currently set to a refused value is itself a boot violation. Remove both the hatch and IG_PRODUCTION_RECOVERY_ACK once the recovery is done.

Do not unset IG_PRODUCTION for a recovery boot

Booting without IG_PRODUCTION is far broader than one acknowledged hatch: it re-enables admin SQL, leaves the :9200 mirror open unless IG_ES_ACL_ENFORCE is set independently, and drops the encryption-required default — plaintext records written during such a boot then block the next production boot. Prefer, in order: restore from backup, re-seed the node from a healthy peer through Raft replication, and only then a one-off acknowledged hatch.

Warned settings

These settings do not block startup, but each is logged as a warning on the audit log target at every boot so it remains visible until removed:

Setting Meaning
IG_RAFT_LOG_RECOVERY=truncate Bounded torn-tail truncation (see Torn-Log Recovery); unset after recovery
IG_RAFT_RECOVERY_DISABLE_ELECT_UNTIL_MULTI_MEMBER=false Singleton-recovery election guard disabled; for deliberate disaster-recovery flows only
IG_ENCRYPTION_REQUIRED=false Encryption at rest is off during a migration window
IG_CRYPTO_MIGRATE (truthy) Plaintext-to-sealed re-seal pass authorised; unset when complete
IG_DQS_REQUIRE_TENANT=false Untagged distributed-query requests accepted during a rolling upgrade; time-bound it
IG_READY_SHARD_CONDITIONS=false /ready no longer reflects shard health
IG_PITR_ALLOW_PLAINTEXT (truthy) Point-in-time recovery artifacts archived without authentication
IG_ENABLE_ADMIN_SQL (truthy) Inert — production always disables admin SQL; remove it from the manifest
IG_LEADER_FORWARD_ALLOWED_ORIGINS (non-empty) Inert — production uses its built-in leader-forward policy; remove it from the manifest

Elasticsearch Mirror Requirements

When the Elasticsearch-compatible API on port 9200 is enabled, IG_PRODUCTION=true additionally requires:

Variable Requirement Effect
IG_ES_ACL_ENFORCE must be true Fail-closed ACL gate on the :9200 read path, evaluated against the live security annotation index
IG_ES_PROXY_TOKEN must be non-empty Shared secret between IndentiaDB and your identity-injecting proxy
IG_ES_RDF_SOT must be true in cluster mode Document writes go through the replicated RDF write path, so every node's :9200 view is derived from the same committed data

The ES mirror receives the caller's identity via the X-Indentia-Sid header. Without a proxy token, that header would be spoofable by any client. When IG_ES_PROXY_TOKEN is set, X-Indentia-Sid is only honored if the request also carries a matching X-Indentia-Proxy-Token header (compared in constant time); a mismatch is rejected.

Client ──► Search proxy (authenticates user, injects X-Indentia-Sid
                          + X-Indentia-Proxy-Token)
                 │
                 ▼
         IndentiaDB :9200  (verifies proxy token, enforces ACL per SID)

Pair this with a NetworkPolicy so only the proxy can reach port 9200.


Cluster Posture

In cluster mode, production changes several defaults and refuses the settings that would weaken them.

Durable Raft acknowledgements

IG_RAFT_LOG_SYNC defaults to full under IG_PRODUCTION: every acknowledged Raft write is flushed and fdatasynced, so it survives power loss. Outside production the default remains range, which orders writes but issues no device flush. IG_RAFT_LOG_SYNC=range is refused in production and cannot be acknowledged.

Join identity

When IG_CLUSTER_ID is configured — always the case in production — a joining node verifies the cluster identity in the seed or leader's response. A response without a cluster UUID is rejected; a mismatching UUID is always fatal.

IG_REQUIRE_JOIN_IDENTITY (or [cluster] require_join_identity) Behaviour
unset (default) Required whenever IG_CLUSTER_ID is configured
true Required explicitly; startup fails if IG_CLUSTER_ID is not configured
false Transition setting for rolling upgrades from older peers: a missing UUID is only logged. Refused under IG_PRODUCTION

The environment variable takes precedence over the configuration file. Standalone nodes ignore the setting.

Tenant tags on distributed queries

Distributed-query workers re-check the tenant tag carried by each request against the replicated cluster state (see Authorization). IG_DQS_REQUIRE_TENANT controls how an untagged request from an older coordinator is treated; it defaults to the value of IG_PRODUCTION, so production refuses untagged requests. Set it to false only for the duration of a rolling upgrade — the boot audit records it as a warning.

Fixed topology

A production cluster's topology is operator-managed. The cluster-admin endpoints that would change it at runtime return 409 Conflict with error production_fixed_topology (not retryable):

Endpoint Operation
POST /cluster/indices Create an index
DELETE /cluster/indices/{index} Delete an index
POST /cluster/tenants Map a tenant
POST /cluster/indices/{index}/allocate Allocate shards
POST /cluster/rebalance Rebalance shards
POST /cluster/meta/replicas Change the metadata replica count
POST /cluster/nodes/{id}/drain Drain a node

Plan topology changes as a separate backup/restore or membership procedure.


Production Traffic Behaviour

  • Strong reads by default. Production reads default to strong (linearizable) consistency. An explicit consistency parameter or X-Indentia-Consistency header with any value other than strong or linearizable is rejected with 400 invalid_request. (Outside production, reads stay local unless strong consistency is requested — see Read Consistency.)
  • Readiness-gated traffic. Ordinary requests are admitted only while the node is ready, which in production includes a certified ACL index and a healthy audit sink. Otherwise requests receive 503 linearizable_read_unavailable (or 503 durable_audit_unavailable when the audit sink is the cause) with Retry-After: 1. /live, /ready, /health, /health/details, and /metrics stay available for diagnosis.
  • No internal redirects. Production never redirects a client to an internal node address; a leader change during a request returns 503 leader_transition with Retry-After: 1. Retry through your stable client endpoint.

Durable Audit Log

IG_AUDIT_LOG_FILE enables an append-only, SHA-256 hash-chained audit log. It is required under IG_PRODUCTION and must be an absolute path on persistent storage (e.g. /data/audit/audit.log). Every audit event is appended as one JSON line and fsynced before the chain advances. Each record embeds the hash of the previous record:

hash(n) = SHA-256( hash(n-1) ‖ seq(n) ‖ canonical_json(event(n)) )

Editing, reordering, or deleting any record breaks the chain — tampering is detectable, not preventable-in-silence.

Startup

Before any listener binds, the server takes an exclusive lock on the file, verifies the complete existing chain, and appends and fsyncs a startup record. A symlinked or non-regular destination, a file locked by another process, or a corrupt or partially written chain fails startup; the server never silently starts a new chain over a damaged one.

Runtime failures

If an append, flush, fsync, or lock later fails, audit health becomes sticky-unhealthy:

Signal Under IG_PRODUCTION
GET /ready 503
ig_audit_sink_healthy (on /metrics) 0
Ordinary API requests 503 with error durable_audit_unavailable and Retry-After: 1, before any data or admin handler runs
/live, /ready, /health, /health/details, /metrics Remain available for diagnosis

Outside production, a sink failure is logged and recorded in audit health, but requests are not blocked. When IG_AUDIT_LOG_FILE is unset (development only), audit events are still emitted through the structured log (target audit).

Captured events

Event Contents
AuthenticationSuccess actor, auth method, timestamp
AuthenticationFailure attempted identity, generic reason (no user-existence leak), source IP
AuthorizationFailure actor, resource, required permission
RateLimitViolation identifier, source IP
WriteAttempt actor, target graphs, success/failure — SPARQL /update, Graph Store, and GraphQL mutations
ApiMutationAttempt production only: a correlated admission record (fsynced before the handler runs) and outcome record (final HTTP status) for every API request that is not GET, HEAD, or OPTIONS (health, readiness, and metrics endpoints excepted); carries the actor, method, and route template — never the raw URI, query string, headers, or body
InternalRpcMutationAttempt correlated admission and outcome records for mutating cluster-internal RPCs
AclFiltering actor, number of filtered results

Verifying the chain

The chain is verified programmatically with verify_chain, which walks every record and reports the first break: an inserted/removed record (sequence gap), a tampered chain link, or a tampered record body.

A hash chain is tamper-evident, not tamper-proof

The chain is local and unkeyed. It detects corruption, incomplete tails, and edits or deletions inside the file, but a privileged storage administrator can still rewrite the whole history or truncate a valid suffix. For a regulated bar, export records to an authenticated off-cluster collector with immutable retention.


IG_PRODUCTION=true
IG_CLUSTER_ID=<stable-cluster-uuid>        # required; record it with your recovery secrets
IG_AUDIT_LOG_FILE=/data/audit/audit.log    # required; absolute path on persistent storage
IG_CLUSTER_BOOTSTRAP_ARMED=false           # required; true only for the first bootstrap of a new cluster

# Standalone nodes: durable RDF write-ahead log
IG_RDF_WAL_DIR=/data/rdf-wal

# Elasticsearch-compatible API on :9200
IG_ES_ACL_ENFORCE=true
IG_ES_PROXY_TOKEN=<shared-with-proxy>
IG_ES_RDF_SOT=true                         # required in cluster mode

Leave IG_RAFT_LOG_SYNC, IG_REQUIRE_JOIN_IDENTITY, and IG_DQS_REQUIRE_TENANT unset so the production defaults apply, and keep every escape hatch from the boot audit out of the manifest.

Pair this with a security.toml that enables authentication (OIDC, LDAP, or service tokens), sets allow_anonymous = false, acl.require_tenant_scope = true, acl.default_access = "deny_all", audit.enabled = true, keeps admin = "admin" in [authorization.roles], and leaves [authorization].default_permission at "none" (see Authorization). Cluster mode additionally needs security.mtls_enabled = true and security.tls_enabled = true. For a regulated bar, add encryption at rest with Vault Transit.


See Also