Only registred users can make comments

Unlock the Power of AWS and OIDC in GitHub Actions - Part 1 of 2

TL:DR

With GitHub Actions, you can authenticate users and services using OIDC and assume a role in AWS IAM, allowing access to AWS resources.

Welcome to this two-part article series on OpenID Connect (OIDC) and its benefits for federated identity management. In Part 1, we'll dive into the why and how of using OIDC instead of creating users in AWS account for granting permissions. We'll also look at a high-level overview of a small solution design and functionality that we will build in next part.

In Part 2 we'll walk through a step-by-step tutorial on deploying IAM resources with Terraform, writing a simple Lambda function, and creating a GitHub action file with three separate jobs. These jobs will use short-lived tokens to create a Lambda resource in AWS by assuming a role that we'll create. 

We will utilize the following technologies to accomplish it:

  • Terraform for creating IAM resources within AWS,
  • GitHub Actions for automating the process, and a straightforward
  • Lambda function as a resource in AWS

Content

This part has the following content:

 

Prerequsite

This article assumes some prior knowledge of the following technologies:

  • GitHub: creating a repository and committing code.
  • Terraform and Infrastructure as Code (IaC): even though detailed instructions are provided, it's helpful to have some understanding.
  • AWS: navigating the management console and basic IAM knowledge.

Analogy

The following diagram presents an analogy to explain the use of OIDC to assume an AWS role:

 

 

  1. Jan can currently only watch a wall  (which can be seen as a resource)
  2. Jan wants sometimes to draw something and asks if he can do so.
  3. Bob, Jan's manager says it's ok and creates an entity that will provide Jan some short-lived access keys to assume a role with drawing permissions. 
  4. Every time Jan wants to draw something, he uses the keys to assume the role.
  5. The role has permissions associated with it which Jan can use to draw on the wall.
  6. Jan gets short-lived access keys to assume the role with the drawing permission.

Please note! The role is not attached to Jan user object or to a group that Jan belongs to. The role can only be assumed when needed. 

Why use OIDC to assume an AWS role instead of using aws_access_key_id and aws_secret_access_keys?

One key advantage is that your user or service does not require any role or policy to be directly attached to itself.

This eliminates a need for direct access and instead relies on short-lived credentials obtained by assuming a role.

Some more advantages:

  • Security: OIDC integrates with your identity provider to secure the authentication process, providing a more secure way to manage AWS access compared to using long-lived access keys.
  • Increased control: OIDC allows you to control who can access your AWS resources by specifying identity-based policies and permissions.
  • Improved manageability: OIDC enables you to manage AWS access for multiple users in a centralized and consistent manner.
  • Audibility: OIDC provides a clear audit trail of who performed actions in your AWS account, making it easier to meet regulatory compliance requirements.
  • Scalability: OIDC can scale to accommodate a large number of users and resources, making it a suitable option for organizations of all sizes.

Introduction

One common way to access the AWS API is to use the access key id and secret access key. Even if this works well, the best practice is to grant the least privileged access and use role assumption wherever possible, instead of providing long-term access keys.

You can access AWS resources without using user credentials in GitHub Actions.

Assuming a role in AWS Identity and Access Management (IAM) means that a user or an application is temporarily taking on the permissions and permissions boundaries of another IAM entity, called the "role." This allows a user or application to perform actions on AWS resources that the role has access to, without having to be granted those permissions directly.

When a user or application assumes a role, AWS generates temporary security credentials for the role and provides them to the requesting entity. 

These temporary credentials are called "role session credentials" and consist of an access key, a secret key, and a session token.

These credentials are valid for a short period of time (short-lived, like one hour)


ref: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html


Assuming a role is helpful in several scenarios, such as:

  • Sharing access to AWS resources across multiple users or applications
  • Granting users or applications access to resources in another account
  • Allowing users or applications to perform actions on resources with temporary permissions

❗It is important to note that when you assume a role, you inherit the permissions and permissions boundaries of the role, but not the permissions policies of the role's users or groups.

