A reference lakeFS target

provcat is a library. It reads and writes stores, and lakeFS is one kind of store that it can target. This page sets up one lakeFS that keeps several orgs apart, as a reference for that target. Each org reads and writes only its own repositories. provcat reaches it through one store and one set of keys for each org.

The setup runs on one machine, for development and tests. It shows that one lakeFS can keep orgs apart, and how.

The parts

Two programs run side by side. Both are built from the fork rahuldave/lakeFS, branch provcat.

  provcat (acme's keys)        provcat (globex's keys)
           |                            |
           v                            v
  +------------------------------------------------+
  | lakeFS 1.86.0, patched        127.0.0.1:8002   |
  |   S3 gateway and REST API                      |
  |   data, commits and branches in <dir>/data     |
  +------------------------------------------------+
           |  "may this user do this?"  (Bearer token)
           v
  +------------------------------------------------+
  | access-list server, patched   127.0.0.1:8011   |
  |   users, groups, policies and keys             |
  +------------------------------------------------+
  • lakeFS holds the data, the commits and the branches. It runs with auth.ui_config.rbac: external, so it asks another server who may do what.
  • The access-list server is the reference authorization server that lakeFS ships in contrib/auth/acl. It keeps the users, groups, policies and keys. lakeFS fetches each user’s policies from it, and checks every call against them.
  • packages/provcat/scripts/lakefs_acl_local.py in this project starts both, sets up the orgs, and stops both.

Where the data lives

lakeFS keeps the objects on local disk, in <dir>/data, with blockstore.type: local. It is not in front of S3. provcat reaches the data only through lakeFS. It uses the S3-compatible gateway for objects, and the REST API for commits and branches.

lakeFS can keep the objects in S3, ADLS Gen2 or GCS instead. That takes a blockstore section with the bucket in lakefs.yaml, and the org policies’ namespace rule then names that bucket, not local://. provcat would not change, because it talks only to lakeFS. This setup keeps the objects on local disk.

Without lakeFS, provcat reads and writes S3, GCS and Azure buckets directly. It branches in any bucket. In a bucket with versioning on, it also pins and commits, as “Commits in a bucket” in the design explains. The cloud’s own access control keeps orgs apart.

Why the setup uses a patched lakeFS 1.86.0

lakeFS 1.87.0 removed the external authorization server and the access-list server. It also moved to the Business Source License 1.1, which is not an open-source license. So this setup stays on 1.86.0, the last Apache 2.0 release, built from source so it can be changed.

Stock 1.86.0 did not keep orgs apart. The fork fixes that. PROVCAT.md at the root of the fork lists every change, why we made it, and its test. In short:

  • lakeFS:
    • staging and linking can use storage outside the repository only with fs:ImportFromStorage on it, which no org has
    • storage names cannot use . or .. segments, or @, ? or #
    • meta-range IDs are plain names
  • The access-list server:
    • callers need a token
    • the policy calls work
    • deleting a user or a group takes its keys or its policy with it
    • the Admins policy is guarded

Start and stop it

Build the fork first, as the setup page explains under “Build lakeFS 1.86.0 from source”. Then start the server:

uv run python packages/provcat/scripts/lakefs_acl_local.py

It runs in the foreground. If you press Ctrl+C, run kill or close the terminal, lakeFS stops, and then the access-list server stops. Start it again with the same command.

When it is ready, it prints two lines:

lakeFS on http://127.0.0.1:8002, with access control for acme, globex
export AWS_SHARED_CREDENTIALS_FILE=/Users/<you>/.local/share/provcat/lakefs-acl/credentials

The script has these options:

Option Default What it sets
--dir ~/.local/share/provcat/lakefs-acl The folder for the configs, the databases and the data.
--lakefs ~/Projects/lakeFS/lakefs The lakeFS program.
--acl-server ~/Projects/lakeFS/acl-server The access-list server program.
--port 8002 The lakeFS port on 127.0.0.1.
--acl-port 8011 The access-list server port on 127.0.0.1.
--org acme and globex An org to set up. Give it once for each org.
--repair-admin off Puts the admin back after a lockout, as “Repair the admin” explains.
--wait 60 Seconds to wait for each program to start.
--cache-seconds 25 After a repair, how long lakeFS may still answer from its policy cache.
--admin-wait 90 After a repair, how long to wait for lakeFS to take the new admin keys.

The first run fixes the ports, because the config files hold them. The script refuses a later run with other ports. It also refuses a port that something else already uses.

The folder

The first run makes the folder and writes everything into it. Later runs reuse it.

Path Mode What it holds
lakefs.yaml 0600 The lakeFS config, with a random secret key.
acl.yaml 0600 The access-list server config, with its own random secret key.
api-token 0600 The random token between lakeFS and the access-list server.
credentials 0600 An AWS credentials file with every key, as “Orgs, users and keys” explains.
lakefs-db/ 0700 The lakeFS metadata: repositories, branches and commits.
acl-db/ 0700 The access-list server’s users, groups, policies and keys.
data/ 0700 The objects of every repository, one folder for each.
cache/ The lakeFS commit cache, kept apart from every other lakeFS on the machine.

