gobridge

IAM Least Privilege

Overview

The exact task-role and execution-role policies a GoBridge deployment needs, why each statement is scoped the way it is, and the few places a wildcard is unavoidable.

Part of the AWS Deployment Overview.


Follow the principle of least privilege when configuring IAM roles. The CDK constructs create scoped policies automatically, but if you manage IAM manually, use these as a reference.

Config-source grants

For Bootstrap.ConfigSource: dynamodb, the Single and DynamoDB HA facades own one config table. The control task role receives the native CDK GrantReadWriteData grant; each worker task role receives GrantReadData only. These grants are scoped to the config table, not to the separate lease, outbox, DLQ, managed-subscription or rollout stores, whose existing grants do not change. CAS makes config writes concurrency-safe, but it does not grant a worker write authority: the deployed worker role remains read-only.

Only config_dynamodb.watch_mode: streams adds GrantStreamRead on the enabled config stream for both roles (DescribeStream, GetRecords, GetShardIterator, plus ListStreams). Poll mode adds no stream-read grant. No runtime config-table creation grant is added. Only the control process may initialize an absent document through a strict conditional write; workers remain read-only in runtime wiring as well as IAM. Initialization and ordinary admin updates share the control task role, not a separate container. There is no config S3 download grant or seeder log group. The deploy principal still needs the normal permissions for CDK image assets.

An EFS-free facade adds no EFS mount/write grants or EFS-CMK grant. SSM, logging and adapter permissions continue to be derived as before.

SQS discovery grants

The facade’s shared base calls GrantSQSConfig, which resolves receiver, sender, and binding references against the Queues prop. A tag selector must be listed in QueueTags under the same key as its queue in Queues. Missing or ambiguous mappings do not fall back to wildcard message permissions.

A stable physical queue_name uses sqs:GetQueueUrl. Tag selection adds sqs:ListQueues and sqs:ListQueueTags; do not add those discovery actions for name-only or direct-URL configurations.

ListQueues needs Resource: "*", because the SQS listing operation has no queue-level resource scope. The client account and region bound the search; an optional queue_name_prefix reduces the candidates. Scope ListQueueTags to the candidate queue resources that the selector may inspect. CDK retains the IQueue handles for precise send/receive grants and deployment dependencies, independently of runtime discovery.

For example, a prefix orders- needs tag-read access to candidate queues under arn:aws:sqs:REGION:ACCOUNT:orders-*, not only the one selected queue. Without a prefix, metadata reads cover all candidate queues in that account and region. Send and receive grants still target the exact registered queue.

The binding marker sqs:queue uses that configured queue. It adds no per-message discovery or broader message-operation grants.

Discovery requires exactly one match. Zero matches retry without readiness; multiple matches fail as ambiguous. A denied list or tag read is an authorization error, never an empty result. No CloudFormation read grant or generic token resolver is required. See embedded queue references.

Task Role

The task role is assumed by the running container. It needs access to EFS, SSM, any transport-specific services (e.g. SQS), and DynamoDB when you configure DynamoDB lease/outbox/DLQ stores.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EfsAccess",
      "Effect": "Allow",
      "Action": [
        "elasticfilesystem:ClientMount",
        "elasticfilesystem:ClientRead"
      ],
      "Resource": "arn:aws:elasticfilesystem:REGION:ACCOUNT:file-system/fs-XXXXXXXX",
      "Condition": {
        "StringEquals": {
          "elasticfilesystem:AccessPointArn":
            "arn:aws:elasticfilesystem:REGION:ACCOUNT:access-point/fsap-XXXXXXXX"
        }
      }
    },
    {
      "Sid": "SsmParameterAccess",
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": [
        "arn:aws:ssm:REGION:ACCOUNT:parameter/gobridge/admin-key",
        "arn:aws:ssm:REGION:ACCOUNT:parameter/gobridge/monitor-key",
        "arn:aws:ssm:REGION:ACCOUNT:parameter/gobridge/rx-*",
        "arn:aws:ssm:REGION:ACCOUNT:parameter/gobridge/tx-*"
      ]
    },
    {
      "Sid": "SqsAccess",
      "Effect": "Allow",
      "Action": [
        "sqs:SendMessage",
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage",
        "sqs:ChangeMessageVisibility",
        "sqs:GetQueueUrl",
        "sqs:GetQueueAttributes"
      ],
      "Resource": "arn:aws:sqs:REGION:ACCOUNT:my-queue-*"
    },
    {
      "Sid": "DynamoDbStoreAccess",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query",
        "dynamodb:Scan",
        "dynamodb:TransactWriteItems",
        "dynamodb:DescribeTable",
        "dynamodb:DescribeTimeToLive"
      ],
      "Resource": [
        "arn:aws:dynamodb:REGION:ACCOUNT:table/gobridge-*",
        "arn:aws:dynamodb:REGION:ACCOUNT:table/gobridge-*/index/*"
      ]
    }
  ]
}

