One of the first things that we do during deployment configuration is creating credentials for CI/CD tool.
For AWS, the simplest way is to create IAM User with programmatic keys. They are placed into secrets or environment variables inside CI/CD tool as: AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
. It is called long-term credentials, because there is not expiration time by default. We have to rotate them yourself.
The other, more secure, way is to use short-term credentials which invalidates automatically after specific time.
In this article I will present example solution using *OpenID Connect protocol, CircleCI as deployment platform and AWS as target. I will not dive into protocol details.
Main idea overview
This is simplified diagram that shows how the whole process looks like.
We need 2 things to authorize in AWS:
- OIDC Token
- AWS IAM Role
OIDC Token will be generated by CircleCI, but IAM Role needs to be created by us.
Terraform - create IAM Role & OIDC Provider
main.tf - quite common. Description no needed
terraform {
required_version = "1.1.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = "4.8.0"
}
}
}
provider "aws" {
region = "eu-central-1"
}
variables.tf
We need 3 values to configure our IAM Role & OIDC Provider resources in AWS. circleci_org_id
and circleci_thumbprint
are used to create OIDC Provider, and they are rather static for our purposes (we have multiple repositories/CircleCI projects in the same CircleCI organization).
circleci_project_id
allows us to limit IAM Role to specific projects. We want to authorize to AWS from specific (one or multiple) projects, not the whole organization.
variable "circleci_org_id" {
default = "CHANGE_ME"
description = "CircleCI Organization ID"
}
variable "circleci_thumbprint" {
# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
default = "CHANGE_ME"
description = "Fingerprint of CircleCI servers"
}
variable "circleci_project_id" {
default = "CHANGE_ME"
description = "CircleCI Project ID"
}
oidc.tf
The most important part. There we have OIDC Provider and IAM Role with federated principal and 2 conditions. They are compared with values from OIDC Token. Second condition contains schema:
"org/${var.circleci_org_id}/project/${var.circleci_project_id}/user/*"
It means, that all users that have permissions to specific CircleCI project in specific CircleCI organization are able to successfully run this pipeline. This whole configuration works with Github platform connected to CircleCI, so Github manage permissions.
At end we attach other permissions to role.
resource "aws_iam_openid_connect_provider" "default" {
url = "https://oidc.circleci.com/org/${var.circleci_org_id}"
client_id_list = [var.circleci_org_id]
thumbprint_list = [var.circleci_thumbprint]
}
###########################################################
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "circleci" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"]
principals {
type = "Federated"
identifiers = [
"${aws_iam_openid_connect_provider.default.arn}"
]
}
condition {
test = "StringEquals"
variable = "oidc.circleci.com/org/${var.circleci_org_id}:aud"
values = [var.circleci_org_id]
}
condition {
test = "ForAnyValue:StringLike"
variable = "oidc.circleci.com/org/${var.circleci_org_id}:sub"
values = ["org/${var.circleci_org_id}/project/${var.circleci_project_id}/user/*"]
}
}
}
resource "aws_iam_role" "circleci" {
name = "circleci"
path = "/"
assume_role_policy = data.aws_iam_policy_document.circleci.json
}
resource "aws_iam_role_policy_attachment" "attach_1" {
role = aws_iam_role.circleci.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
CircleCI configuration
.circleci/config.yml
version: 2.1
orbs:
aws-cli: circleci/aws-cli@3.0.0
commands:
aws-oidc-setup:
description: Setup AWS auth using OIDC token
parameters:
aws-role-arn:
type: string
steps:
- run:
name: Get short-term credentials
command: |
STS=($(aws sts assume-role-with-web-identity --role-arn << parameters.aws-role-arn >> --role-session-name "${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}" --web-identity-token "${CIRCLE_OIDC_TOKEN}" --duration-seconds 900 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text))
echo "export AWS_ACCESS_KEY_ID=${STS[0]}" >> $BASH_ENV
echo "export AWS_SECRET_ACCESS_KEY=${STS[1]}" >> $BASH_ENV
echo "export AWS_SESSION_TOKEN=${STS[2]}" >> $BASH_ENV
- run:
name: Verify AWS credentials
command: aws sts get-caller-identity
jobs:
test:
executor: aws-cli/default
steps:
- checkout
- aws-cli/install
- aws-oidc-setup:
aws-role-arn: "arn:aws:iam::XXXXXXXXXXXX:role/circleci"
- run:
name: Test AWS connection
command: aws s3 ls
workflows:
build_and_deploy:
jobs:
- test:
context:
- example-context
Here we have configuration with our custom command used to authorize in AWS. There are few important things:
-
parameters.aws-role-arn
-> previously created IAM Role -
role-session-name
-> it could be anything. It will shows up asuser
field in AWS CloudTrail -
CIRCLE_OIDC_TOKEN
-> it is OIDC Token. It is added by CircleCI only if job uses any context. That's why I addedexample-context
in workflows. This context can be empty also. -
duration-seconds
- time that token is valid
You can check what is inside CIRCLE_OIDC_TOKEN
using SSH in job, and decode it here: jwt.io
circleci@a094c7852509:~$ echo $CIRCLE_OIDC_TOKEN
eyJraWQiOiJhSzlqNmRZQm8zS0R3NGdmV3k4eGRNTFU1UThPeWNzcW44S.......
Documentation
- https://circleci.com/docs/2.0/openid-connect-tokens/
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
It's my first article, so feedback is welcome :)
Top comments (0)