In this tutorial, we will perform the following:

  • Set up OpenID Connect (OIDC) provider in AWS
  • Define a Trust Policy for the OIDC provider, which enables it to assume a role in the AWS account
  • Create an IAM role that allows the OIDC provider to assume a role in the AWS account
  • Create an IAM role policy that is attached to the role
  • Write a simple Lambda function that we will deploy through GitHub Actions
  • Write a GitHub Actions workflow file that will deploy the Lambda function. The jobs used will use short-lived cloud access token provided by AWS. The access will be based on the assumed role

❗We will unfortunately have to break one of the rules in this tutorial. We will use the access key id and secret access key only once to set up our initial IAM resources via Terraform.  It will be a one-time exception.

Source Code

You can find the source code under the following link:

https://github.com/devoriales/oidc-aws-github

OpenID Connect (OIDC) - What Is It?

OpenID Connect (OIDC) is a protocol for authenticating users and it can be used to authenticate users and obtain a set of temporary security credentials that can be used to access AWS resources.

Instead of using IAM roles, you can use OIDC to authenticate users and obtain temporary security credentials that are valid for a period of time. These credentials are called "federated credentials" and they consist of an access key, a secret key, and a session token.

To use OIDC to authenticate users and obtain temporary security credentials in a GitHub Action, you would need to use the AWS SDKs or the AWS CLI and the aws sts get-federation-token command. Then, you can set the resulting temporary credentials as environment variables in your GitHub Action workflow, and use them to interact with AWS services in subsequent steps of your workflow.

The following high-level diagram shows what this project is intending to do and what we could expect from the process:

 

  1. The GitHub Actions job requests a token from GitHub's OIDC provider
  2. GitHub's OIDC Provider generates an OIDC token and provides it to the job
  3. The job provides its credentials along with the OIDC token to AWS. (Here comes what needs to be defined in the Role in AWS)
  4. AWS validates the OIDC token and if it is coming from a trusted source, it issues a short-lived cloud access token. In this case, GitHub is trustworthy and we will learn later how to set up GitHub as a Identity Provider in AWS.
  5. The job uses a short-lived token to perform tasks on the resources it has access to, via the assumed role

The OIDC token and job credentials obtained by the GitHub Actions job are only valid for the duration of the job execution. 

The job can only access the AWS resources for which the assumed role has the necessary permissions.

The OIDC token is included in the request header and is used by AWS to verify the authenticity of the request.

This ensures that only authorized actions can be performed on the resources.

Trust Policy 

In AWS, a trust policy is a document that outlines the permissions granted to other entities, such as other AWS accounts, to access resources within your own AWS account. These policies are written in JSON and are linked to both the principal (the entity receiving permissions) and the trustor (the entity granting permissions). Trust policies play a key role in various AWS services (such as Lambda, IAM, S3, and SNS), to manage access and maintain security for resources.


❗If you want to learn more about how to configure OpenID Connect  (OIDC) in Amazon Web Services, please read this official documentation


Based on research on GitHubs web page, we want to create the following trust policy via Terraform:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<xxxxxxx>:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringLike": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                    "token.actions.githubusercontent.com:sub": "repo:devoriales/oidc-aws-github:*"
                }
            }
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

In the policy above, we have defined two statements:

  • The first statement: The "Principal" field is set to the ARN of the GitHub OIDC provider, and the "Action" field is set to "sts:AssumeRoleWithWebIdentity", which states that the GitHub OIDC provider is allowed to assume a role with web identity
    The "Condition" field establishes additional conditions for assuming the role, such as the "aud" claim in the token must be "sts.amazonaws.com" and the "sub" claim must match the specified repository

  • The second statement: The "Principal" field is set to "lambda.amazonaws.com", which states that AWS Lambda services will be granted permission. The "Action" field is set to "sts:AssumeRole", which states that the AWS Lambda service is allowed to assume a role.

This policy allows the GitHub OIDC provider and AWS Lambda services to assume a role in the AWS account, which grants them access to the resources in that account.