To start over, stop the server and delete the folder. The next run makes new keys for everyone.

The script passes four settings to the programs in the environment, not in the config files. So an older folder gets them too.

Variable To What it sets
ACLSERVER_API_TOKEN access-list server The token every call must carry.
ACLSERVER_CACHE_ENABLED=false access-list server No cache. Its cache made a deleted user or key look present for 20 seconds.
LAKEFS_AUTH_API_TOKEN lakeFS The token lakeFS sends to the access-list server.
LAKEFS_COMMITTED_LOCAL_CACHE_DIR lakeFS Its own commit cache, in <dir>/cache.

Orgs, users and keys

Each org gets four things:

  • A group, named after the org, for example acme.
  • A user, <org>-user, in that group.
  • Keys for that user, saved in credentials as the profile lakefs-<org>.
  • A policy, <org>-repos, attached to the group.

The policy for acme is this:

{"id": "acme-repos", "statement": [
  {"effect": "allow", "action": ["fs:*"],
   "resource": "arn:lakefs:fs:::repository/acme-*"},
  {"effect": "allow", "action": ["fs:AttachStorageNamespace"],
   "resource": "arn:lakefs:fs:::namespace/local://acme-*"}
]}

So acme can do anything in a repository whose name starts with acme-, and can make one only on storage named local://acme-.... An org name is 1 to 20 of a-z and 0-9, with no -. So acme-* never matches another org’s repositories.

The admin is the user admin, in the group Admins. Its keys are the profile lakefs. The admin can do everything, in every repository.

What each org can and cannot do

Own repositories Another org’s repositories
Read, list, write and delete objects Yes Refused
Commit, branch, tag and merge Yes Refused
Make a repository Yes, named <org>-... Refused
List repositories Sees only its own Not shown
Manage users, groups, policies or keys Refused Refused

The system test packages/provcat/tests/system/test_lakefs_acl.py checks reading, writing, committing, branching, merging, making and listing repositories, through the REST API, the S3 gateway and provcat. Tags follow from the same policy, but no test runs them.

Add an org

Stop the server, then start it with every org you want, the old ones too:

uv run python packages/provcat/scripts/lakefs_acl_local.py --org acme --org globex --org initech

A run only adds orgs. It keeps the keys of the orgs that already exist, and prints the new ones as (new: initech). It also writes each org’s policy again, so a change to the policy in the script reaches an existing server.

Each run also checks the saved key of every org it is given with --org. If that key no longer works, the run deletes all of the org’s keys and makes a new one.

Use it with provcat

Point provcat at the server and its keys:

export AWS_SHARED_CREDENTIALS_FILE=~/.local/share/provcat/lakefs-acl/credentials
export LAKEFS_S3_ENDPOINT=http://127.0.0.1:8002

The example configuration, packages/provcat/config/provcat.example.yaml, then works as it is. Its store lakefs-acme uses the profile lakefs-acme, and lakefs-globex uses lakefs-globex. The logical schemes pick the store from the org in the URI, so data+table://acme/... reads and writes with acme’s keys only. The store lakefs uses the admin’s keys.

Give each org a configuration with only its own store, and only its own profile in its credentials file. The mapping in one shared configuration routes requests. The keys keep orgs apart. The design explains this under “Tenants”.

AWS_SHARED_CREDENTIALS_FILE hides ~/.aws/credentials in that shell. So the plain S3 stores lose their keys there. Use another shell for them, or copy the profiles into ~/.aws/credentials.

Run it as the admin

The admin manages the server through the lakeFS REST API, with the keys of the profile lakefs. lakeFS passes the calls about users, groups and policies on to the access-list server. These calls are the useful ones:

Task Call
List users GET /api/v1/auth/users
List an org’s keys GET /api/v1/auth/users/acme-user/credentials
Make a new key for an org POST /api/v1/auth/users/acme-user/credentials
Delete a key DELETE /api/v1/auth/users/acme-user/credentials/<access key id>
List policies GET /api/v1/auth/policies
Show a group’s policies GET /api/v1/auth/groups/acme/policies
Take an org’s access away DELETE /api/v1/auth/groups/acme/policies/acme-repos
Give it back PUT /api/v1/auth/groups/acme/policies/acme-repos

For example, with curl and the admin keys from the profile:

curl -u "$ADMIN_KEY:$ADMIN_SECRET" http://127.0.0.1:8002/api/v1/auth/users

An org’s user cannot list, make or delete its own keys. Only the admin can.

A change to a policy can take up to about 23 seconds to apply, because lakeFS caches each user’s policies.

Guards on the admin

The fork refuses calls that would leave the admin unable to do anything:

  • deleting the Admins group
  • deleting the ACL(_-_) policy of a group that exists, or detaching it from its own group
  • making an ACL(_-_) policy for a group that does not exist
  • writing anything into ACL(_-_)Admins but the full-access statement

The fork answers each refused call with status 400.

Repair the admin

