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.
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.
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.
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:
dynamodb:DescribeTable.dynamodb:DescribeTable and
dynamodb:DescribeTimeToLive – it enforces that DynamoDB TTL is disabled
on the fencing table, which a reaper would otherwise use to delete lease rows
and reset the fencing version.Preflight posture is fail-closed and matters for how you grant these actions:
DescribeTable call that cannot verify the table – the permission is
missing (AccessDenied), the control plane throttles it during a mass rollout,
or the backend does not implement DescribeTable – is also fatal at boot.
An unreadable table is not proof the table is valid, and an unreadable +
mis-shaped table is the exact silent-shredder scenario the preflight exists to
catch (the first record per partition writes, the rest ack-and-drop as
“duplicates”). The store refuses to start.DescribeTimeToLive call that cannot
verify the TTL state (missing dynamodb:DescribeTimeToLive, a throttle, or a
backend that does not implement it) is fatal for the same reason: it proves
nothing about the TTL state, and a TTL-reaped fence row is a split-brain hazard.dynamodb:DescribeTable on every
configured/default store table and additionally grants
dynamodb:DescribeTimeToLive on the exact lease table. Both are therefore
required for boot under the default posture,
and TTL must stay disabled on the lease table.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.
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.