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".