The admin can still lock itself out in other ways:

  • leave the Admins group
  • join a group with a deny policy
  • attach a deny policy to itself or to Admins
  • delete its own keys or its own user

To undo any of these, run the script with --repair-admin:

uv run python packages/provcat/scripts/lakefs_acl_local.py --repair-admin

If the server runs, with both ports in use, the script repairs the admin in place and exits. Otherwise it starts the server and repairs the admin first. It works through the access-list server and the token, not through lakeFS, so a locked-out admin does not stop it. It does the same steps every time:

  1. It makes the Admins group again if it is missing. It writes the Admins policy back to its full-access statement, and detaches every other policy from Admins, including the ones that only allow.
  2. It puts the admin back in Admins, takes it out of every other group, and detaches the policies on the admin itself.
  3. It deletes the admin’s keys, makes a new key, and saves it in credentials.
  4. It waits until lakeFS takes the new key for policies and for repositories, after lakeFS’s policy cache has expired.

It prints what it changed, for example repaired the admin: put the admin back in Admins; made new admin keys. It exits 1 if lakeFS still refuses the new key after --admin-wait seconds.

What the setup protects and what it does not

The setup protects each org’s data from the other orgs:

  • Org keys. An org’s keys reach only that org’s repositories. That covers objects, commits, branches, staged and linked addresses, storage names and meta-range IDs.
  • The access-list server. It takes only calls that carry the token, and it listens on 127.0.0.1 only.
  • The secrets. The config files, the token and the keys are readable by your user only.

The setup does not protect against these:

  • Your own user account. Anyone who can read <dir> has the admin keys and the token.
  • Other machines. Both programs listen on 127.0.0.1. Do not expose them on a network without TLS and a firewall.
  • Revocation at once. lakeFS caches each user’s policies, so a revoked policy can work for up to about 23 seconds.
  • Server paths. Error messages and stat replies show absolute paths on the server.
  • Every admin mistake. The guards cover the admin’s own policy, and --repair-admin undoes the rest. They do not stop the admin from deleting an org’s data.
  • A rare race. If a policy is deleted and made again at the same moment that it is attached, a stale link can survive. It has not been reproduced.

Test it

Integration tests run the script against fake servers, in about 40 seconds:

uv run pytest packages/provcat/tests/integration/test_lakefs_acl_local.py

The system tests run against the real server. Start it first, then:

PROVCAT_SYSTEM_LAKEFS_ACL=http://127.0.0.1:8002 \
PROVCAT_SYSTEM_LAKEFS_ACL_SERVER=http://127.0.0.1:8011 \
PROVCAT_SYSTEM_LAKEFS_ACL_DIR=~/.local/share/provcat/lakefs-acl \
PROVCAT_SYSTEM_LAKEFS_ACL_CREDENTIALS=~/.local/share/provcat/lakefs-acl/credentials \
PROVCAT_SYSTEM_LAKEFS_ACL_DATA=~/.local/share/provcat/lakefs-acl/data \
uv run pytest -m system packages/provcat/tests/system/test_lakefs_acl.py

They take about 3 minutes, most of it waiting out lakeFS’s policy cache after each repair. They make repositories named acme-provcat-test-... and globex-provcat-test-..., and delete them at the end. The repair tests lock the admin out and repair it, so they change the admin’s keys in credentials.

The fork has its own Go tests. From ~/Projects/lakeFS:

GOTOOLCHAIN=go1.26.4 go test ./pkg/api ./pkg/graveler/... ./pkg/catalog/... ./pkg/pyramid/... ./contrib/auth/...

Errors and their fixes

What you see Why What to do
no program at .../lakefs The fork is not built. Build it, as the setup page explains.
something already listens on 127.0.0.1:8002 Another lakeFS, or an earlier run, holds the port. Stop it. lsof -i :8002 shows what holds it.
... was made with other ports The ports differ from the first run. Use the first run’s ports, or edit listen_address and endpoint in the config files.
... takes calls without the token The access-list server is a stock build. Build acl-server from the fork.
api-token ... can be read by other users Its mode changed. chmod 600 ~/.local/share/provcat/lakefs-acl/api-token.
An org’s keys get insufficient permissions The org may not do this, or a policy changed less than 23 seconds ago. Check the group’s policies as the admin.
The admin gets 401 everywhere The admin locked itself out. Run with --repair-admin.
lakeFS still refuses the new admin keys lakeFS did not take the new key in time. Look at the lakeFS output, then run --repair-admin again.

The lakeFS banner says to complete setup at 127.0.0.1:8000. That line is stock lakeFS text and does not apply. The script has set everything up.

Where to read more

  • The fork’s changes: PROVCAT.md at the root of rahuldave/lakeFS.
  • Building lakeFS: the section “Build lakeFS 1.86.0 from source” in the setup page.
  • Tenancy: the section “Tenants” in the design, with the holes in stock lakeFS.
  • Writes through provcat: the section “Writes” in the design.
  • The script itself: packages/provcat/scripts/lakefs_acl_local.py.