Skip to main content

Least Privilege by Design: Custom Access Policies for FoundryDB Files

· 3 min read
FoundryDB Team
Engineering @ FoundryDB

A Files access key used to say one thing: read, write, or readwrite, optionally under a single prefix. That covers the common case, but real apps want sharper edges. A per-service key that can upload but never delete. An auditor key that can list and read one folder and nothing else. A write-only dropbox confined to inbox/. A locked archive that no key can touch. Today those become one-line policy decisions. Files access keys can now carry a custom inline policy: an ordered list of statements, each with an effect (Allow or Deny), a set of S3 data actions, and object-key prefixes.

What you can build

A policy is a list of statements evaluated together, with the familiar rule that an explicit Deny always wins. That is enough to express least privilege directly on the key, so the credential itself carries the boundary rather than your application code:

{
"statements": [
{ "effect": "Allow", "actions": ["s3:GetObject", "s3:ListBucket"], "prefixes": ["reports/"] },
{ "effect": "Allow", "actions": ["s3:PutObject"], "prefixes": ["reports/inbox/"] },
{ "effect": "Deny", "actions": ["s3:DeleteObject"], "prefixes": ["reports/archive/"] }
]
}

This key can read and list everything under reports/, drop new objects only into reports/inbox/, and can never delete anything under reports/archive/, no matter what the rest of the policy allows. Mint one key per app, one per environment, one per teammate, each scoped to exactly what it needs.

Tenant isolation is a property, not a convention

You supply actions and prefixes only. You never write a resource ARN. The platform compiles every statement's resource to your own bucket before it reaches the storage provider, so a policy has no way to name, reach, or reference another bucket or another tenant. That boundary is structural: it holds regardless of what you put in the statement.

Actions are restricted to an S3 data-plane allowlist: GetObject, PutObject, DeleteObject, their versioned and multipart forms, RestoreObject, ListBucket, GetBucketLocation, ListBucketVersions, and ListBucketMultipartUploads. Wildcards like s3:* and administrative actions (bucket policy, ACL, IAM) are rejected outright. A policy holds up to 20 statements, with up to 20 actions and 20 prefixes each.

Enforced at the storage layer

The most important detail: these policies are enforced by the object storage provider's own IAM, not by FoundryDB bookkeeping. An action outside the policy, or one hit by a Deny, is refused at the S3 layer itself, whether or not your app behaves. The key simply cannot do what the policy does not permit.

Everywhere you work

Custom policies are available in the console (a statement editor in the create-key dialog), the REST API (POST /file-services/{id}/keys with a policy field), the Go SDK, and the MCP server. In the API, policy is mutually exclusive with the prefix-and-permission shorthand: pass one or the other, not both.

And that shorthand is not going anywhere. If all you need is readwrite under one prefix, keep doing exactly that. Custom policies are here for when the common case is not enough.