<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Liran Aknin</title>
    <description>The latest articles on DEV Community by Liran Aknin (@liran_aknin_b51925757a6d7).</description>
    <link>https://dev.to/liran_aknin_b51925757a6d7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2054053%2F7cd2e7c6-10c4-4940-abfe-f866f9e0e6fb.png</url>
      <title>DEV Community: Liran Aknin</title>
      <link>https://dev.to/liran_aknin_b51925757a6d7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/liran_aknin_b51925757a6d7"/>
    <language>en</language>
    <item>
      <title>Farewell Static Access Keys</title>
      <dc:creator>Liran Aknin</dc:creator>
      <pubDate>Tue, 03 Dec 2024 09:23:22 +0000</pubDate>
      <link>https://dev.to/liran_aknin_b51925757a6d7/farewell-static-access-keys-5h7m</link>
      <guid>https://dev.to/liran_aknin_b51925757a6d7/farewell-static-access-keys-5h7m</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;CI/CD pipelines frequently require integration with external cloud services to support a wide range of use cases. This may include uploading artifacts to object storage solutions (e.g. Amazon S3, Azure Blob Storage), publishing container images to artifact repositories (e.g. AWS Elastic Container Registry, Google Artifact Registry), or deploying applications to container orchestration platforms (e.g. Amazon ECS, Kubernetes, Azure AKS).&lt;/p&gt;

&lt;p&gt;For example, in AWS, the common practice involves creating an IAM user, assigning the necessary permissions, and generating AWS access keys to facilitate this interaction.&lt;/p&gt;

&lt;p&gt;These keys are then typically stored manually in the CI/CD platform, such as GitHub Secrets, CircleCI Contexts, or similar tools. However, this approach presents several challenges.&lt;/p&gt;

&lt;p&gt;Storing static secrets in third-party platforms poses a significant risk of unauthorized access and potential exposure.&lt;br&gt;
High-profile breaches, such as the &lt;a href="https://circleci.com/blog/jan-4-2023-incident-report/" rel="noopener noreferrer"&gt;CircleCI breach&lt;/a&gt; and &lt;a href="https://about.codecov.io/apr-2021-post-mortem/" rel="noopener noreferrer"&gt;the Codecov supply chain attack&lt;/a&gt;, highlight the appeal of CI/CD services as prime targets for attackers.&lt;br&gt;
With these platforms' critical role in modern development pipelines, the question isn't &lt;strong&gt;&lt;em&gt;"if"&lt;/em&gt;&lt;/strong&gt; but &lt;strong&gt;&lt;em&gt;"when"&lt;/em&gt;&lt;/strong&gt; the next breach will occur.&lt;/p&gt;

&lt;p&gt;If not handled perfectly, rotating these keys frequently can result in unexpected downtimes or operational challenges. Even small errors during the rotation process can disrupt workflows and cause delays. Moreover, the process of key rotation demands some time investment, diverting valuable engineering resources from other critical tasks.&lt;br&gt;
Given the sensitive nature of CI/CD pipelines, it is evident that relying on such fragile and resource-intensive practices is a considerable risk.&lt;/p&gt;

&lt;p&gt;Luckily, there is a better approach. OpenID Connect (OIDC) offers a solution by replacing static credentials with short-lived tokens.&lt;br&gt;
These tokens are dynamically issued during each workflow run, removing the need for long-lived secrets in CI/CD platforms and significantly enhancing security and operational efficiency.&lt;/p&gt;

&lt;p&gt;In this blog, I’ll demonstrate how I set up &lt;strong&gt;GitHub Actions&lt;/strong&gt; to securely interact with &lt;strong&gt;AWS&lt;/strong&gt; resources using OIDC.&lt;br&gt;
My specific use case involves uploading files to a website hosted on S3 and clearing the CloudFront cache to ensure the new content is served.&lt;/p&gt;

&lt;p&gt;By the end of this blog, you’ll gain a clear understanding of how OIDC overcomes the challenges discussed earlier.&lt;/p&gt;

