objectgate

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
  &param_BucketName=example-bucket
  &param_Prefix=deliveries/
  &param_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.