Tenants¶
Available in lakeFS Enterprise. Start a free trial.
Private preview
Tenants are in private preview for self-managed lakeFS Enterprise, and interfaces and behaviour can change before general availability. Several surfaces are not tenant-aware yet; see Known limitations before relying on the feature.
When many teams share a single lakeFS installation, every user who logs in can ordinarily see every repository, and the only thing standing between one team's data and another's is a carefully authored set of RBAC policies. That approach works, but it puts the platform team in the business of hand-writing isolation rules, and a single mistaken policy is enough to expose data across team boundaries.
Tenants replace those hand-written rules with a boundary the server enforces. A tenant is a named partition of the installation that owns its own repositories, and a user reaches a tenant's data only if they are a member of it. Membership is checked for every data-plane action no matter which policies the user holds, so a policy alone can never reach across the boundary. Onboarding a team becomes a matter of creating a tenant and attaching a group to it, and because existing repositories live in a reserved tenant called root, turning the feature on changes nothing for an installation that is already running.
Known limitations¶
Tenants are in private preview for self-managed lakeFS Enterprise, and interfaces and behaviour can change before general availability. The following are not supported yet:
- lakeFS Cloud. Tenants are not available on the hosted service.
- Iceberg REST catalog. The catalog ignores the tenant header and serves repositories in the
roottenant only, so repositories in other tenants are not reachable through it. - lakeFS Mount. Mount sends unscoped requests, so it works with repositories in the
roottenant only, and mounting a repository that lives in another tenant fails as though the repository did not exist. - Metadata search. The indexer runs without a tenant scope, so the repositories it is configured to index are resolved in the
roottenant only, and the index repositories it creates live there as well. - Datasets. Dataset metadata keys and dataset pull requests are installation-wide rather than scoped to a tenant.
- Audit log. Entries carry no tenant context, and every tenant's activity is recorded on the
roottenant's log. - Tenant count limits. The license does not enforce a limit on the number of tenants, but will in future releases.
- The high-level Python package (
lakefson PyPI). It has no tenant support; use the generated SDK client, which accepts a tenant when it is constructed.
How tenant isolation works¶
Users, groups, and policies remain installation-wide, which keeps identity management and single sign-on exactly as they are. What tenants add is a membership gate in front of the data plane. Every repository belongs to exactly one tenant, and when a request is scoped to a tenant, lakeFS verifies that the caller is a member of it before evaluating any fs: permission. A user holding FSFullAccess on every repository still cannot read a repository in a tenant they do not belong to, because the gate denies the request before the policy is considered.
Tenant administration is governed separately, by policy alone rather than by membership. Administering a tenant and being able to read its data are therefore independent capabilities, and an administrator who also needs the data has to be a member as well. This separation is what makes delegation safe, and the policies that express it are described under delegating tenant administration below.
Because identities are global, an installation administrator holding auth:* retains full visibility over tenants and can attach themselves to any of them. This is an accepted property of the design rather than an oversight, so treat installation administrator credentials with the care that implies.
The request path below shows where the gate sits relative to authentication and policy evaluation.
flowchart TD
A["Request<br/>X-LakeFS-Tenant: team-a"] --> B[Authenticate the caller]
B --> C["Resolve the tenant<br/>no header means root"]
C --> D{Is the caller a member<br/>of the tenant?}
D -- no --> E["404, indistinguishable from<br/>a tenant that does not exist"]
D -- yes --> F[Evaluate RBAC policies]
F -- deny --> G[Denied]
F -- allow --> H["Repository inside<br/>the tenant"]
The membership gate runs before any policy is evaluated, so no policy can reach a repository in a tenant the caller does not belong to.
The root tenant¶
Every installation has a reserved tenant named root that holds all repositories created before tenants were in use, as well as any repository created without a tenant scope. It is implicit, cannot be created, updated, or deleted, and never appears in tenant listings. A request that carries no tenant scope operates on root, which is why enabling tenants leaves existing clients working unchanged.
Tenant names follow the same character rules as repository names, with two additional restrictions that keep names unambiguous when they appear alongside repository names in S3 bucket addressing: a tenant name cannot contain a double hyphen (--) and cannot end with a hyphen. The name root is reserved.
Creating tenants and managing membership¶
Tenants are created through the REST API, through lakectl, or through the admin UI, and creating one requires the auth:CreateTenant permission. The caller who creates a tenant is attached to it automatically, so a new tenant is immediately usable by whoever made it.
lakectl auth tenants create --name team-a --description "Team A workspace"
lakectl auth tenants list
lakectl auth tenants show --name team-a
Membership is managed through the REST API, through lakectl, or through the admin UI, by attaching either individual users or whole groups to a tenant. Attaching a group is the pattern to reach for, since it lets an identity provider drive tenant access through existing group assignments rather than through per-user calls:
lakectl auth tenants groups attach --name team-a --group data-engineering
lakectl auth tenants groups list --name team-a
lakectl auth tenants groups detach --name team-a --group data-engineering
lakectl auth tenants users attach --name team-a --user jane.doe
lakectl auth tenants users list --name team-a
lakectl auth tenants users detach --name team-a --user jane.doe
The same operations are available on the API as PUT and DELETE on /auth/tenants/{tenant}/groups/{group} and /auth/tenants/{tenant}/users/{user}. Passing '*' as the tenant name attaches to every tenant at once, including tenants created later, which is how an installation-wide identity such as the one used by garbage collection is granted, and the name is quoted so the shell does not expand it.
A user can discover which tenants they belong to through GET /user/tenants, which is also what the tenant switcher in the web UI uses to populate its list. Deleting a tenant requires that it hold no repositories, and the request fails with a conflict if any remain. Memberships are removed along with the tenant.
Scoping a request to a tenant¶
A request carries its tenant in the X-LakeFS-Tenant header, and a request without that header operates on the root tenant. Every API surface honours the same header, so the choice of tenant is a property of the request rather than of the credentials, and one set of credentials can work across every tenant its holder belongs to.
lakectl exposes the same scoping through a --tenant flag, the LAKEFS_DEFAULT_TENANT environment variable, and a tenant key in the configuration file, in that order of precedence. Passing --tenant root overrides a configured default when you need to reach back into root:
lakectl --tenant team-a repo list
export LAKEFS_DEFAULT_TENANT=team-a
lakectl repo list # scoped to team-a
lakectl --tenant root repo list
Repositories are created inside whichever tenant the request is scoped to, and a repository never moves between tenants afterwards. Listing repositories returns only those in the active tenant, so the same command run under two different scopes shows two disjoint sets.
In the web UI, the tenant switcher in the navigation bar sets the scope for the session, and the rest of the interface then behaves as though the selected tenant were the whole installation.
Delegating tenant administration¶
The point of a tenant is that its own administrators can run it without installation-wide power, and that delegation is expressed with two policies documented under preconfigured policies. Neither is provisioned automatically, because a per-tenant grant names the tenant it applies to and therefore has to be written once per tenant.
A per-tenant administrator holds a policy granting auth:ReadTenant, auth:ListTenants, auth:UpdateTenant, and the four attach and detach actions on arn:lakefs:auth:::tenant/team-a, which lets them manage their own tenant's membership and description while leaving the creation and deletion of tenants with the installation administrator. Attaching that policy to the team's group is the whole of onboarding a self-managing team.
Listing tenants returns only the tenants their policies admit, so they see their own and nothing else, and the tenant administration screens in the web UI render the same single tenant. Any other tenant responds as though it did not exist rather than as though access were denied, which means a delegated administrator cannot use the API to discover what other tenants are present.
Addressing tenants through the S3 gateway¶
The S3 gateway has only a bucket name to work with, so reaching a tenant-scoped repository requires encoding both names into that one string. Setting gateways.s3.resolve_tenant to true turns on a composite bucket namespace in which a name of the form team-a--my-repo addresses the repository my-repo inside the tenant team-a, splitting at the first double hyphen. The flag is disabled by default, and while it is disabled every bucket name is taken whole and every request operates on root.
Because the split takes the first -- in the name, a repository in root whose own name contains a double hyphen needs the tenant stated explicitly once resolution is enabled. Repository names are not restricted by this feature, and the explicit root-- prefix is how such a repository stays reachable, so a repository named team-a--data sitting in root is addressed as root--team-a--data:
aws s3 ls s3://team-a--my-repo/main/ # my-repo, in tenant team-a
aws s3 ls s3://my-repo/main/ # my-repo, in root
aws s3 ls s3://root--team-a--data/main/ # a root repository named team-a--data
Composite bucket names work the same way under virtual-host addressing, where the whole name occupies the single DNS label that the gateway's wildcard record already covers.
Listing buckets participates in the same namespace through the prefix parameter. A prefix carrying the separator scopes the listing to the tenant named before it, the remainder filters repository names, and each returned name carries the prefix so that it is addressable exactly as printed. A prefix of team-a-- therefore lists that tenant in full, root-- reaches root repositories whose names contain the separator, and a prefix with no complete separator filters root as it always did. The prefix parameter is a recent addition to the S3 API, and clients that cannot send it continue to receive the root listing. Discovering which tenants exist is a job for auth:ListTenants on the REST API rather than for bucket listings.
Tenant addressing is also careful not to become a discovery channel. A tenant that does not exist and a tenant the caller is not a member of both answer NoSuchBucket, and both list nothing, so neither bucket addressing nor a listing prefix reveals which tenants are present.
One interaction is worth noting if you run the gateway alongside another S3 endpoint. The gateways.s3.fallback_url setting forwards requests for repositories lakeFS does not have to another endpoint, which is how lakeFS runs side by side with S3. A tenant-addressed bucket name is unambiguously a lakeFS address, so it never falls back: a miss inside a tenant answers NoSuchBucket, and only root-scoped misses reach the fallback.
Tenant scoping in policies¶
Policies can be pinned to a tenant through the account segment of a resource ARN, which is the fifth colon-separated field and is empty in the ARNs most policies use. The ARN reference covers the syntax and the four cases in full, including why omitting the segment continues to mean every tenant and therefore keeps existing policies working.