&lt;p&gt;Before diving into the technical details of this setup, let’s take a step back to explore what OIDC is and how it works so that we can lay a solid foundation for the implementation.&lt;/p&gt;
&lt;h1&gt;
  
  
  Understanding OIDC
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://openid.net/developers/how-connect-works/" rel="noopener noreferrer"&gt;OpenID Connect (OIDC)&lt;/a&gt; is a protocol built on top of &lt;a href="https://datatracker.ietf.org/doc/html/rfc6749" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; that simplifies verifying the identity of users or services. Instead of relying on static credentials like passwords or API keys, OIDC uses tokens to provide secure access via short-lived credentials.&lt;/p&gt;

&lt;p&gt;Using an OIDC Identity Provider allows you to manage external identities, such as users or services, to access your internal resources without the need to create and maintain separate internal user accounts.&lt;/p&gt;

&lt;p&gt;To illustrate this, consider the analogy of a pass for a convention or conference. The event spans multiple days with various sessions, but access to specific sessions requires an appropriate pass. This pass contains your name (identity), your registration type (role or scope), and a unique QR code (short-lived credentials) to grant you access to the sessions. The pass is only valid for the event and time it’s issued.&lt;/p&gt;

&lt;p&gt;If we extend this analogy to our use case, GitHub workflows represent the attendees needing access to the specific event sessions (AWS resources).&lt;br&gt;
To gain access, the workflows request a pass from the GitHub OIDC provider, which issues an OIDC token in the form of a &lt;a href="https://datatracker.ietf.org/doc/html/rfc7519" rel="noopener noreferrer"&gt;JSON Web Token (JWT)&lt;/a&gt;. This token includes key details, called &lt;a href="https://openid.net/specs/openid-connect-core-1_0.html#Claims" rel="noopener noreferrer"&gt;claims&lt;/a&gt;, about the specific request.&lt;/p&gt;

&lt;p&gt;Lets focus on the main ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iss&lt;/code&gt;: The issuer of the token (GitHub OIDC provider). This claim, along with other header parameters such as &lt;code&gt;alg&lt;/code&gt; (algorithm) and &lt;code&gt;kid&lt;/code&gt; (key ID), helps AWS validate the token's origin and verify its authenticity.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub&lt;/code&gt;: The subject of the token. Represents the unique workflow making the request. It includes metadata like the repository, branch, and triggering event.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aud&lt;/code&gt;: The audience of the token. Specifies who the token is intended for (default to the URL of the repository owner). In my case, because I use the official &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt; action in my workflow, it must be set to AWS Security Token Service - &lt;code&gt;sts.amazonaws.com&lt;/code&gt;. More on that later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS then validates the OIDC token’s details against its predefined trust configuration. If the token’s claims match, AWS generates a temporary set of credentials. These credentials provide the workflow with secure, time-limited access to the specified resources.&lt;/p&gt;
&lt;h1&gt;
  
  
  Putting It All Together
&lt;/h1&gt;

&lt;p&gt;Now with the basic understanding of what OIDC is, it’s time to piece everything together.&lt;/p&gt;

&lt;p&gt;I will use Terraform, my preferred tool for provisioning infrastructure. However, the same setup can be achieved using the AWS Management Console, the AWS Command Line Interface (CLI), or Tools for Windows PowerShell.&lt;br&gt;
Detailed instructions can be found in the official &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;It is important to note that support for &lt;a href="https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token" rel="noopener noreferrer"&gt;custom claims&lt;/a&gt; for GitHub OIDC is unavailable in AWS&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building The Trust
&lt;/h2&gt;