The SQS statement is optional and should be scoped to the exact queue ARNs your bridge routes reference. Omit it entirely if your deployment does not use SQS transport.

sqs:ChangeMessageVisibility backs the receiver’s auto_extend (visibility renewal at one-third of the timeout); a missing grant surfaces as NOT_AUTHORIZED only after the first extension attempt, not at startup. sqs:GetQueueUrl backs queue-name resolution – a receiver or sender configured with queue_name (rather than a full queue_url) resolves the canonical URL at build time. The adapter does not call GetQueueAttributes; the action is retained here as a harmless allowance for operators who inspect queues out-of-band, and can be dropped from a least-privilege policy.

DynamoDB stores. The DynamoDbStoreAccess statement is needed only when a store role is configured with type: dynamodb. Scope Resource to your actual table ARNs – the default names are gobridge-leases, gobridge-outbox, and gobridge-dlq – and keep the /index/* entry, which the outbox and DLQ queries need for their GSIs. Omit the statement entirely for memory/SQLite-only deployments. The data-plane actions each role uses, if you split the statement per table for tighter least privilege:

Role Runtime data-plane actions
Lease GetItem, PutItem, UpdateItem
Outbox GetItem, PutItem, UpdateItem, Query, TransactWriteItems
DLQ GetItem, PutItem, DeleteItem, Query, Scan

Each store also runs a boot-time schema preflight that adds control-plane actions on top of the data-plane set above:

Preflight posture is fail-closed and matters for how you grant these actions:

The advisory opt-outs are Go-code-level factory options, not config keys. WithSchemaPreflightAdvisory() downgrades an unverifiable DescribeTable to a loud WARN-and-continue; WithTTLPreflightAdvisory() does the same for the lease TTL check (both an observed enabled TTL and an unverifiable DescribeTimeToLive). Neither relaxes a confirmed schema mismatch, which stays fatal. Use them only for a dev/emulator that cannot serve these control-plane calls.

The shipped deployment/aws deployment builds the factory as NewDynamoDBStoreFactory(client) with no options and exposes no schema_preflight_advisory or ttl_preflight_advisory config key, so opting into advisory mode requires code-level wiring in a custom composition root. The DynamoDB Local (ddblocal) test emulator implements both DescribeTable and DescribeTimeToLive, so tests and local development against it boot cleanly under the default fail-closed posture – only an emulator or backend that lacks these control-plane calls needs the advisory opt-outs.

Table creation and TTL setup (dynamodb:CreateTable, dynamodb:UpdateTimeToLive) are a deploy-time concern; the CDK constructs provision tables out-of-band. Grant those two actions only if you let the bridge self-provision through its EnsureTable helper. See the DynamoDB Store reference for store behavior and Monitoring for the backlog and store-health signals.

Execution Role

The execution role is used by the ECS agent to pull images and write logs. It does not need access to application-level resources.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EcrPull",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "arn:aws:ecr:REGION:ACCOUNT:repository/gobridge"
    },
    {
      "Sid": "EcrAuth",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:REGION:ACCOUNT:log-group:/ecs/gobridge-*:*"
    }
  ]
}

Note that ecr:GetAuthorizationToken requires Resource: "*" because the authorization token is account-scoped, not repository-scoped.