Connecting a bucket
objectgate reads your bucket with credentials you create and can take back. On AWS the better of the two is a cross-account role: no long-lived key leaves your account, and deleting the stack ends the access. Every other provider uses an access key scoped to the bucket.
What objectgate is allowed to do
Three statements, one bucket, one prefix, no write. The same policy applies whether it
ends up on a role or on a key. Substitute your bucket name for
example-bucket and your prefix for deliveries/; an empty prefix
means the bucket root.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "LocateBucket",
"Effect": "Allow",
"Action": "s3:GetBucketLocation",
"Resource": "arn:aws:s3:::example-bucket"
},
{
"Sid": "ListPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::example-bucket",
"Condition": {
"StringLike": { "s3:prefix": ["deliveries/*"] }
}
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-bucket/deliveries/*"
}
]
}
The condition on ListPrefix is what keeps a share inside its prefix: a list
call for anything else is denied by IAM, not by our code. It has one consequence worth
knowing in advance. A HeadBucket call carries no prefix at all, so the
condition cannot match and the call is denied. The connect check expects that 403 and
moves straight on to a prefixed list, so a denied head is not a broken connection.
Nothing in the policy grants s3:PutObject, s3:DeleteObject or
any bucket-level write. A key or role carrying this policy cannot change the bucket.
A role, created by a quick-create link
The dashboard builds a CloudFormation quick-create link for each connection. Opening it lands you in the CloudFormation console of whichever AWS account you are signed in to, with the template and its three parameters already filled in:
https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review
?templateURL=<the objectgate template, served over https from S3>
&stackName=objectgate-access
¶m_BucketName=example-bucket
¶m_Prefix=deliveries/
¶m_ExternalId=9f3c7a1e5b2d48c0a6f1e39d72b45c8e
The template is served at objectgate.dev/objectgate-role.yaml. Read it before you run it; it is short. CloudFormation only accepts a template from an S3 URL, so the copy the link points at is the same file in a public bucket of ours.
Review the stack and check the box acknowledging that it creates an IAM resource with a
name you chose. That acknowledgement is CAPABILITY_NAMED_IAM, and it is
required because the role's name is fixed rather than generated. The name is what lets
objectgate find the role without you pasting anything back:
- role name
objectgate-<external id>- role ARN
arn:aws:iam::<your account id>:role/objectgate-<external id>- what the form asks you for
- the bucket, the prefix, and your 12-digit AWS account id
objectgate generates the external id, derives that ARN, and tries to assume it every few
seconds until the stack finishes. The stack also prints the ARN as an output, which is
worth reading if your account is in the GovCloud or China partition: the ARN there starts
with arn:aws-us-gov or arn:aws-cn and has to be pasted in.
The role also needs a trust policy. Only objectgate's AWS account may assume the role, and only while presenting the external id issued for this connection:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ObjectgateAssume",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::AWS_ACCOUNT_ID_REPLACE_AT_BOOTSTRAP:root" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "sts:ExternalId": "9f3c7a1e5b2d48c0a6f1e39d72b45c8e" }
}
}
]
}
The external id belongs to one connection and is used nowhere else. The condition is what makes the role ARN insufficient on its own: a third party who learned your ARN still cannot borrow our identity to reach it.
To revoke: delete the stack. The role goes with it and every session minted from it stops working within the hour.
An access key
Outside AWS, and on AWS when you would rather not run a stack, objectgate takes an access key. Create a user or an application key whose only attached policy is the one above, then paste the key id and secret into the connect form.
The secret goes straight into the share cell's encrypted vault. It is never written to our database, never logged, and never sent back to a browser; the dashboard shows the key id and nothing else. To rotate, paste the new key and delete the old one at your provider once the connect check has gone green.
Some providers can scope a key at creation time, which is stronger than a policy you attach afterwards. A Backblaze B2 application key restricted to one bucket cannot list any other bucket even if asked, and Cloudflare R2 tokens carry per-bucket read permission the same way.
Provider settings
What to enter for each provider. The dashboard fills these in when you pick one, so the table is here for checking a value that came out wrong.
| provider | endpoint | region | addressing | what to know |
|---|---|---|---|---|
| Amazon S3 | https://s3.{region}.amazonaws.com | us-east-1 | host-style | The region must be the bucket's own. The connect check reads it from the bucket and corrects a wrong answer instead of failing. |
| Cloudflare R2 | https://{account_id}.r2.cloudflarestorage.com | auto | path-style | The account id in the endpoint is the Cloudflare account, not the bucket. R2
signs with the region auto. |
| Backblaze B2 | https://s3.{region}.backblazeb2.com | us-west-004 | host-style | The region is the one inside your bucket's S3 endpoint, such as
us-west-004. Use an application key restricted to the bucket. |
| Wasabi | https://s3.{region}.wasabisys.com | us-east-1 | host-style | The endpoint and the region name the same Wasabi region. Mixing two of them produces a signature error rather than a redirect. |
| DigitalOcean Spaces | https://{region}.digitaloceanspaces.com | nyc3 | host-style | The region is the datacentre slug, nyc3 or fra1, and it
appears in the endpoint too. |
| Scaleway Object Storage | https://s3.{region}.scw.cloud | fr-par | host-style | The region is the one in the endpoint, such as fr-par. |
| MinIO | https://minio.example.com | us-east-1 | path-style | The server has to answer on a public hostname over https. A LAN address or a VPN-only host is rejected at connect time and cannot be connected at all. |
| Other S3-compatible | https://s3.example.com | us-east-1 | path-style | Path-style is assumed, because it is what an unknown implementation is most likely to serve. Switch to host-style if the bucket answers on its own hostname. |
Buckets encrypted with a KMS key
Default S3 encryption needs nothing extra. A customer-managed KMS key needs two grants, and missing either one produces the same 403 on the first download. In the policy:
{
"Sid": "DecryptObjects",
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
}
And in the key policy itself, where the role has to be named as a principal:
{
"Sid": "ObjectgateRoleDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/objectgate-9f3c7a1e5b2d48c0a6f1e39d72b45c8e"
},
"Action": ["kms:Decrypt", "kms:DescribeKey"],
"Resource": "*"
}
The connect check reads one object end to end, so a KMS gap shows up while you are still on the connect screen rather than on a viewer's first click.
What you do not have to do
- Turn off Block Public Access
- Leave all four settings on. objectgate reads with credentials, and a viewer's download is a signed URL, not a public object.
- Edit the bucket policy
- Nothing on this page touches it. The permission lives on a role or a key, which is also where you remove it.
- Create a CloudFront distribution
- No distribution, no origin access control, no signed cookies, no key group to rotate.
- Add a CORS rule
- A download is a navigation to a redirect target, which CORS does not apply to. You only need a rule if your own page fetches objects with a script.
- Move or copy your objects
- They stay where they are, in their storage class, under their existing lifecycle rules.
- Give viewers a cloud identity
- No IAM user, no federation into your account, no account with objectgate either.
When the connect check fails
The check reports the storage provider's own error code with the thing to change. These are the ones worth having written down.
| code | what happened | what to change |
|---|---|---|
| SignatureDoesNotMatch | The secret or the region used for signing is wrong. | Re-paste the secret, and check the region against the provider table above. On AWS a bucket in eu-west-1 signed as us-east-1 fails here. |
| AccessDenied on the list | The policy has no s3:ListBucket for that prefix. |
Add the ListPrefix statement, with the prefix condition matching the
prefix you entered. A share can also run with listing off and a key per download. |
| AccessDenied on the object | The policy has no s3:GetObject for that key. |
Check the resource ARN ends in /<prefix>* and that the object
really is under that prefix. |
| KMS.AccessDeniedException | The object is encrypted with a KMS key the role may not use. | Add kms:Decrypt to the policy and the role to the key policy. Both,
as above. |
| InvalidObjectState | The object is in Glacier or Deep Archive. | Restore it in your account first, or point the share at a prefix that holds objects in a readable class. |
| NoSuchBucket | No bucket by that name in that region. | Check the spelling, and check you are looking at the account the key belongs to. |
| PermanentRedirect | The bucket lives in a different region from the endpoint. | Take the region from the check's own message, which reads it out of the provider's response. |
| InvalidAccessKeyId | The key id does not exist at that provider. | Confirm the key was not deleted, and that it belongs to the provider you picked rather than another one you also use. |
| ExpiredToken | The role session ended before the request was made. | Raise the role's maximum session duration to an hour. objectgate renews sessions on its own above that. |
| AllAccessDisabled | The storage account itself is disabled. | Check billing or a suspension at the provider. Nothing in the policy will fix this one. |
| RequestTimeTooSkewed | Our signing clock and the provider's disagree by more than the allowed window. | Nothing on your side. Retry, and tell us if it repeats. |