Skip to content

Public Access

How data is made readable without credentials, how that is prevented, and how a policy can depend on where a request comes from.

Anonymous requests

A request with no signature is anonymous. It reaches only what a bucket policy grants to everyone ("Principal": "*"). Nothing else ever applies to it: no identity policy, no ownership, no ACL, and nothing outside bucket and object operations (GET /, the admin API and the console all refuse it).

{"Version": "2012-10-17", "Statement": [{
  "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::assets/*"
}]}

With this policy, curl https://s3.example.com/assets/logo.png works. Listing assets, writing to it, and everything else still need credentials.

Block Public Access

Block Public Access stops data being made public by mistake. It works as in S3, at three levels, and a flag set at any level holds:

level set by API
bucket whoever may s3:PutBucketPublicAccessBlock (its owner by default) PUT /{bucket}?publicAccessBlock (S3)
tenant the tenant's admin PUT /_admin/public-access-block
cluster the system admin PUT /_admin/public-access-block (no tenant)

A bucket can't lift its tenant's block, and a tenant can't lift the cluster's.

flag effect
BlockPublicPolicy a bucket policy that would make the bucket public is refused (403 AccessDenied)
RestrictPublicBuckets a public grant already in place no longer reaches anonymous callers, or anyone outside the bucket's tenant
BlockPublicAcls, IgnorePublicAcls stored and reported. ACLs are owner-enforced, so no ACL can grant public access and both always hold

New buckets start with all four flags set, as S3's have since 2023. Making a bucket public is therefore a deliberate, two-step act: lift the block, then set the policy.

# A bucket: S3 XML, as aws s3api put-public-access-block sends it
aws s3api put-public-access-block --bucket assets \
  --public-access-block-configuration BlockPublicPolicy=false,RestrictPublicBuckets=false
aws s3api get-public-access-block --bucket assets
aws s3api delete-public-access-block --bucket assets
aws s3api get-bucket-policy-status --bucket assets      # IsPublic: true/false

# A tenant (as its admin) or the cluster (as the system admin): JSON
PUT /_admin/public-access-block            {"BlockPublicPolicy": true, "RestrictPublicBuckets": true}
GET /_admin/public-access-block[?tenant=acme]
DELETE /_admin/public-access-block[?tenant=acme]

# The cluster only: whether new buckets start blocked (default true)
PUT /_admin/public-access-block            {"new_buckets_blocked": false}

What counts as public. A policy is public if it allows "*" and no condition limits that to fixed callers. These conditions make it not public: - aws:SourceIp with a network smaller than the whole internet; - aws:SourceVpce; - aws:SourceArn, aws:SourceAccount, aws:PrincipalOrgID, aws:userid.

Negative matches and wildcards don't count: "anyone not in 10.0.0.0/8" is still public.

Where a request came from

Policies can test two facts about a request's origin.

aws:SourceVpce: which endpoint it arrived on

Each gateway listener can be given a name. The usual setup is two: - an internal endpoint, the in-cluster Service that pods use; - an external endpoint, which the ingress points at.

iolake-gateway --listen 0.0.0.0:9000 --endpoint-name internal \
                 --data-listen 0.0.0.0:9010=external

--data-listen ADDR=NAME adds a listener that serves S3, Iceberg and Delta Sharing only: no admin API, no console. It can be repeated. A request on an unnamed listener has no aws:SourceVpce, just as an AWS request over the internet has none.

The name is IOLake's equivalent of an AWS VPC endpoint ID, so AWS policies written for VPC endpoints carry over.

Readable without keys, from inside the cluster only. Because it's pinned, this policy isn't public. It is accepted with Block Public Access on, and anonymous callers can use it:

{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject",
 "Resource": "arn:aws:s3:::models/*",
 "Condition": {"StringEquals": {"aws:SourceVpce": "internal"}}}

A key that only works from inside. A leaked key is then useless from the internet:

{"Effect": "Deny", "Principal": "*", "Action": "s3:*",
 "Resource": ["arn:aws:s3:::payroll", "arn:aws:s3:::payroll/*"],
 "Condition": {"StringNotEquals": {"aws:SourceVpce": "internal"}}}

aws:SourceIp: the client's address

By default this is the address of the connection's peer. Behind a load balancer or ingress controller, the peer is the proxy. List the proxies with --trusted-proxies (CIDRs, comma-separated). For a request from one of them, the client is then the nearest address in X-Forwarded-For that isn't a trusted proxy. X-Forwarded-For from anyone else is ignored, so a client can't claim an address it doesn't have.

iolake-gateway --trusted-proxies 10.244.0.0/16      # e.g. the ingress controller's pod network

The same rule applies to X-Forwarded-Proto, which sets aws:SecureTransport (whether the client used TLS, terminated at the proxy). From an untrusted peer the header is dropped, so a policy that requires TLS can't be passed by claiming it over plain HTTP. If you terminate TLS at an ingress and use aws:SecureTransport, set --trusted-proxies; without it, every request counts as plain HTTP.

{"Effect": "Allow", "Principal": "*", "Action": "s3:GetObject",
 "Resource": "arn:aws:s3:::partner-feed/*",
 "Condition": {"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}}}

In Kubernetes, pod addresses change. To mean "inside the cluster", prefer endpoint names; keep IP conditions for fixed networks such as an office range or a partner's servers.

Helm

gateway:
  endpoints:
    internalName: internal        # the gateway Service
    external:
      port: 9010                  # a second port; the ingress targets it
      name: external
  trustedProxies: "10.244.0.0/16"

Do I need a private endpoint?

Usually not. If the applications run in the same cluster as IOLake, the gateway's ClusterIP Service is already private: traffic stays in the cluster unless you also expose it through an ingress.

Block Public Access is about something else: who may read without credentials, not which network the request uses. Keep it on either way. Endpoint names connect the two when you want "anyone inside the cluster, but no one outside".