Documentation
Security
Written for security, risk and compliance teams. The trust model, how isolation and per-file access control are enforced, and the controls protecting data in transit, at rest and in use.
Security overview
The trust model in one page.
- Self-hosted in your own Kubernetes cluster. Data never leaves your boundary, and the product does not phone home.
- Tenant isolation and per-file access control are enforced in the database with native row-level security, not in application code.
- Memory-safe core written in Rust, eliminating entire classes of memory-corruption vulnerability.
- Identity-first. Every caller is a named principal, authenticated through your own identity provider over OIDC or OAuth 2.1.
- Auditable. Every operation is logged with the acting principal, tenant and resource.
- Continuously scanned. A rolling daily security report publishes vulnerability findings across every shipped container image.
Security architecture
K-Lake runs entirely inside your infrastructure. The components are the API, the web console, a fleet of stateless workers, an optional MCP server, and a managed database. Your identity provider and your content sources stay under your control: K-Lake reads from sources read-only and federates sign-in to your IdP.
Improvements on prior approaches
K-Lake was designed to close gaps common in conventional indexing and enterprise-search products.
| Concern | Conventional approach | K-Lake approach |
|---|---|---|
| Access control | Filtering applied in application code — one missed WHERE clause leaks data | Row-level security in the database, enforced on every query, fail-closed by default |
| Multi-tenancy | Shared tables with application-side tenant filters | Tenant isolation enforced by row-level security in the database |
| Memory safety | C and C++ connectors prone to buffer overflows and use-after-free | Rust — memory-safe by construction, no garbage collector |
| Identity | Bolt-on auth or shared service accounts | Native per-user OIDC and OAuth 2.1, federated to your IdP |
| AI access | Bulk export of data to a model | MCP with per-user trimming and citations — data stays in your cluster |
| Deployment | SaaS; data leaves your boundary | Self-hosted in your cluster, air-gap capable, no phone-home |
Memory-safe foundation
The core services and every source connector are written in Rust. This is a deliberate security decision.
- Memory safety by construction. Rust's ownership model eliminates buffer overflows, use-after-free and data races at compile time — the vulnerability classes behind a large share of CVEs in systems software.
- No garbage collector, so performance is predictable, resource cleanup is deterministic, and there is no GC-pause attack surface.
- Strong typing and exhaustive error handling reduce the logic errors that lead to unsafe states.
Multi-tenant isolation
Every source, user, token and indexed file belongs to exactly one tenant. Isolation is not a matter of careful application coding — it is enforced by the database.
Each request runs in a database session bound to its authenticated tenant and principal. Row-level security policies attached to every table restrict visibility to that tenant's rows. A bug in application code cannot widen this boundary, because the boundary lives below the application.
Authorisation and security trimming
Beyond tenant isolation, K-Lake enforces per-file access control. A caller sees a file only if the file's own source-native permissions — NTFS or NFSv4 ACLs, POSIX ownership, S3 policy — grant their identity access. Native principals are correlated to your IdP identities by directory enrichment, then enforced, again, by row-level security at the database layer.
- The client presents an OIDC bearer token or a personal access token.
- The API validates the token with your identity provider and resolves the identity.
- The IdP returns a verified principal and its group memberships.
- The API queries in a database session bound to that tenant and principal.
- Row-level security filters by tenant and per-file grants.
- Only authorised rows are returned — and every access is audited.
Because trimming is enforced where the data lives, the same guarantee applies uniformly across the console, CLI, REST API and AI access over MCP. There is no admin code path that bypasses it.
Application-layer filtering has to be applied correctly in every code path that touches data; one oversight leaks records. Row-level security inverts this. Access is denied by default and granted only by explicit policy, so new code paths inherit the controls automatically and fail closed.
Identity and access management
- Federated sign-in. Users authenticate through your own identity provider over OIDC and OAuth 2.1, and SAML where applicable. K-Lake does not become a parallel identity store — it consumes verified identities from providers such as Microsoft Entra ID, Okta, Google or Keycloak.
- Personal access tokens. For programmatic and CLI use, scoped tokens are minted per user, shown once and revocable.
- AI access over MCP. The MCP server is a full OAuth 2.1 authorisation server, following the relevant RFCs for protected resources and audience restriction. Assistants connect as the individual user, inherit that user's per-file trimming, and receive a citation for every fact — so answers are grounded and verifiable, and nothing is exported to the model provider in bulk.
Encryption
| Layer | Control |
|---|---|
| In transit | TLS for all client-to-service traffic, and TLS for service-to-database connections. |
| At rest — secrets | Source credentials, IdP client secrets and tokens are encrypted with authenticated encryption before they reach the database, using a master key held outside the API process. |
| At rest — datastore | Deployable on encrypted volumes for full at-rest encryption of the database and its backups. |
| Key handling | Keys are supplied through Kubernetes Secrets or an external secret manager. Certain operations require the master key to be held by the worker rather than the API, narrowing exposure. |
Credentials are never written to logs or shell history — secrets are read from stdin or the environment, never from command-line flags.
Auditability and observability
- Audit trail. Every privileged operation — sign-in, source change, crawl, search, file open, token mint and AI query — is recorded with the acting principal, tenant and resource, supporting forensic review and access reporting.
- Structured logs and metrics. Services emit structured logs and Prometheus-compatible metrics that integrate with your existing SIEM and monitoring stack. All of it is scraped and stored by you, inside your boundary — see Telemetry.
- Verifiable retrieval. Every search result and AI citation links back to the original file and re-checks the caller's permissions at open time, so an auditor can confirm exactly where any answer came from.
Supply chain and vulnerability management
- Daily security report. A rolling daily report publishes automated vulnerability scan results across every shipped container image. [Mirror URL under zegress.ai to be inserted before publishing.]
- Signed images. Release images are cryptographically signed, so you can verify provenance and integrity before deployment.
- Minimal images. Containers ship only what each service needs, reducing both attack surface and patch burden.
Deployment and data residency
- Runs in your cluster. K-Lake deploys into your own Kubernetes environment; all content and metadata stay within your boundary.
- Air-gap capable. K-Lake can run with no internet egress at all, including fully on-premise AI retrieval over MCP, so regulated and disconnected environments are first-class.
- Offline licensing. Licensing is enforced offline. There is no licence-server call-home.
Data protection and compliance
Zegress.Ai is registered with the UK Information Commissioner's Office under registration {{ICO_REGISTRATION}}.
Because K-Lake is self-hosted in your own infrastructure, your content and metadata remain under your control and within your data-residency boundary. Zegress does not process or receive your indexed data.
The ICO registration placeholder must be replaced with Zegress.Ai Limited's own registration once incorporation and registration are complete. Do not substitute a third party's registration number. This paragraph should also be reviewed against the customer DPA — a reseller in the supply chain changes what a compliance reader needs to be told about controllers and processors.
Reporting a vulnerability
If you believe you have found a security issue, email [email protected] or contact your Zegress representative. Please don't disclose publicly until we have confirmed a fix.
Security trimming
Per-file access control, enforced where the data lives.
Per-file security trimming means a caller sees only the files their source-native permissions already grant them — the file's own owner, group or ACL. Someone who can't read a file on the NAS can't see it, its metadata or its extracted content through K-Lake either.
Enforcement lives in row-level security, so it applies everywhere automatically: listings, file detail and content search are trimmed by the same predicate. There is no separate "filter the search results" code path to keep in sync.
Trimming is fail-closed and strictly opt-in. A source you don't configure for it keeps source-level visibility. A source you do configure hides any file whose grants haven't been computed yet — K-Lake would rather hide a file you could read than leak one you can't.
How it works
Both halves of the question — who is this caller, and who may read this file — collapse into one namespace, the principal ref, of the form <kind>:<scope>:<value>. Each distinct ref is interned to an integer principal ID.
- File side. When a file is indexed, K-Lake derives the set of principal refs that may read it from its POSIX mode bits and, if captured, its ACL, then records a read-grant for each granted principal.
- Caller side. When a caller hits the API, their IdP claims — object ID, email, UPN, group memberships — are turned into principal refs, resolved to a principal ID set and handed to the datastore.
A file is authorised for a caller when the two sets intersect. The same predicate applies to listings, metadata and content, ANDed on top of the existing tenant and source-visibility checks. It short-circuits in this order:
- Tenant admin, or source owner or editor — unrestricted admin path.
- Source not in
per_filemode — source-level visibility applies. - File is world- or everyone-readable (
grants_state = 2) — visible. - Caller holds a read grant on the exact file — visible.
fail_closedis off and grants aren't computed yet — visible, the fail-open fallback.- Otherwise — hidden.
Steps 1 and 2 are the source-visibility gate: a caller must be a tenant admin or hold a source access grant — owner, editor or viewer — before the source is visible at all. Steps 3 to 6 are per-file trimming within a visible source. This section covers the second gate; manage the first through the Access grants card, zegress-control acl, or /api/sources/:id/acl.
Principal-ref kinds
| Kind | Scope | Origin |
|---|---|---|
sid | AD / domain, often empty | NTFS owner or ACE SID |
upn | none | User principal name — the cross-directory bridge key |
email | none | Email or Google Workspace group address, lowercased |
oid | issuer | Entra object GUID, user or group |
posixuid | source ID | Numeric POSIX owner — source-scoped, since UID spaces are server-local |
posixgid | source ID | Numeric POSIX group — source-scoped |
name | directory | Resolved friendly user or group name |
nfs4who | source ID | NFSv4 who-string, before resolution |
The same ref often appears on both sides — a POSIX group on the file and the caller's resolved group — and that's the common case. When the two sides are in different namespaces, such as an on-premise AD SID on the file against an Entra group object ID in the token, they're bridged by directory enrichment, which builds the alias graph.
Enabling it on a source
The trim policy lives in a security_trim block inside the source's config, read by both the workers and the authorisation predicate.
| Field | Values | Default | Meaning |
|---|---|---|---|
mode | per_file · source_only · open | source_only | per_file turns on per-file trimming. source_only keeps source-level visibility. open explicitly marks the source fully public, even if other sources in the tenant trim. An absent block or unrecognised value falls back to source_only. |
fail_closed | true · false | true | Under per_file, decides what happens to files whose grants aren't computed yet: hide them, or fall back to source-level visibility. |
Set it through the dedicated endpoint, which merges the block into the source config without disturbing anything else. The change takes effect within about 30 seconds.
zegress-control --api-url "$ZEGRESS_URL" --api-token "$ZEGRESS_TOKEN" \
source security-trim --source-id 'smbfs://nas.corp/finance' \
--mode per_file --fail-closed true
--fail-closed is optional — omit it to change the mode while leaving the stored value untouched.
curl -X POST -H "Authorization: Bearer $ZEGRESS_TOKEN" \
-H "Content-Type: application/json" \
"$ZEGRESS_URL/api/sources/<urlencoded-source-id>/security-trim" \
-d '{ "mode": "per_file", "fail_closed": true }'
The response echoes the stored policy. Authorisation is tenant-admin or the source's owner or editor; an unauthorised caller gets 404 rather than 403, because the row isn't visible under row-level security in the first place. In the console, the source detail page has a Per-file security trimming panel that posts to the same endpoint.
Prerequisite for ACL-based trimming
POSIX grants are derived automatically from the mode, UID and GID the crawl already captures. ACL grants are not. The source must first capture the raw ACL bytes through a metadata-capture cap — ntfs_acl for SMB and SMBFS, nfs4_acl for NFS. Without the cap there are no ACL bytes to derive grants from, and an ACL-controlled file falls back to its POSIX bits, commonly leaving grants_state = 0 and therefore hidden under fail-closed.
If a backup or file-gateway product stamped the file's original Windows security descriptor into blob metadata, K-Lake reads it from there — no ntfs_acl capture cap is needed, and NTFS ACL trimming works on the cloud copies. If the descriptor can't be parsed, the file fails closed.
zegress-control --api-url "$ZEGRESS_URL" --api-token "$ZEGRESS_TOKEN" \
source meta-caps --source-id 'smbfs://nas.corp/finance' --caps ntfs_acl
# NFS export
zegress-control --api-url "$ZEGRESS_URL" --api-token "$ZEGRESS_TOKEN" \
source meta-caps --source-id 'nfs://nas.corp/export/data' --caps nfs4_acl
The smb protocol has no ACL surface and ntfs_acl is masked out for it at registry time. Use the smbfs kernel-mount backend if you need NTFS ACL capture.
After enabling a cap on a source that's already indexed, run a backfill so existing files get their ACL bytes, and therefore their grants, computed:
zegress-control source backfill-meta --source-id 'smbfs://nas.corp/finance'
POSIX and ACL grants
K-Lake derives read-grants from whichever permission model the source exposes. Each cap that produced a grant is tracked against that grant, so re-crawling one model's grants never clobbers another's.
POSIX — automatic
| Mode bit | Octal | Effect |
|---|---|---|
| other-read | 0o004 | File is world-readable — grants_state = 2, no per-principal rows needed |
| owner-read | 0o400 | Grant to posixuid:<source_id>:<uid> |
| group-read | 0o040 | Grant to posixgid:<source_id>:<gid> |
ACL — requires the cap
Derivation is deliberately conservative: it emits only grants it is certain of, and any parse error or malformed field yields no grants at all.
- NFSv4 (
nfs4_acl, RFC 7530 §5.11 wire format). Allow-read ACEs become grants.OWNER@andGROUP@resolve to the file's POSIX owner and group principals; named whos becomenfs4whorefs for directory enrichment to alias later. AnEVERYONE@allow-read marks the file world-readable, and a deny-read onEVERYONE@yields no grants. - NTFS (
ntfs_acl, parsed from the self-relative security descriptor, MS-DTYP §2.4.6). DACL allow-read ACEs becomesid::<SID>grants, and the well-known Everyone SIDS-1-1-0marks the file world-readable. Deny-overrides-allow is resolved at compute time, so reads stay a positive set-intersection.
Grants state and fail-closed behaviour
Each file's grants_state records whether grants have been computed and what they say. It's how the read-time predicate distinguishes "no grants yet" from "grants present" without scanning the full grant set on every hit.
| State | Meaning | Visible under per_file? |
|---|---|---|
0 | No per-file grants computed — uncomputed, or no metadata captured yet | Hidden when fail_closed = true; falls back to source-level visibility when false |
1 | Grants computed and present | Visible only to callers holding a matching grant |
2 | Grants computed; file is world- or everyone-readable | Visible to everyone with source access |
The default for every new file is 0. So the moment you flip a source to per_file, its files trim down to nothing until grants are computed. That is exactly the fail-closed guarantee working as intended — but it means you should enable the capture cap and backfill before relying on the source being usefully visible.
Correlating callers to files
POSIX trimming works out of the box when the caller's IdP groups resolve to the same names or IDs as the file owners. Cross-directory cases — an on-premise AD SID on a Windows ACL against an Entra object ID in an OIDC token, or a numeric POSIX GID against a name-based IdP group — need the two namespaces bridged.
That bridging is directory enrichment. It materialises the alias graph from declared operator mappings, POSIX name resolution, Entra Graph discovery including group-membership and overage resolution, and AD/LDAP correlation for unlinked on-premise directories. Only high-confidence alias edges expand grants, so a fuzzy match can never produce a false grant.
Limitations and management surface
- Set through the console, CLI or API. The Per-file security trimming panel, the
source security-trimcommand and thePOST /sources/:id/security-trimendpoint all write the same config block. - ACL trimming needs the capture cap first. Enable
ntfs_aclornfs4_acland backfill before trusting ACL-controlled files to be visible. POSIX-only trimming needs no cap. - Cross-directory trimming needs directory enrichment. Without the alias graph, only same-namespace grants — typically POSIX — will match.
- Fail-closed means hidden until computed. Plan a backfill as part of any cutover to
per_file. - The trim layer is consumed by both the API and MCP. Both resolve the caller's principal set into the same authorisation context, so a caller sees the same trimmed view through either surface.
Directory enrichment
Tying caller identities to file identities, so trimming can decide.
When a caller signs in, your identity provider hands K-Lake a set of claims: an object ID, an email, a UPN, some group IDs. When K-Lake indexes a file, the file carries a different kind of identity entirely — a POSIX UID or GID, or an NTFS and Active Directory SID. These two namespaces rarely match on their own. Directory enrichment ties them together, so security trimming can decide whether a given caller may see a given file.
The correlation problem
A caller's token yields refs like oid:<issuer>:<guid> and upn:[email protected]. A file's ACL yields refs like sid::S-1-5-21-… or posixgid:<source>:5000. Trimming authorises a caller for a file only when the caller's refs and the file's grant refs land on the same principal. Directory enrichment's job is to discover the edges that make that true — that an Entra group is the on-premise SID, that a POSIX group is a named directory group.
Every identity, caller-side or file-side, is normalised to a principal ref of the form <kind>:<scope>:<value>. Emails and names are lowercased; SIDs are globally unique, so they use the empty scope.
The alias graph and fail-closed confidence
Correlation is stored as an alias graph — bidirectional edges between two interned principals. At trim time, K-Lake takes the caller's refs and expands them across the graph to reach every principal they are equivalent to, then checks those against the file's grants.
| Confidence | Meaning | Expands grants? |
|---|---|---|
high | Exact identity key — SID to object ID, rewritten UPN, mail, or a declared mapping | Yes |
medium | Fuzzy fallback, such as a sAMAccountName or display-name match | No — recorded only |
Only high-confidence edges feed grant expansion. A medium edge is materialised and visible, but never authorises anyone on its own. An operator who has verified a fuzzy correlation can promote it — but nothing fuzzy grants access by default.
Correlation strategies
K-Lake builds the alias graph from four edge sources. Use any combination; pick by what your environment can offer.
| Strategy | Bridges | When to use |
|---|---|---|
| Declared mappings | Any ref to any ref | You know specific equivalences and want them deterministic and immediate |
| POSIX names | posixuid / posixgid to name:<dir>:<…> | NFS and POSIX sources where a name-based IdP such as Okta or Keycloak emits group names |
| Entra Graph | oid to sid | File ACL SIDs and caller Entra groups belong to the same, or an AD-synced, tenant |
| AD / LDAP | sid to upn, email or name | The file server's AD is a separate, unlinked directory from the cloud IdP |
Declared mappings
The deterministic, immediately usable path. You state from/to/confidence triples in the tenant's principal mappings; each becomes a bidirectional edge, high-confidence unless you mark it medium. Good for one-off equivalences you already know, or for promoting a correlation you've verified by hand.
POSIX names
For per-file-trimmed NFS and POSIX sources, K-Lake resolves the UID and GID set captured during crawling to names — via the worker host's name-service resolution, for instance on an AD-joined worker — and links the POSIX group ref to the named group ref a name-based IdP produces. This runs automatically; there's no separate command.
Entra Graph discovery
For Entra ID, K-Lake uses Microsoft Graph with app-only client credentials to discover three kinds of edge:
- Groups. Each group's object ID is aliased to its security identifier and, when the group is AD-synced, its on-premise security identifier. Whichever SIDs are present become edges to the caller's group ref. This is how a file ACL'd to a group resolves to a caller's group membership.
- Users. Each synced user's on-premise security identifier is aliased to their UPN or email ref, bridging files a user owns to the caller's UPN. Because it reads the already-correct cloud UPN straight from Graph, no UPN rewrite is needed for a properly synced tenant.
- Membership, and overage resolution. For each SID-bearing group, K-Lake reads its transitive user members and adds a directed member-to-group edge. This handles JWT group overage: when a user belongs to more than roughly 200 groups, Entra omits the groups claim from the token — but the caller's UPN ref still reaches their groups' grants through these pre-synced edges, with no per-request Graph call. The edges are directed, so two members of the same group never gain access to each other's personal files.
The app registration needs the Graph application permissions Group.Read.All, GroupMember.Read.All and User.Read.All — all read-only — plus admin consent.
This works when the SIDs on the files belong to the same Entra tenant, or its AD-synced on-premise side. It does not bridge a SID living in a directory Entra has never seen.
AD and LDAP correlation for unlinked directories
The common hard case: the file server's AD is a separate directory from a cloud-only Entra tenant. The two share no SIDs and use different UPN suffixes, so Graph SID-bridging alone cannot connect them — Entra has never heard of the on-premise SID.
LDAP correlation solves it. K-Lake binds to the on-premise domain controller over LDAPS, enumerates users and groups, and aliases each AD principal's object SID to the caller-side ref the Entra token actually carries. The key setting is a UPN-suffix rewrite: an on-premise UPN is rewritten to the cloud UPN, producing a high-confidence edge. The mail attribute, where present, is likewise high confidence. A sAMAccountName match is a medium fallback — recorded, never granting on its own.
Because the worker binds over LDAPS, the deployment must trust the domain controller's CA in the worker's trust store.
Choosing a strategy
- Same or AD-synced Entra tenant — Entra Graph.
- Separate, unlinked on-premise AD against a cloud-only Entra tenant — AD/LDAP with a UPN rewrite, or declared mappings for the handful of groups you care about. Graph alone will not connect them.
- NFS or POSIX with a name-based IdP — POSIX names, which run automatically, plus declared mappings for anything names don't cover.
Command line
All directory commands run in direct-database mode. They need --postgres-url or ZEGRESS_POSTGRES_URL and, where secrets are involved, ZEGRESS_MASTER_KEY. They are not proxied through the API, because the API process does not hold the master key. Secrets are always read from an environment variable, never a flag — a deliberate choice so credentials stay out of argv, shell history and logs.
Discover Entra groups once
Runs Graph discovery immediately and materialises all three edge sets without storing any config. Use it to validate credentials or do a one-shot sync.
export ZEGRESS_GRAPH_CLIENT_SECRET='<app-client-secret>'
zegress-control --postgres-url "$ZEGRESS_POSTGRES_URL" \
directory sync-graph \
--tenant <tenant-slug> \
--entra-tenant <entra-tenant-id> \
--client-id <app-registration-client-id>
Store Entra config for automatic discovery
Stores the non-secret config and encrypts the client secret at rest. After this the worker runs Graph discovery automatically — you don't run sync-graph again.
export ZEGRESS_GRAPH_CLIENT_SECRET='<app-client-secret>'
export ZEGRESS_MASTER_KEY='<base64-master-key>'
zegress-control --postgres-url "$ZEGRESS_POSTGRES_URL" \
directory set-graph \
--tenant <tenant-slug> \
--entra-tenant <entra-tenant-id> \
--client-id <app-registration-client-id>
Store AD/LDAP config
Stores the non-secret config and encrypts the bind password at rest. --name-scope sets the scope for name fallback refs, typically the NetBIOS domain. --upn-rewrite is repeatable, each as from=to.
export ZEGRESS_LDAP_BIND_PASSWORD='<ad-bind-password>'
export ZEGRESS_MASTER_KEY='<base64-master-key>'
zegress-control --postgres-url "$ZEGRESS_POSTGRES_URL" \
directory set-ldap \
--tenant <tenant-slug> \
--url 'ldaps://dc.corp.example:636' \
--bind-user 'CORP\svc-zegress' \
--base-dn 'DC=corp,DC=example' \
--name-scope CORP \
--upn-rewrite corp.example=example.co.uk
Run AD/LDAP correlation once
Runs correlation immediately using the tenant's stored config. Useful straight after set-ldap, to materialise edges without waiting for the next refresh.
export ZEGRESS_MASTER_KEY='<base64-master-key>'
zegress-control --postgres-url "$ZEGRESS_POSTGRES_URL" \
directory sync-ldap --tenant <tenant-slug>
There is no POSIX sync command. POSIX name resolution and declared mappings run only inside the workers' automatic refresh, which you trigger implicitly by storing config and letting it run.
Automatic refresh
Once Graph or LDAP config is stored, the workers keep the alias graph fresh on their own, running a directory-sync refresh roughly every ten minutes. Each refresh runs declared mappings and POSIX name resolution for every tenant, always — and Entra Graph and AD/LDAP discovery for every tenant with stored config, but only when the worker has the master key set, since it needs the key to decrypt stored secrets.
If the master key is absent from the worker, declared and POSIX edges still refresh — but Graph and LDAP silently do not. Correlation looks configured and partially works. The refresh is otherwise best-effort and idempotent: a failure on one tenant logs and retries next cycle without stalling the others.
Managing directory enrichment
A candid view of what each surface can do:
| Surface | Coverage | Detail |
|---|---|---|
| CLI | Full | All four directory commands, in direct-database mode. The only path that can store an encrypted secret or run a one-shot sync. |
| API | Non-secret config | GET and PATCH /api/tenants/:slug/directory set the non-secret blocks, cluster-admin only. Encrypted secrets and one-shot syncs stay CLI-only — they need the master key, which the API process does not hold. |
| Console | Non-secret config | The tenant detail page's Directory correlation card edits the same blocks and shows a per-block badge for whether the encrypted secret is stored yet. |
In practice the surfaces compose: set the non-secret config over the API or CLI, then store the credential once with set-graph or set-ldap. The API response reports whether each secret is present, so you can tell if a block is still waiting on one.
curl -X PATCH -H "Authorization: Bearer $ZEGRESS_TOKEN" \
-H "Content-Type: application/json" \
"$ZEGRESS_URL/api/tenants/<tenant-slug>/directory" \
-d '{
"graph": { "entra_tenant_id": "<entra-tenant>", "client_id": "<app-client-id>" },
"ldap": { "url": "ldaps://dc.corp.example:636", "bind_user": "CORP\\svc-zegress",
"base_dn": "DC=corp,DC=example", "name_scope": "CORP",
"upn_rewrite": { "corp.example": "example.co.uk" } },
"principal_mappings": [
{ "from": "oid:<issuer>:<group-guid>", "to": "sid:CORP:S-1-5-21-…", "confidence": "high" }
]
}'
This endpoint merges only the blocks you send, leaving issuer, audience and sibling blocks untouched — unlike a wholesale tenant patch, which replaces the config entirely. A has_graph_secret of false means the Graph config is in place but inert until you store the encrypted client secret.