&lt;p&gt;The first step is to add the GitHub OIDC Provider as a trusted identity Provider (IdP) in AWS. This is done by creating an OIDC provider entity in AWS IAM.&lt;br&gt;
By doing so, we set the foundations to establish trust between our AWS account and GitHub’s OIDC provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_iam_openid_connect_provider" "github_actions_oidc_provider" {
        url   = "https://token.actions.githubusercontent.com"

        client_id_list = [
            "sts.amazonaws.com",
        ]

        thumbprint_list = ["33e4e80807204c2b6182a3a14b591acd25b5f0db"] # Thumbprint of the GitHub OIDC provider's Intermediate Certificate
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OIDC provider resource has three key components that AWS uses during the validation process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt;: The OIDC provider URL, which corresponds to the &lt;code&gt;iss&lt;/code&gt; (Issuer) claim in the token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client_id_list&lt;/code&gt;: These correspond to the &lt;code&gt;aud&lt;/code&gt; (Audience) claim. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;thumbprint_list&lt;/code&gt;: A list of certificate thumbprints for AWS to validate the OIDC provider’s SSL certificate. This ensures that the OIDC provider is trusted.
You can follow &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html" rel="noopener noreferrer"&gt;this procedure&lt;/a&gt; to get the thumbprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creating a Role and Trust Policy
&lt;/h2&gt;

&lt;p&gt;Once the OIDC provider is configured in AWS, the next step is to create an IAM role.&lt;br&gt;
Unlike a user, an IAM role does not have long-term credentials. Instead, it is a temporary identity assigned to a federated user, such as a GitHub workflow, after successful authentication by the Identity Provider (IdP).&lt;/p&gt;

&lt;p&gt;The role defines the trust relationship, allowing the workflow to request temporary security credentials from AWS STS using a token issued by the IdP. These credentials grant access to AWS resources, with the scope of access determined by the policies attached to the role.&lt;/p&gt;

&lt;p&gt;Restricting which entities can assume the role by defining conditions in the trust policy is a best practice.&lt;br&gt;
For example, you can use the &lt;code&gt;token.actions.githubusercontent.com:sub&lt;/code&gt; condition key with &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_String" rel="noopener noreferrer"&gt;string operators&lt;/a&gt; to limit access to a specific GitHub organization, repository, or branch.&lt;br&gt;
This ensures that only trusted repositories can request tokens for your resources. In fact, in our use case, IAM will enforce that the &lt;code&gt;token.actions.githubusercontent.com:sub&lt;/code&gt; condition key is defined and prohibits the use of wildcard (&lt;code&gt;*&lt;/code&gt; or &lt;code&gt;?&lt;/code&gt;) or &lt;code&gt;null&lt;/code&gt; values. If not explicitly set, any workflow or GitHub organization would be able to request access using the role name. Therefore, to prevent unauthorised access we would want to set it at least to our github organization/account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_iam_role" "github_actions_oidc_role" {
        name = "github-actions-oidc-role"

        assume_role_policy = jsonencode({
            "Version": "2012-10-17",
            "Statement": [
            {
                "Effect": "Allow",
                "Principal": {
                    "Federated": "${aws_iam_openid_connect_provider.github_actions_oidc_provider.arn}"
                },
                "Action": "sts:AssumeRoleWithWebIdentity",
                "Condition": {
                    "StringEquals": {
                        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                    },
                    "StringLike": {
                        "token.actions.githubusercontent.com:sub": "repo:GitHubOrg/GitHubRepo:ref:refs/heads/GitHubBranch" # Restrict access to a specific GitHub repository and branch
                    }
                }
            }
            ]
        })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Some Tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Using Locals
&lt;/h3&gt;

&lt;p&gt;Here’s an approach to restrict access to workflows from specific GitHub repositories, regardless of the branch, by leveraging &lt;a href="https://developer.hashicorp.com/terraform/language/values/locals" rel="noopener noreferrer"&gt;Terraform locals&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Environment-specific variable for repository list
    variable "github_repos" {
        type = list(string)
        default = [
          "repo1",
          "repo2",
          "repo3"
        ]
    }

      locals {
        repo_sub_list = [for repo in var.github_repos : "repo:GitHubUser/${repo}:*"]
      }

      # Use the local in the trust policy condition
        "StringLike": {
                  "token.actions.githubusercontent.com:sub": local.repo_sub_list
         }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach becomes especially handy for managing resource access across multiple environments. It allows you to bind specific repositories to environments based on the required access.&lt;br&gt;
