Authentication¶
Authentication is the first layer of IndentiaDB's three-layer security model. It establishes the caller's identity and produces a principal — a user identity with roles and SIDs (Security Identifiers) used by the authorization and ACL layers.
LDAP (including Windows Active Directory)¶
LDAP authentication supports any LDAP-compliant directory, including Microsoft Active Directory, OpenLDAP, and FreeIPA.
Configuration¶
[authentication.ldap]
server_url = "ldaps://ldap.example.com:636"
bind_dn = "cn=service,ou=services,dc=example,dc=com"
bind_password = "${LDAP_BIND_PASSWORD}"
user_search_base = "ou=users,dc=example,dc=com"
user_search_filter = "(sAMAccountName={0})" # AD; use "(uid={0})" for OpenLDAP
group_member_attribute = "memberOf"
sid_attribute = "objectSid"
display_name_attribute = "displayName"
email_attribute = "mail"
timeout_seconds = 10
pool_size = 5
follow_referrals = false
# Map AD groups to IndentiaDB roles
[authentication.ldap.group_role_mapping]
"CN=IndentiaAdmins,OU=Groups,DC=example,DC=com" = "admin"
"CN=DataScientists,OU=Groups,DC=example,DC=com" = "writer"
"CN=Readers,OU=Groups,DC=example,DC=com" = "reader"
# Map AD groups to Windows SIDs for triple-level ACL
[authentication.ldap.group_sid_mapping]
"CN=Finance,OU=Groups,DC=example,DC=com" = "S-1-5-21-domain-2001"
"CN=HR,OU=Groups,DC=example,DC=com" = "S-1-5-21-domain-2002"
"CN=Engineering,OU=Groups,DC=example,DC=com" = "S-1-5-21-domain-2003"
"CN=Executive,OU=Groups,DC=example,DC=com" = "S-1-5-21-domain-2004"
How LDAP Authentication Works¶
- Client sends
Authorization: Basic base64(username:password)header. - IndentiaDB takes a connection from the pool and performs a bind with the service DN.
- A search with
user_search_filter(substituting{0}with the supplied username) retrieves the user's DN and group memberships. - IndentiaDB attempts a second bind with the user's DN and supplied password.
- If the bind succeeds, the
memberOfattribute is read to determine group membership. - Groups are mapped to roles via
group_role_mappingand to SIDs viagroup_sid_mapping. - The connection is returned to the pool.
Windows SID Support (objectSid)¶
Active Directory stores SIDs as binary objectSid attributes. IndentiaDB automatically parses the binary SID format into the standard string representation (S-1-5-21-<domain>-<RID>). This allows triple-level ACL annotations to reference AD group SIDs directly.
If the user's own objectSid attribute is present, it is also included in the principal's SID set alongside group SIDs.
Connection Pooling¶
pool_size controls how many pre-established LDAP connections IndentiaDB maintains. Each authentication request borrows a connection from the pool, performs the bind verification, and returns it. Under high authentication load, requests queue until a connection is available.
Pool Size Tuning
Set pool_size to slightly above your peak concurrent authentication requests. A pool of 5–10 is sufficient for most deployments. Too-large pools waste AD server resources.
LDAPS (TLS)¶
Use ldaps:// (port 636) for encrypted connections. The server's TLS certificate is validated against the system trust store. For self-signed or internal CA certificates:
[authentication.ldap]
server_url = "ldaps://ad.internal.example.com:636"
ca_cert_file = "/etc/indentiadb/certs/internal-ca.pem"
OpenID Connect (OIDC) / Keycloak¶
OIDC is the recommended authentication provider for modern deployments. IndentiaDB acts as an OIDC Resource Server — it validates JWT access tokens issued by the OIDC provider.
Configuration¶
[authentication.oidc]
enabled = true
issuer_url = "https://auth.example.com/realms/myrealm"
client_id = "indentiadb-client"
audience = "indentiadb-api"
roles_claim = "realm_access.roles"
sids_claim = "groups"
# user_sid_claim = "employee_sid" # optional; see "User SIDs" below
# jwks_url = "https://auth.example.com/keys" # optional; skips discovery
jwks_refresh_interval_secs = 3600
http_timeout_secs = 10
[authentication.oidc.role_mapping]
"realm-admin" = "admin"
"realm-writer" = "writer"
"realm-reader" = "reader"
How OIDC Authentication Works¶
- Client obtains a JWT access token from the OIDC provider (e.g., via Authorization Code flow, Client Credentials, or Device Authorization Grant).
- Client sends
Authorization: Bearer <token>with every request to IndentiaDB. - IndentiaDB locates the OIDC provider's JWKS (JSON Web Key Set) through the discovery document at
{issuer_url}/.well-known/openid-configuration— or directly atjwks_urlwhen that is configured — on first request. The JWKS is cached and refreshed everyjwks_refresh_interval_secs. - The JWT signature is validated against the matching key in the JWKS.
- Standard claims are verified:
iss(must matchissuer_url),aud(must containaudience),exp(must be in the future). - Roles are extracted from
roles_claim(e.g.,realm_access.rolesfor Keycloak) and mapped viarole_mapping. - The principal's user SID is taken from
user_sid_claimor derived from the verified issuer and subject (see User SIDs). Group SIDs are extracted fromsids_claim(e.g., AD group SIDs embedded as a claim by the OIDC provider) and added to the principal's SID set.
Issuer Endpoints¶
All issuer, discovery, and JWKS URLs must use HTTPS. Two options cover issuers that do not fit the standard discovery flow:
| Option | Default | Effect |
|---|---|---|
jwks_url |
unset | Explicit JWKS endpoint. When set, discovery is skipped entirely — use it for issuers that publish signing keys but no /.well-known/openid-configuration. Tokens are still validated against issuer_url, and the URL is subject to the same transport rules. |
allow_insecure_cluster_http |
false |
Permits plain HTTP for this provider's issuer and JWKS endpoints only when the host is a cluster-local service name (*.svc or *.svc.cluster.local), which never resolves outside the pod network. Public hosts stay HTTPS-only whether or not the option is set. Refused under IG_PRODUCTION: production cluster-local issuers must use HTTPS. |
User SIDs¶
Every authenticated OIDC principal gets a stable user SID, so per-user triple-level ACL grants keep resolving to the same person across logins:
- If
user_sid_claimis set and the token carries that claim, its value is the user SID — either an SDDL SID (S-1-5-21-…) or a principal label such asuser:jan@example.com. - Otherwise the user SID is the principal label
user:id:<iss>|<sub>, built from the token's verified issuer and subject. Subjects are only unique per issuer, so including the issuer keeps users of different identity providers apart.
A grant for one OIDC user therefore looks like:
@prefix security: <http://indentiagraph.io/security#> .
@prefix ex: <http://example.org/> .
<< ex:report ex:salary "82000" >>
security:allowedSid "user:id:https://auth.example.com/realms/myrealm|4f1b9c2e-8d7a-4e21-9b3f-0c6d5e7a8b91" .
When an ACL context needs a user SID for a principal that has none, IndentiaDB falls back to a synthetic SID derived from a hash of the principal id: S-1-5-21-synthetic-<8 hex digits>-<rid>, with the high 32 bits of the hash in the domain and the low 32 bits as the RID. (Earlier releases dropped the high bits, so synthetic SIDs from before this format no longer match.) Set IG_ACL_SYNTHETIC_SID_SALT to key them per deployment, and keep the value constant once set — changing it re-keys every synthetic SID. Prefer grants on principal labels or directory SIDs over synthetic SIDs.
Multiple Issuers and Subject ACLs¶
Additional trusted issuers are configured as [[authentication.additional_oidc]] entries with the same fields as [authentication.oidc]; a token from any listed issuer is accepted against that issuer's own keys.
Issuer selection uses the token's verified issuer configuration. A token from an additional issuer is not validated against the primary issuer's key set. Keep each issuer's audience, claim mapping, and tenant binding explicit.
Delegated SIDs for a trusted service¶
On the primary API, only an explicitly configured service token with the
sid-delegator role may supply per-request SIDs through X-Indentia-Sid.
The first valid value is the user SID; subsequent values are group SIDs.
This replaces that request's SID set, while the token's tenant and operation
permissions remain authoritative. It is a trust grant to the service.
OIDC/JWT callers and registry-backed machine identities cannot acquire this
behavior by supplying the header or using the same role name. Ordinary service
tokens retain their configured principals. The separate :9200 identity-proxy
contract also requires its configured proxy token; see
Elasticsearch authentication.
Because an OIDC subject is only unique within its issuer, a subject-specific ACL entry for an OIDC principal must name the verified issuer with security:identityIssuer in the ACL file ([acl] config_file):
@prefix access: <http://indentiagraph.io/access#> .
@prefix security: <http://indentiagraph.io/security#> .
access:alice
access:graphs ( <https://id.indentia.ai/sources/acme/finance> ) ;
security:identityIssuer "https://auth.example.com/realms/myrealm" .
- The entry applies only to tokens whose verified
issequals the declared issuer. A token for the same subject from another issuer does not match it and is denied, rather than falling back to role or default graph access. - An entry without
security:identityIssuerwhose name matches an OIDC subject now denies that principal instead of granting access. Add the issuer to existing subject entries when you upgrade. - Each actor entry may declare exactly one issuer; role entries cannot declare one. Entries for LDAP users and service tokens are unaffected.
Supported Signing Algorithms¶
| Algorithm | Type | Notes |
|---|---|---|
RS256 |
RSA PKCS#1 v1.5, SHA-256 | Most common; Keycloak default |
RS384 |
RSA PKCS#1 v1.5, SHA-384 | — |
RS512 |
RSA PKCS#1 v1.5, SHA-512 | — |
ES256 |
ECDSA P-256, SHA-256 | Smaller tokens, faster verification |
ES384 |
ECDSA P-384, SHA-384 | — |
PS256 |
RSA-PSS, SHA-256 | — |
PS384 |
RSA-PSS, SHA-384 | — |
PS512 |
RSA-PSS, SHA-512 | — |
EdDSA |
Ed25519 | Fastest verification; used by newer providers |
HS256 Not Supported
HMAC-based JWT algorithms (HS256, HS384, HS512) are not supported because they require sharing the signing secret with IndentiaDB, which violates the principle of the OIDC provider being the sole issuer. Use RS256 or ES256.
Keycloak Setup¶
Configure IndentiaDB as a client in Keycloak:
- Create a client with
Client Protocol: openid-connectandAccess Type: bearer-only. - Set
Valid Redirect URIsto*(not required for bearer-only clients). - Add a mapper:
Token Claim Name: realm_access.roles,Claim JSON Type: JSON. - If using AD group SIDs, configure a
Group Membershipmapper with claim namegroups.
Fetching a Token for Testing¶
# Client Credentials (service-to-service)
TOKEN=$(curl -s -X POST \
"https://auth.example.com/realms/myrealm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=my-service" \
-d "client_secret=$CLIENT_SECRET" \
| jq -r '.access_token')
# Use the token
curl -X POST http://localhost:7001/sparql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/sparql-query" \
-d 'SELECT * WHERE { ?s ?p ?o } LIMIT 10'
JWT Bearer Tokens¶
For service accounts and programmatic access, IndentiaDB can validate JWTs issued by any standard-compliant OIDC provider or by an internal token service. Configure as OIDC above — JWT bearer authentication is the underlying mechanism used by the OIDC integration.
For self-issued JWTs (e.g., tokens issued by your own backend):
[authentication.jwt]
algorithm = "RS256"
public_key_file = "/etc/indentiadb/certs/jwt-public-key.pem"
audience = "indentiadb-api"
issuer = "https://my-service.example.com"
roles_claim = "roles"
sids_claim = "sids"
JWT Claim Structure¶
IndentiaDB reads the following claims from the JWT:
| Claim | Required | Description |
|---|---|---|
sub |
Yes | Subject (user identifier) |
iss |
Yes | Issuer (validated against config) |
aud |
Yes | Audience (validated against config) |
exp |
Yes | Expiry timestamp |
iat |
No | Issued-at timestamp |
Configured roles_claim |
No | Array of role strings |
Configured sids_claim |
No | Array of SID strings |
Service Tokens¶
Service tokens are tenant-scoped, file-backed bearer credentials for machine identities — monitoring agents, reconciliation loops, internal services — that should not depend on an external OIDC provider. Unlike JWTs they are opaque: no claims, no signature, no expiry. Identity, roles, and tenant are bound to the token in server configuration, not carried in the token itself.
Token Format¶
A service token is the prefix igsvc_ followed by exactly 64 lowercase hex characters (32 random bytes):
Configuration¶
[[authentication.service_tokens]]
enabled = true
subject = "prometheus"
token_file = "/etc/indentiagraph/tokens/prometheus" # absolute path required
roles = ["reader"]
tenant = "platform-monitoring"
# principals = ["user:prometheus"] # optional ACL identities
Validation is strict and happens before the HTTP listener binds: the token file must exist and contain a well-formed token, duplicate subjects and duplicate token material are rejected, and the tenant must be a valid tenant slug. A misconfigured entry fails startup rather than silently degrading.
Usage¶
- Tokens are compared in constant time. An unknown
igsvc_-prefixed token fails authentication outright — it never falls through to other providers. Non-igsvc_bearer tokens are passed on to OIDC/JWT as usual, so both can coexist. - The tenant is bound to the credential: the configured tenant is stamped onto the authenticated principal, and no request header can override it. It is authoritative for tenant-scoped authorization; see Service Token Tenants and Principals, which also covers the optional
principalslist. - A configured service-token provider satisfies the "at least one authentication provider" requirement of the
IG_PRODUCTIONboot gate.
Rotation¶
Service tokens have no built-in expiry. Rotate operationally: replace the token file (or the mounted Kubernetes Secret) and restart the server so the identity re-resolves. Keep one identity per consumer so a rotation affects exactly one client.
HTTP Basic Auth¶
Basic Auth is supported for backward compatibility and development convenience. Credentials are sent as Authorization: Basic base64(username:password).
Use Only for Development
HTTP Basic Auth transmits credentials with every request. Without TLS, credentials are exposed in plaintext. In production, always use OIDC/JWT with HTTPS, or LDAP over LDAPS. Basic Auth is suitable only for local development on localhost.
[authentication.basic]
enabled = true
users = [
{ username = "dev-admin", password_hash = "$argon2id$v=19$m=65536,t=3,p=4$...", roles = ["admin"] },
{ username = "dev-reader", password_hash = "$argon2id$v=19$m=65536,t=3,p=4$...", roles = ["reader"] },
]
Passwords are stored as Argon2id hashes. Generate a hash:
indentiagraph auth hash-password --password "mysecretpassword"
# Output: $argon2id$v=19$m=65536,t=3,p=4$...
CLI Authentication¶
The IndentiaDB CLI supports OIDC Device Authorization Grant (device flow) for interactive logins — no browser redirect required for CLI use.
OIDC Device Login¶
indentiagraph auth login \
--profile dev \
--endpoint http://localhost:7001 \
--issuer https://auth.example.com/realms/main \
--client-id indentiagraph-cli \
--scope "openid profile email offline_access" \
--default
The CLI prints a verification URL and code:
Open https://auth.example.com/realms/main/device in your browser
Enter code: ABCD-EFGH
Waiting for authorization...
✓ Logged in as alice@example.com (roles: admin)
Profile 'dev' saved.
--default marks this profile as the default for subsequent CLI commands.
List Profiles¶
NAME ENDPOINT USER EXPIRES
dev * http://localhost:7001 alice@example.com 2025-11-15T10:30:00Z
staging http://staging:7001 alice@example.com 2025-11-15T08:00:00Z
prod http://prod-lb:7001 svc-deploy@example.com never (client_credentials)
Get Current Token¶
Prints the raw JWT access token to stdout. Useful for scripting:
TOKEN=$(indentiagraph auth token --profile dev)
curl -H "Authorization: Bearer $TOKEN" http://localhost:7001/sparql ...
Refresh Token¶
Uses the stored refresh token to obtain a new access token from the OIDC provider. The profile is updated with the new tokens. The CLI automatically refreshes expired tokens before each command.
Rate Limiting (Brute Force Protection)¶
IndentiaDB rate-limits failed authentication attempts per client IP and per attempted username to slow down brute-force attacks. The defaults are:
[authentication.rate_limit]
max_attempts_per_ip = 10 # failed attempts from one IP before lockout
max_attempts_per_user = 5 # failed attempts against one username before lockout
lockout_duration_secs = 300 # lockout duration (5 min)
Behavior:
- Repeated failures from the same IP or against the same username impose an increasing delay before the next attempt is accepted: 2 seconds after the second failure, doubling with each further failure, capped at 5 minutes.
- Reaching max_attempts_per_ip or max_attempts_per_user locks that IP or username out for lockout_duration_secs.
- Rate-limited requests return 429 Too Many Requests with error rate_limited and a Retry-After header.
- A successful authentication resets the counters for that IP and username.
- Lockouts are recorded as RateLimitViolation audit events.
- Limiter state is bounded: idle entries that are not locked out are swept periodically, each table (IPs, usernames) holds at most 100,000 entries — evicting the oldest first and active lockouts last — and username hints are truncated to 256 bytes, so a flood of distinct keys cannot grow memory without bound.
Per-Node State
Rate-limit state is kept in-process. In a multi-node Raft cluster each node tracks its own failure counts.
HTTP Error Codes¶
| Code | Meaning | Common Causes |
|---|---|---|
401 Unauthorized |
Authentication required or failed | Missing Authorization header; expired JWT; invalid Basic Auth credentials; LDAP bind failure |
403 Forbidden |
Authenticated but not authorized | Role has Read permission but request needs Write; ACL blocks access to the requested graph |
429 Too Many Requests |
Rate limit exceeded | Too many failed login attempts from this IP |
The 401 response includes a WWW-Authenticate header indicating the supported schemes: