Skip to content

CodeCommit IAM policies for team access — least-privilege per repo, branch-protected main

CodeCommit's identity model is IAM, not user roles in a UI. Here are the policy patterns that give teams the right access per repo and block force-push on main.

The platform lead wants the same guardrails on CodeCommit that GitHub gives you out of the box: a developer can push to their feature branch, can open a PR against main, but cannot push directly to main, cannot force-push to delete history, and cannot delete the repo. The team is split across three squads with overlapping read access and squad-specific write access. CodeCommit has no settings UI for any of this — every guardrail is an IAM policy. Done wrong, you either grant codecommit:* to everyone (the wrong default the team falls into) or you write per-developer policies that don’t survive the next hire. Done right, the policies are tag-scoped, role-based, and survive turnover.

What’s happening

CodeCommit authorization flows entirely through IAM. There are no “owner,” “maintainer,” or “writer” roles inside CodeCommit itself — those concepts come from IAM principals (users or roles) and the actions their attached policies permit. The relevant actions split into three families:

  • Repo-level reads: codecommit:GitPull, codecommit:GetRepository, codecommit:ListBranches, codecommit:GetCommit, and the PR-related reads (codecommit:GetPullRequest, codecommit:ListPullRequests).
  • Repo-level writes: codecommit:GitPush, codecommit:CreatePullRequest, codecommit:MergePullRequest*, codecommit:UpdatePullRequestStatus.
  • Repo-level admin: codecommit:CreateRepository, codecommit:DeleteRepository, codecommit:PutRepositoryTriggers, codecommit:UpdateRepositoryDescription.

The branch-level control is the part teams miss. CodeCommit ships a condition key — codecommit:References — that lets you scope codecommit:GitPush to specific refs. Combine that with a Deny for force-push (codecommit:References evaluates against the destination ref; the aws:ResourceTag condition lets you scope by repo) and you get the GitHub-equivalent branch protection without external tooling.

The fragile pattern teams fall into is identifying repos by ARN in every policy. As repo count grows past about ten, the policies become unmaintainable. The durable pattern is tagging — every repo carries a Squad tag and a Tier tag, and policies scope on those tags via aws:ResourceTag. New repo, right tags, no policy changes.

How to verify

Check the current state of any developer’s effective permissions before changing anything:

# What actions can this user actually perform against this repo?
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:user/developer-alice \
  --action-names \
    codecommit:GitPush \
    codecommit:GitPull \
    codecommit:DeleteRepository \
    codecommit:DeleteBranch \
  --resource-arns arn:aws:codecommit:us-east-1:123456789012:my-repo \
  --output table

The simulator returns allowed or implicitDeny for each action, with the matching policy line. Run it before any policy change and re-run after — if the diff isn’t what you expected, the policy is broken.

To enumerate every principal that currently has any CodeCommit permission against any repo:

aws iam get-account-authorization-details \
  --query 'Policies[?DefaultVersion.Document.Statement[?Action[?contains(@, `codecommit`) == `true`]]].{Policy:PolicyName,Arn:Arn}' \
  --output table

The list should be short — a handful of group-attached policies, not dozens of inline policies on individual users. If it’s long, the cleanup work precedes the new policies.

Check that the tagging discipline is in place:

aws codecommit list-repositories --query 'repositories[].repositoryName' --output text | \
  while read r; do
    tags=$(aws codecommit list-tags-for-resource \
      --resource-arn "arn:aws:codecommit:us-east-1:123456789012:$r" \
      --query 'tags' --output json)
    echo "$r: $tags"
  done

Repos without Squad and Tier tags will fall outside the tag-scoped policies — they need tags applied before the policies will protect them.

The fix

The pattern uses three policies: a read policy attached broadly, a write policy attached to squad-specific groups, and a deny policy that overrides everything for protected refs.

  1. Tag every repo. Establish two mandatory tags: Squad (which team owns it) and Tier (prod, staging, internal). Apply them with a tag-policy or a creation-time script — repos created without tags are out of scope of the protections.
