S3 access policies

Team sharing (S3) explains how to point a folder at a bucket. This page is for whoever administers that bucket: it gives two ready-made access levels so a…

Team sharing (S3) explains how to point a folder at a bucket. This page is for whoever administers that bucket: it gives two ready-made access levels so a publisher can push results without being able to read them back, and readers can pull everything without being able to change it.

Both levels are locked to your office / VPN IP addresses.

LevelCan doCannot do
PublisherUpload every shared file; read and write issues.csvRead any other shared file
ReaderList and download everything shared; write issues.csvChange anything except issues.csv

issues.csv is the exception in both directions on purpose — it is the one file both sides need to write, so issues raised by readers survive and publishers can see them.

Before you paste

Replace these placeholders everywhere they appear:

  • stood-flows-share — your bucket name.
  • shared — the S3 folder configured in Stood Flows (drop the shared/ part of the paths entirely if you left the S3 folder blank).
  • 203.0.113.0/24, 198.51.100.7/32 — the IP ranges allowed to reach the bucket.

Attach each policy to its own identity, so one set of keys is a publisher and the other a reader.

Publisher

Uploads anything, reads nothing but issues.csv.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideAllowedIPs",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::stood-flows-share",
        "arn:aws:s3:::stood-flows-share/*"
      ],
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": ["203.0.113.0/24", "198.51.100.7/32"]
        }
      }
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::stood-flows-share",
        "arn:aws:s3:::stood-flows-share/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "ListBucketForConnectionTest",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::stood-flows-share"
    },
    {
      "Sid": "PublishAnyShareableObject",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::stood-flows-share/shared/*"
    },
    {
      "Sid": "ReadIssuesCsvOnly",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:::stood-flows-share/shared/graphs/*/issues.csv"
    },
    {
      "Sid": "DenyReadingAnythingElse",
      "Effect": "Deny",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "NotResource": "arn:aws:s3:::stood-flows-share/shared/graphs/*/issues.csv"
    }
  ]
}

Reader

Downloads everything, writes nothing but issues.csv.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideAllowedIPs",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::stood-flows-share",
        "arn:aws:s3:::stood-flows-share/*"
      ],
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": ["203.0.113.0/24", "198.51.100.7/32"]
        }
      }
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::stood-flows-share",
        "arn:aws:s3:::stood-flows-share/*"
      ],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    },
    {
      "Sid": "ListShared",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::stood-flows-share"
    },
    {
      "Sid": "ReadEverythingShared",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:GetObjectVersion"],
      "Resource": "arn:aws:s3:::stood-flows-share/shared/*"
    },
    {
      "Sid": "WriteIssuesCsvOnly",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::stood-flows-share/shared/graphs/*/issues.csv"
    },
    {
      "Sid": "DenyWritingAnythingElse",
      "Effect": "Deny",
      "Action": ["s3:PutObject", "s3:PutObjectAcl", "s3:DeleteObject", "s3:DeleteObjectVersion"],
      "NotResource": "arn:aws:s3:::stood-flows-share/shared/graphs/*/issues.csv"
    }
  ]
}

Good to know

  • Stood Flows signs in with an Access Key ID and Secret Access Key, so attach these policies to two IAM users. Temporary role credentials (which also carry a session token) are not accepted by the S3 settings form.
  • A publisher should not press Refresh. Refresh pulls down every shared file, so with the publisher policy it stops with an access-denied error as soon as it reaches a file that is not issues.csv. Publishers publish; readers refresh.
  • Issue lists are merged, never overwritten. Both sides write issues.csv — a publisher publishes it with the analysis, and readers raise, comment on and close issues against their own copy — so publishing and refreshing each merge the two lists instead of one replacing the other. Readers also get a Publish issues button that sends their issue changes back to the bucket and touches nothing else: exactly what the Reader policy above allows.
  • Listing is allowed on the whole bucket in both policies. That is what makes the Test button in the S3 settings work — it checks the bucket itself, not a folder inside it. You can restrict listing to the shared folder only, but then Test always reports a failure even though publishing and refreshing work.
  • Nothing can be deleted with either policy. Stood Flows never deletes from S3 — it only overwrites — so old files stay until you remove them. Turning on bucket versioning plus a lifecycle rule gives you history and automatic tidying.
  • IP restrictions have edges. They apply to traffic arriving over the internet, so anyone off the VPN, on a home connection with a changing IP, or reaching S3 through a VPC endpoint will be denied. Keep the allowed ranges under review.