{"id":4651,"date":"2026-07-24T06:05:19","date_gmt":"2026-07-24T06:05:19","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/24\/building-soc-2-compliant-ci-cd-pipelines-on-aws-with-github-actions\/"},"modified":"2026-07-24T06:05:19","modified_gmt":"2026-07-24T06:05:19","slug":"building-soc-2-compliant-ci-cd-pipelines-on-aws-with-github-actions","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/07\/24\/building-soc-2-compliant-ci-cd-pipelines-on-aws-with-github-actions\/","title":{"rendered":"Building SOC 2 Compliant CI\/CD Pipelines on AWS with GitHub Actions"},"content":{"rendered":"<div><img data-opt-id=410432392  fetchpriority=\"high\" decoding=\"async\" width=\"770\" height=\"330\" src=\"https:\/\/devops.com\/wp-content\/uploads\/2026\/07\/aws_soc2_cicd_pipeline_770x330.jpg\" class=\"attachment-large size-large wp-post-image\" alt=\"\" \/><\/div>\n<p><img data-opt-id=180419862  fetchpriority=\"high\" decoding=\"async\" width=\"150\" height=\"150\" src=\"https:\/\/devops.com\/wp-content\/uploads\/2026\/07\/aws_soc2_cicd_pipeline_770x330-150x150.jpg\" class=\"attachment-thumbnail size-thumbnail wp-post-image\" alt=\"\" \/><\/p>\n<p><a href=\"https:\/\/devops.com\/a-beginners-guide-to-ci-cd-and-ci-cd-pipelines\/\">CI\/CD pipelines make developers\u2019 lives easier<\/a> and let them ship changes to end users faster. However, they are also part of compliance frameworks such as SOC 2, which require careful design to satisfy specific controls.<\/p>\n<h3>AWS Account Architecture<\/h3>\n<p>You don\u2019t want to host all your environments, such as staging and prod, in the same AWS account but rather use a multi-account AWS architecture.<\/p>\n<p>Each of your environments \u2014 dev, staging, UAT, prod \u2014 should live in its own separate AWS account, rather than having all of them share a single account. This way, you can have clean logical access controls (CC6.1).<\/p>\n<p>Once each environment has its own account, a natural question follows: Where should your ECR repository live? Should you host one in each environment account or centralize it in a single account that all environments pull images from?<\/p>\n<h3>ECR Repository Architecture<\/h3>\n<p>You could host your ECR repository in the prod environment account, but ideally, you should create a separate AWS account for shared tooling and host your ECR repository there. Your pipeline should only have push access to that repository, scoped to the common account.<\/p>\n<p>Each environment account (dev, staging and prod) then needs cross-account permissions to pull (not push) images from that shared repository. In the common account, create an IAM role that authenticates via OIDC and can push to the relevant ECR repositories. This role doesn\u2019t need pull access.<\/p>\n<p>An example permissions policy for that role is as follows:<\/p>\n<p><code>{<\/code><code>  \"Version\": \"2012-10-17\",<\/code><code>  \"Statement\": [<\/code><code>    {<\/code><code>      \"Sid\": \"AllowECRAuth\",<\/code><code>      \"Effect\": \"Allow\",<\/code><code>      \"Action\": \"ecr:GetAuthorizationToken\",<\/code><code>      \"Resource\": \"*\"<\/code><code>    },<\/code><code>    {<\/code><code>      \"Sid\": \"AllowPushToScopedRepos\",<\/code><code>      \"Effect\": \"Allow\",<\/code><code>      \"Action\": [<\/code><code>        \"ecr:BatchCheckLayerAvailability\",<\/code><code>        \"ecr:PutImage\",<\/code><code>        \"ecr:InitiateLayerUpload\",<\/code><code>        \"ecr:UploadLayerPart\",<\/code><code>        \"ecr:CompleteLayerUpload\"<\/code><code>      ],<\/code><code>      \"Resource\": [<\/code><code>        \"arn:aws:ecr:&lt;REGION&gt;:&lt;COMMON_ACCOUNT_ID&gt;:repository\/myapp-*\"<\/code><code>      ]<\/code><code>    }<\/code><code>  ]<\/code><code>}<\/code><\/p>\n<p>ecr:GetAuthorizationToken has to stay scoped to all resources since AWS doesn\u2019t support restricting that specific action to a repository ARN. It\u2019s how the ECR login token is issued. Everything else is locked to the specific repository naming pattern, not a bare wildcard.<\/p>\n<p>The trust policy below is what actually enforces who can assume this role. It defines exactly which GitHub repository and branch is permitted to authenticate via OIDC. Without this scoping, any workflow in your GitHub organization could potentially assume the role.<\/p>\n<p><code>{<\/code><code>  \"Version\": \"2012-10-17\",<\/code><code>  \"Statement\": [<\/code><code>    {<\/code><code>      \"Effect\": \"Allow\",<\/code><code>      \"Principal\": {<\/code><code>        \"Federated\": \"arn:aws:iam::&lt;COMMON_ACCOUNT_ID&gt;:oidc-provider\/token.actions.githubusercontent.com\"<\/code><code>      },<\/code><code>      \"Action\": \"sts:AssumeRoleWithWebIdentity\",<\/code><code>      \"Condition\": {<\/code><code>        \"StringEquals\": {<\/code><code>          \"token.actions.githubusercontent.com:aud\": \"sts.amazonaws.com\"<\/code><code>        },<\/code><code>        \"StringLike\": {<\/code><code>          \"token.actions.githubusercontent.com:sub\": \"repo:&lt;GITHUB_ORG&gt;\/&lt;GITHUB_REPO&gt;:ref:refs\/heads\/main\"<\/code><code>        }<\/code><code>      }<\/code><code>    }<\/code><code>  ]<\/code><code>}<\/code><\/p>\n<p>The sub condition is what actually locks this down. It ensures that only workflows running against the main branch of this specific repository can assume the role. You can modify this role later or create new ones as you add more GitHub repositories and their corresponding ECR repositories.<\/p>\n<p>Last, make sure \u2018scan on push\u2019 is enabled on your ECR repository. This ensures that every image is automatically scanned for known vulnerabilities the moment it\u2019s pushed, rather than on a periodic schedule. It\u2019s a direct piece of evidence for CC7.1 (vulnerability management).<\/p>\n<h3>ECR Life Cycle Policies<\/h3>\n<p>Set up life cycle policies so your ECR repository doesn\u2019t grow indefinitely and rack up storage costs. Don\u2019t use one blanket rule though, since untagged images, dev builds and promoted releases have different retention needs.<\/p>\n<p>Expire untagged images within a few days; they have no ongoing value. For dev-tagged images, size the retention count to your actual dev cycle rather than guessing a fixed time window. If your team iterates through many builds before settling on one to promote, keep a larger count (500\u20131,000); if your dev cycles are short, a smaller count (100\u2013200) is fine. For staging- and prod-tagged images, keep the last 100\u2013200 releases regardless of age, since these serve as your audit trail.<\/p>\n<p><code>{<\/code><code>  \"rules\": [<\/code><code>    {<\/code><code>      \"rulePriority\": 1,<\/code><code>      \"selection\": {<\/code><code>        \"tagStatus\": \"untagged\",<\/code><code>        \"countType\": \"sinceImagePushed\",<\/code><code>        \"countUnit\": \"days\",<\/code><code>        \"countNumber\": 3<\/code><code>      },<\/code><code>      \"action\": { \"type\": \"expire\" }<\/code><code>    },<\/code><code>    {<\/code><code>      \"rulePriority\": 2,<\/code><code>      \"selection\": {<\/code><code>        \"tagStatus\": \"tagged\",<\/code><code>        \"tagPrefixList\": [\"dev-\"],<\/code><code>        \"countType\": \"imageCountMoreThan\",<\/code><code>        \"countNumber\": 200<\/code><code>      },<\/code><code>      \"action\": { \"type\": \"expire\" }<\/code><code>    },<\/code><code>    {<\/code><code>      \"rulePriority\": 3,<\/code><code>      \"selection\": {<\/code><code>        \"tagStatus\": \"tagged\",<\/code><code>        \"tagPatternList\": [\"*.*.*\"],<\/code><code>        \"countType\": \"imageCountMoreThan\",<\/code><code>        \"countNumber\": 200<\/code><code>      },<\/code><code>      \"action\": { \"type\": \"expire\" }<\/code><code>    }<\/code><code>  ]<\/code><code>}<\/code><\/p>\n<p>ECR evaluates rules in priority order, and each image is expired by the first rule that matches it, so the order and tag prefixes matter. Adjust the dev- prefix and the numbers to match your own tagging convention and release cadence.<\/p>\n<p>Now you can design your pipeline based on how your team works. Here\u2019s one example, assuming you have dev, staging and prod environments.<\/p>\n<h3>CI\/CD Pipeline Architecture Example<\/h3>\n<p>When a pull request merges into main, the pipeline deploys to dev. Use image tagging, for example semver, so each dev build gets a dev- prefix, a patch version and the last seven characters of the commit SHA. Dev image tags in ECR would look like this:<\/p>\n<p><code>dev-0.2.1-a3f9c2e<\/code><code>dev-0.2.2-8d4b17f<\/code><\/p>\n<p>Each of these tags should also be tagged in GitHub so you can show an auditor exactly which commit was running in a given environment at a given time.<\/p>\n<p>Once you\u2019re happy with a dev version and want to promote it to staging, use a manual workflow trigger that retags that specific dev image, passing the target minor version as a parameter (no commit SHA needed). For example, dev-0.2.2-8d4b17f gets a second tag, 0.3.0. This is the same image with two tags, not a new build, which is the point: You\u2019re promoting the exact artifact that was tested in dev, not rebuilding it.<\/p>\n<p>If you find that dev, staging and prod need genuinely different images, that\u2019s usually a sign of a design problem elsewhere in the pipeline. The better fix is to parameterize whatever differs between environments (config, environment variables or feature flags) rather than building separate images per environment.<\/p>\n<p>Say staging runs integration tests, load tests and whatever else is part of your pipeline, and you\u2019re ready to promote to prod. Use another manual workflow trigger, ideally with a required approval step that retags the same image with a version bump. For example, 0.3.0 gets a third tag, 1.0.0. At this point, the same image carries three tags: dev-0.2.2-8d4b17f, 0.3.0 and 1.0.0.<\/p>\n<p>Now you\u2019re running the same artifact in prod that was tested in dev and staging. It\u2019s also traceable back to the exact code version running on GitHub, satisfying CC8.1 (change management).<\/p>\n<h3>Conclusion<\/h3>\n<p>This covers a common CI\/CD pattern, not a one-size-fits-all blueprint. Your account structure, tagging scheme and life cycle rules should reflect how your team actually works. Review this against your own SOC 2 scope and controls and adjust it where your workflow genuinely differs.<\/p>\n<p>Keep in mind that this is only the CI\/CD part of SOC 2 on AWS. Achieving <a href=\"https:\/\/alandcloud.com\/blogs\/soc2-aws-complete-guide\/\">complete SOC 2 compliance on AWS<\/a> requires considering more details such as GuardDuty, Security hub and more tools.<\/p>\n<p><a href=\"https:\/\/devops.com\/building-soc-2-compliant-ci-cd-pipelines-on-aws-with-github-actions\/\" target=\"_blank\" class=\"feedzy-rss-link-icon\">Read More<\/a><\/p>\n<p>\u200b<\/p>","protected":false},"excerpt":{"rendered":"<p>CI\/CD pipelines make developers\u2019 lives easier and let them ship changes to end users faster. However, they are also part [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4652,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[5],"tags":[],"class_list":["post-4651","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4651","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=4651"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/4651\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/4652"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=4651"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=4651"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=4651"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}