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_BYPASSis set (admin ACL bypass), orIG_ACL_ENFORCE=false(global ACL off-switch).- The Elasticsearch mirror is enabled without ACL enforcement (see below), or
IG_ES_ALLOW_UNREPLICATED_WRITESis enabled. IG_CLUSTER_BOOTSTRAP_ARMEDis not set explicitly totrueorfalse(trueonly 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, withoutsecurity.mtls_enabled = true, or withoutsecurity.tls_enabled = truefor 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:9200document 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_REQUIREDunless 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:
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
consistencyparameter orX-Indentia-Consistencyheader with any value other thanstrongorlinearizableis rejected with400 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(or503 durable_audit_unavailablewhen the audit sink is the cause) withRetry-After: 1./live,/ready,/health,/health/details, and/metricsstay 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_transitionwithRetry-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:
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.
Recommended Production Configuration¶
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¶
- Encryption at Rest — AES-256-GCM envelope encryption for WAL, checkpoints, and Raft state
- Authentication — providers, including tenant-scoped service tokens
- Authorization — role permissions,
default_permission, and tenant scoping - Triple-Level ACL — SID annotations and the fail-closed ACL index
- High Availability & Clustering — mTLS, linearizable reads, replication robustness