It also makes it more readable and maintainable.&lt;/p&gt;

&lt;p&gt;You can explore &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html#idp_oidc_Create_GitHub" rel="noopener noreferrer"&gt;this page&lt;/a&gt; to see some examples of how you might use these conditions in the GitHub OIDC trust policy.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using ABAC
&lt;/h3&gt;

&lt;p&gt;In larger environments, Attribute-Based Access Control (ABAC) can greatly simplify access management by utilizing tags in IAM policies.&lt;br&gt;
Tags allow you to logically group resources, eliminating the need to explicitly list individual resources in your policies.&lt;/p&gt;

&lt;p&gt;In fact, in a &lt;a href="https://dev.to/liran_aknin_b51925757a6d7/how-i-set-up-cross-account-ecr-access-for-lambda-functions-opl"&gt;previous blog&lt;/a&gt;, I demonstrated how to control Lambda access to ECR repositories based on their tags.&lt;/p&gt;

&lt;p&gt;To learn more, check out the AWS documentation on &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_attribute-based-access-control.html" rel="noopener noreferrer"&gt;ABAC&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_iam-tags.html" rel="noopener noreferrer"&gt;access control with tags&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating The Policies
&lt;/h2&gt;

&lt;p&gt;Once the role and its trust policy are in place, the next step is to &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html#access_policies_create-start" rel="noopener noreferrer"&gt;create your policies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In my use case, as described earlier, I need the workflow to upload files to an S3 bucket and create a CloudFront invalidation to clear the cache.&lt;/p&gt;

&lt;p&gt;Here is a sample policy that allows such actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
              ],
              "Resource": [
                "arn:aws:s3:::BucketName/*",
                "arn:aws:s3:::BucketName"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "cloudfront:CreateInvalidation"
              ],
              "Resource": "arn:aws:cloudfront::AccountID:distribution/DistributionID"
            }
          ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring GitHub Workflows for OIDC Integration
&lt;/h2&gt;

&lt;p&gt;Great! We’re making steady progress.&lt;/p&gt;

&lt;p&gt;With the AWS setup complete, we’ve configured it to validate OIDC tokens and issue short-lived credentials. Now, it’s time to &lt;a href="https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#updating-your-github-actions-workflow" rel="noopener noreferrer"&gt;update our GitHub workflows&lt;/a&gt; to request these tokens and utilize the generated credentials within the workflow jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining Permissions
&lt;/h3&gt;

&lt;p&gt;The first step is to define the necessary permissions in the workflow’s YAML configuration file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permissions:
  id-token: write
  contents: read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;id-token: write&lt;/code&gt; permission allows the workflow to request an OIDC token from the GitHub OIDC provider.&lt;br&gt;
The &lt;code&gt;contents: read&lt;/code&gt; is needed if your workflow uses the &lt;code&gt;actions/checkout&lt;/code&gt; action to access repository contents.&lt;/p&gt;
&lt;h3&gt;
  
  
  Requesting Access
&lt;/h3&gt;

&lt;p&gt;With the permissions configured, the next step is to utilize the OIDC token within the workflow job. The &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt; action simplifies this process by retrieving the JWT from GitHub’s OIDC provider and exchanging it for temporary credentials from AWS STS.&lt;br&gt;
You can find more details about this action in the &lt;a href="https://github.com/aws-actions/configure-aws-credentials" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is an example of how its implemented in the workflow file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v3
  with:
    role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-actions-oidc-role
    aws-region: ${{ env.AWS_REGION }}

- name: Sync to S3
  run: aws s3 sync . s3://${{ env.S3_BUCKET_NAME }} --delete