aws codecommit tag-resource \
  --resource-arn arn:aws:codecommit:us-east-1:123456789012:payments-api \
  --tags Squad=payments,Tier=prod
  1. Attach a tag-scoped read policy to the engineering group. Everyone gets read on every prod and internal repo; staging repos are open by default. This is the policy:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadAllRepos",
      "Effect": "Allow",
      "Action": [
        "codecommit:GitPull",
        "codecommit:GetRepository",
        "codecommit:ListBranches",
        "codecommit:GetCommit",
        "codecommit:GetPullRequest",
        "codecommit:ListPullRequests",
        "codecommit:GetCommentsForPullRequest",
        "codecommit:GetDifferences",
        "codecommit:GetBlob",
        "codecommit:GetFile",
        "codecommit:GetFolder"
      ],
      "Resource": "*"
    }
  ]
}

The Resource: "*" is intentional — read on source code at this scale is rarely the sensitive bit. If a repo carries secrets, it shouldn’t (that’s a separate fix), and if a repo carries IP that is sensitive at the read level, that repo needs a separate policy scoping read to the owning squad.

  1. Attach a tag-scoped write policy to each squad group. Squad members can push to repos their squad owns. The condition uses aws:ResourceTag against the Squad tag:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "WriteOwnSquadRepos",
      "Effect": "Allow",
      "Action": [
        "codecommit:GitPush",
        "codecommit:CreatePullRequest",
        "codecommit:CreateCommit",
        "codecommit:CreateBranch",
        "codecommit:MergePullRequestByFastForward",
        "codecommit:MergePullRequestBySquash",
        "codecommit:MergePullRequestByThreeWay",
        "codecommit:PostCommentForPullRequest",
        "codecommit:UpdatePullRequestStatus"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/Squad": "payments"
        }
      }
    }
  ]
}

Same policy for each squad, the Squad value swapped. New repo tagged Squad=payments is automatically writable by the payments squad and no other.

  1. Deny force-push and ref deletion on protected refs. This is the policy that gives you GitHub-equivalent branch protection on main:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyForcePushToMain",
      "Effect": "Deny",
      "Action": "codecommit:GitPush",
      "Resource": "*",
      "Condition": {
        "StringEqualsIfExists": {
          "codecommit:References": ["refs/heads/main"]
        },
        "Bool": {
          "codecommit:References": "true"
        },
        "Null": {
          "codecommit:References": "false"
        }
      }
    },
    {
      "Sid": "DenyDeletionOfProtectedRefs",
      "Effect": "Deny",
      "Action": [
        "codecommit:DeleteBranch",
        "codecommit:DeleteRepository"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/Tier": "prod"
        }
      }
    }
  ]
}

The Deny on codecommit:GitPush against refs/heads/main blocks direct pushes to main entirely. The only path to main becomes a merged PR. Combine this with approval rule templates and you have the full guardrail: developers can’t push to main, only the merge of an approved PR can.

  1. Provide an emergency-break-glass role separately. The deny policy applies to developers; if a release manager needs to push a hotfix directly to main in an outage, they assume a separately-scoped break-glass role that is not subject to the deny. Audit sts:AssumeRole calls into that role aggressively — it’s the bypass and should be rare.

Two rules we hold:

Test deny policies with a non-admin principal. An IAM admin’s policy simulator output is misleading — admins have iam:* which can mask denies in subtle ways. Run the simulator against the developer role, not the admin role.

Tag the repo before the first push. Applying tags after the repo is in use means there’s a window where the protections don’t apply and a force-push could land. The first commit happens after the tags do.

The right policy set for a 30-developer, 50-repo organization is three policies (read, per-squad write, deny on protected refs) plus a break-glass role. That’s the whole footprint. Add more only when a specific repo’s sensitivity justifies a one-off.


Stack Harbor runs CodePipeline + CodeCommit (and the GitHub-on-AWS variants) as part of Managed AWS onboarding and ongoing Managed Cloud Ops. If your CodeCommit IAM has drifted into per-user inline policies that no longer survive turnover, start with an infrastructure consult.