Permissions¶
Three roles take part, plus the infrastructure synthesis role, which needs only
ecs:DescribeServices on the service to read the live revision. Scope every resource to one service: a deploy role shared across services
lets one typo reach a sibling's database.
Audit role, read only¶
Used by ecsc drift and by a plan step on pull requests. It must not be able to deploy.
| Action | Resource | Why |
|---|---|---|
ecs:DescribeServices |
the service | find the live revision |
ecs:DescribeTaskDefinition |
* (the API does not support resource scoping) |
read the live revision |
ssm:GetParameter |
the pointer parameters and shared parameters the contract names | resolve sources |
secretsmanager:DescribeSecret |
the service's config secret | bind vault-sourced keys |
The audit role does not get secretsmanager:GetSecretValue. Without it, a jsonKey on a pointer
cannot be confirmed: ecsc warns that the key is unverified and keeps the binding (see
secret probe outcomes).
A role that a pull_request workflow can assume is reachable by anyone who can push a branch
to the repository, because that branch can edit the workflow. Roles assumable from
pull_request-triggered workflows must not trust unprotected refs for anything beyond the
read-only actions above. The deploy role, and any role that can read a secret value, never
trusts pull_request or an unprotected branch.
drift-audit.yml runs without a GitHub environment, so its token's sub names a ref. Trust the
audit role to the protected default branch and to the job_workflow_ref of drift-audit.yml.
Deploy role¶
Everything the audit role has, plus:
| Action | Resource | Why |
|---|---|---|
ecs:RegisterTaskDefinition |
* |
register the rendered revision |
ecs:TagResource |
task definitions of the family | tags are carried from the live revision |
ecs:UpdateService |
the service | point it at the new revision |
iam:PassRole |
the task role and the execution role, with iam:PassedToService = ecs-tasks.amazonaws.com |
registration passes them |
secretsmanager:GetSecretValue, secretsmanager:PutSecretValue |
the service's config secret only | ecsc sync merges vault values into it |
secretsmanager:GetSecretValue |
secrets behind pointers | optional: confirm a jsonKey exists; explicit access denial warns and keeps the binding; service, transport and decryption failures stop rendering |
Trust it to the repository, the calling workflow, and the GitHub environment. On production, the environment's protection rules restrict which branches or tags may deploy. An example trust policy, with placeholders in angle brackets:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Federated": "<github-oidc-provider-arn>" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:<owner>/<repository>:environment:production",
"token.actions.githubusercontent.com:job_workflow_ref": "tanya-ok/ecs-contract/.github/workflows/deploy.yml@<full-commit-sha>"
}
}
}
]
}
<github-oidc-provider-arn>is the IAM OIDC provider fortoken.actions.githubusercontent.com.subis environment scoped: only jobs that run in theproductionGitHub environment match.job_workflow_refcarries the ref from the caller'suses:line, so the role only works with the reusable workflow pinned to that commit. Update it together with the pin.- IAM evaluates
job_workflow_refas a condition key for GitHub tokens. Names can be reused after a rename, so consider addingrepository_idandrepository_owner_idas well.
Execution role¶
The role the ECS agent uses to start the task. It resolves every binding the contract produces.
| Action | Resource |
|---|---|
secretsmanager:GetSecretValue |
secrets behind pointers, and the config secret |
ssm:GetParameters |
the parameters bound directly |
kms:Decrypt |
the keys encrypting those, when they are customer managed |
Secret probe outcomes¶
Only a successfully decoded JSON object can prove that a key is absent. Missing secrets and invalid/non-object JSON have distinct blocking diagnostics. Throttling, decryption errors, other service errors and transport failures are also blocking; no new task-definition output is written by that invocation. Existing output files are not deleted, so callers must honor the nonzero exit status.
An explicit AccessDenied response retains the existing least-privilege mode: warn that the JSON key is unverified and retain the reference. This does not prove secret availability or value correctness; ECS task startup still needs its own execution-role permissions. Error messages and secret contents are never used in the diagnostic. Tests exercise render with mocked AWS, not live services.