This trust policy must be associated with a role in IAM, which is being assumed by the principal entities (GitHub OIDC provider and AWS Lambda services).


StringEquals vs StringLike conditions

Let's learn the difference between StringEquals and StringLike conditions. In the Trust Policy example above, we had the StringLike condition set, but we could have used StringEquals. The difference is:

  • The "StringEquals" function compares the value of a key-value pair in the JWT with the value specified in the trust policy and returns true if they match exactly. It's useful when you want to specify a strict match between the JWT claim and the value you're comparing it to.

  • The "StringLike" function allows you to specify a pattern of characters that must match the value of a key-value pair in the JWT. It uses the wildcard character (*) to match any string of characters. It's useful when you want to grant access to a group of resources with a common pattern in the name or id. For example, the second key-value pair specifies that the "sub" claim must match the pattern "repo:devoriales/oidc-aws-github:*". This claim represents the subject of the token, it's used to identify the principal that is the subject of the token.

What is AssumeRoleWithWebIdentity?

In AWS, the "AssumeRoleWithWebIdentity" action allows an identity provider (IdP) such as Facebook, Google, or OpenID Connect (OIDC) provider to authenticate the application and then assume a role in the AWS account on behalf of that application. This allows the application to access the resources in the AWS account that are defined in the permissions of the assumed role.

What are "sub" and "aud"  ?

"sub" and "aud" are common terms used in the context of OAuth 2.0 and OpenID Connect (OIDC) authentication protocols.

In our trust policy, we have the following statement:

                 "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
                    "token.actions.githubusercontent.com:sub": "repo:devoriales/oidc-aws-github:*"
  • The "aud" (audience) in a trust policy is the intended audience for the temporary credentials that are issued when the role is assumed. "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" specifies that the intended audience for the temporary credentials that are issued when the role is assumed is the AWS STS service.
  • The "sub" (principal) in a trust policy is the entity that is allowed to assume the role.if you want to allow an IAM service with the ARN "arn:aws:iam::123456789012:user/Jan" to assume a role, the trust policy would include the "sub" condition: "sub": "arn:aws:iam::123456789012:user/Jan". "repo:devoriales/oidc-aws-github:" specifies that the entity that is allowed to assume the role is a GitHub Actions token associated with the "devoriales/oidc-aws-github" repository.

Permission Policy

The permission policy attached to a role is a JSON document that defines the permissions that are allowed or denied for the role.

Now we will have a look at the Permission Policy that we would like to implement:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "iam:PassRole"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:iam::xxxxxxxx:role/oidc-gh-role",
            "Sid": "VisualEditor0"
        },
        {
            "Action": [
                "lambda:CreateFunction",
                "lambda:InvokeFunction",
                "lambda:UpdateFunctionCode",
                "lambda:DeleteFunction",
                "lambda:GetFunction"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:lambda:eu-west-1:xxxxxxxx:role:function:*",
            "Sid": "VisualEditor1"
        }
    ]
}

The resource specified is "arn:aws:iam::xxxxxxxx:role/oidc-gh-role" which is the Amazon Resource Name (ARN) of the role that is being passed.

The policy allows several Lambda actions (CreateFunction, InvokeFunction, UpdateFunctionCode, DeleteFunction, GetFunction) on all resources that have an ARN that starts with "arn:aws:lambda:eu-west-1:xxxxxxxx:function". 

Now we have learned some theory about OIDC. In the next section, we will perform the implemenetation.

In part 2, we will actually build what we have learned. Welcome to join. 

About the Author

Aleksandro Matejic, a Cloud Architect, began working in the IT industry over 21 years ago as a technical specialist, right after his studies. Since then, he has worked in various companies and industries in various system engineer and IT architect roles. He currently works on designing Cloud solutions, Kubernetes, and other DevOps technologies.

In his spare time, Aleksandro works on different development projects such as developing devoriales.com, a blog and learning platform launching in 2022/2023. In addition, he likes to read and write technical articles about software development and DevOps methods and tools. You can contact Aleksandro by visiting his LinkedIn Profile.

Comments