- name: CloudFront Cache Invalidation
  run: aws cloudfront create-invalidation --distribution-id ${{ env.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Configure AWS Credentials&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assumes the specified IAM role (&lt;code&gt;github-actions-oidc-role&lt;/code&gt;) using the OIDC token and retrieves short-lived credentials valid for the duration of the workflow run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Sync to S3&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses the temporary credentials to upload files to the S3 bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;CloudFront Cache Invalidation&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses the temporary credentials to clear the CloudFront cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  And That’s It!
&lt;/h1&gt;

&lt;p&gt;By combining OpenID Connect (OIDC) with GitHub Actions and AWS, we’ve built a secure, seamless workflow that eliminates the need for static credentials. Instead, we’re using dynamically generated, short-lived tokens to ensure secure access to AWS resources.&lt;/p&gt;

&lt;p&gt;Following these steps, both improves the security of your workflows and simplifies credential management. Whether your workflows are deploying applications, managing infrastructure, or updating content like in my use case, this setup ensures that sensitive credentials remain out of reach.&lt;/p&gt;

&lt;p&gt;Oh, and don’t forget to delete those static credentials from your CI/CD platform, you won’t need them anymore! 😉&lt;/p&gt;

&lt;p&gt;Thank you for reading. I hope you found it helpful and enjoyable!&lt;br&gt;
I’d love to hear how you’ve implemented similar setups in your own use cases. Whatever it is... feel free to drop a comment below. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>githubactions</category>
      <category>idp</category>
    </item>
    <item>
      <title>How I Set Up Cross-Account ECR Access for Lambda Functions</title>
      <dc:creator>Liran Aknin</dc:creator>
      <pubDate>Sat, 16 Nov 2024 19:19:41 +0000</pubDate>
      <link>https://dev.to/liran_aknin_b51925757a6d7/how-i-set-up-cross-account-ecr-access-for-lambda-functions-opl</link>
      <guid>https://dev.to/liran_aknin_b51925757a6d7/how-i-set-up-cross-account-ecr-access-for-lambda-functions-opl</guid>
      <description>&lt;h2&gt;
  
  
  The Use Case
&lt;/h2&gt;

&lt;p&gt;Recently, I took on a project to deploy a new Lambda function in each of my AWS accounts to serve as a filter for Guard Duty alerts. Based on internal logic, this function would determine whether it requires immediate handling or can be addressed later. Because this function needed to be deployed across multiple accounts and regions (basically in each of my accounts), I faced the challenge of ensuring it could access a container image stored in a central Elastic Container Registry (ECR) repository in one of my AWS accounts. &lt;br&gt;
This blog will focus on setting up cross-account access to ECR with region replication, rather than the specifics of my GuardDuty setup, which I might cover in a future post.&lt;/p&gt;

&lt;p&gt;Reading the &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/replication.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt; gave me a good sense of what configurations are necessary. Nonetheless, in this blog I will show you how I did it - &lt;em&gt;the terraform way&lt;/em&gt;, while providing some tips and tricks to achieve the solution for my use case. &lt;br&gt;
Terraform, an Infrastructure as Code (IaC) tool, provides a consistent and automated way to manage resources across multiple AWS accounts and regions. While this blog won't dive into all the details of Terraform, you can learn more about it &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is worth mentioning, that this blog requires a basic understanding of Terraform.&lt;/p&gt;
&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Many organizations in today’s cloud environments centralize container images within a single AWS account to streamline management, versioning, and security. However, additional configurations are required when Lambda functions across different accounts and regions need access to these images.&lt;/p&gt;

&lt;p&gt;Following AWS’s &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;same-region access&lt;/a&gt; requirement, each Lambda function must be able to access the image in the same region as its deployment. For that reason, I used the ECR’s region replication feature. This allowed the container image to be replicated to the necessary region.&lt;/p&gt;
&lt;h2&gt;
  
  
  A High-Level Overview of the Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9tsy5z6jufk1lvaxp49.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9tsy5z6jufk1lvaxp49.jpeg" alt="&amp;lt;br&amp;gt;
The image illustrates an AWS architecture for cross-account and cross-region ECR (Elastic Container Registry) image replication. Account 111111111111 hosts an ECR registry in us-east-2, replicating images to eu-west-1. Lambda functions in Account 123456789012 (us-east-2) and Account 222222222222 (eu-west-1) pull images from the closest ECR registry." width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have a high-level idea of what must be done, let's jump into the setup.&lt;/p&gt;
&lt;h3&gt;
  
  
  Some Facts to Consider
&lt;/h3&gt;

&lt;p&gt;I manage multiple repositories in Account ID &lt;code&gt;111111111111&lt;/code&gt; region &lt;code&gt;us-east-2&lt;/code&gt;. Some of my repositories will require Lambda access to pull their images, but not all of my repositories' images will require a replication. However, all repositories replicated to another region will also need lambda access.&lt;/p&gt;

&lt;p&gt;In short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda access ≠ replicating an image&lt;/li&gt;
&lt;li&gt;Replicating an image = Lambda access&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The Terraform module iterates over a list of repository names (&lt;code&gt;var.repository_names&lt;/code&gt;) using &lt;a href="https://developer.hashicorp.com/terraform/language/meta-arguments/for_each" rel="noopener noreferrer"&gt;for_each&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each repository is tagged with: &lt;code&gt;Lambda-Access = true/false&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The tag value is determined dynamically using the &lt;a href="https://developer.hashicorp.com/terraform/language/functions/lookup" rel="noopener noreferrer"&gt;lookup&lt;/a&gt; function on a map (&lt;code&gt;var.ecr_lambda_access&lt;/code&gt;) that specifies which repositories should have Lambda access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ecr_lambda_access = {
        "repo_name_1"  = "true"
        "repo_name_2"  = "true"
        "repo_name_3"  = "true"
}    
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_ecr_repository" "example_registry" {
        for_each        = toset(var.repository_names)
        name            = each.value

        tags = {
          Lambda-Access = lookup(var.ecr_lambda_access, each.value, false)
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To grant cross-account and same-account access to ECR repositories, along with Lambda-specific access, we add three statements to the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy" rel="noopener noreferrer"&gt;aws_ecr_repository_policy&lt;/a&gt; resource:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-account&lt;/strong&gt; access grants general access to all repositories for specific external accounts.&lt;br&gt;
&lt;strong&gt;Cross-account Lambda&lt;/strong&gt; access grants Lambda functions in external accounts access to repositories tagged with &lt;code&gt;Lambda-Access = true&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Same-account Lambda&lt;/strong&gt; access grants Lambda functions in the same account access to repositories tagged with &lt;code&gt;Lambda-Access = true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s the complete policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "Version": "2008-10-17",
    "Statement": [
        {
        "Sid": "CrossAccountAccess",
        "Effect": "Allow",
        "Principal": {
            "AWS": [
            "arn:aws:iam::123456789012:root",
            "arn:aws:iam::222222222222:root"
            ]
        },
        "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability",
        "ecr:ListImages"
        ]
    },
    {
        "Sid": "LambdaECRImageCrossAccountRetrievalPolicy",
        "Effect": "Allow",
        "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "ecr:ListImages"
        ],
        "Principal": {
        "Service": "lambda.amazonaws.com"
        },
        "Condition": {
        "StringLike": {
            "aws:sourceArn": "arn:aws:lambda:us-east-2:123456789012:function:*"
        },
        "StringEquals": {
            "aws:ResourceTag/Lambda-Access": "true"
        }
        }
    },
    {
        "Sid": "LambdaECRSameAccountImageRetrievalPolicy",
        "Effect": "Allow",
        "Principal": {
        "Service": "lambda.amazonaws.com"
        },
        "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
        ],
        "Condition": {
        "StringEquals": {
            "aws:ResourceTag/Lambda-Access": "true"
        }
        }
    }
    ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After solving cross-account and same-account access for the default region, we address cross-account, same-region access using the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration" rel="noopener noreferrer"&gt;aws_ecr_replication_configuration&lt;/a&gt; resource.&lt;/p&gt;

&lt;p&gt;We replicate only to specific regions where the Lambda function resides. In our case, it is &lt;code&gt;eu-west-1&lt;/code&gt; in account ID &lt;code&gt;222222222222&lt;/code&gt;. The replication configuration includes a rule specifying destination regions and repository filters.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks" rel="noopener noreferrer"&gt;dynamic block&lt;/a&gt; is used to iterate over &lt;code&gt;var.repository_to_replicate&lt;/code&gt;, a list of repository prefixes to include in replication. It avoids hardcoding multiple &lt;code&gt;repository_filter&lt;/code&gt; blocks, making the configuration scalable and easier to manage as the list of repositories grows.&lt;/p&gt;

&lt;p&gt;For each prefix, it dynamically generates a &lt;code&gt;repository_filter&lt;/code&gt; block with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;filter&lt;/code&gt;: The prefix for repositories to match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;filter_type&lt;/code&gt;: Set to &lt;code&gt;PREFIX_MATCH&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecr-replicationconfiguration-repositoryfilter.html" rel="noopener noreferrer"&gt;filter&lt;/a&gt; will control which repositories will be replicated.&lt;/p&gt;

&lt;p&gt;Here’s the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_ecr_replication_configuration" "example" {
        replication_configuration {
          rule {
            destination {
              region      = "eu-west-1"
              registry_id = data.aws_caller_identity.current.account_id
            }
            dynamic "repository_filter" {
              for_each = toset(var.repository_to_replicate)
              content {
                filter      = repository_filter.value
                filter_type = "PREFIX_MATCH"
              }
            }
          }
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Region-Specific Repository Policies
&lt;/h3&gt;

&lt;p&gt;We also need to define region-specific repository policies for the replicated repositories. This ensures that Lambda, in those regions, can access the replicated repositories.&lt;/p&gt;

&lt;p&gt;To achieve this, we leverage the &lt;a href="https://developer.hashicorp.com/terraform/language/providers/configuration#alias-multiple-provider-configurations" rel="noopener noreferrer"&gt;aws provider's alias&lt;/a&gt; for the replicated region (e.g. &lt;code&gt;eu-west-1&lt;/code&gt;) and create repository policies for the replicated repositories.&lt;/p&gt;

&lt;p&gt;Here's how it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider "aws" {
        alias  = "ireland_region"
        region = "eu-west-1"
}

resource "aws_ecr_repository_policy" "elastic_container_registry_policy_ireland" {
        for_each   = toset(var.repository_to_replicate)
        provider   = aws.ireland_region
        repository = each.value
        depends_on = [module.elastic_container_registry]
        policy     = &amp;lt;&amp;lt;EOF
        {
        "Version": "2012-10-17",
            "Statement": [
            {
                "Sid": "repositoryPerRegionStatement",
                "Effect": "Allow",
                "Principal": {
                "Service": "lambda.amazonaws.com",
                "AWS": "arn:aws:iam::222222222222:root"
                },
                "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:BatchGetImage",
                "ecr:GetDownloadUrlForLayer",
                "ecr:ListImages"
                ]
            }
            ]
        }
        EOF
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach keeps our configuration modular and ensures seamless access to replicated resources across regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That’s how I tackled the challenge of setting up cross-account and cross-region access for Lambda functions to pull container images from a centralized ECR repository, using a scalable and modular solution with Terraform.&lt;/p&gt;

&lt;p&gt;I hope this blog provided some insights or inspiration for your own projects. Feel free to share your thoughts or questions—I’d be happy to hear from you!&lt;/p&gt;

&lt;p&gt;Thank you for reading!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>automation</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
