One of the challenges we are facing at globaldatanet is how we should give access to CI systems and their runners in a secure way.
Fixed access using IAM users looks so bad these days, with all the drawbacks that it comes with, meaning how you should store and rotate your fixed IAM keys in your CI system, not mentioning the security issues if someone compromises your CI system.
In this blog post, we will demonstrate how to use OpenID Connect (OIDC) to allow your GitLab runners pipelines to access resources in Amazon Web Services (AWS), without needing to store the AWS credentials as long-lived GitLab environment secrets.
Also, we will see how to take this to the next level and allow cross-account access with OIDC and IAM Roles, to make it super simple and secure to deploy to organization-based multiple accounts.
Goal: Using OIDC and AWS SSO, we ensure to always use STS temporary credentials when interacting with AWS resources across accounts
Since we are in love with Terraform, we will use it to configure OIDC, you can create this as a module and add it to your IaC library.
data "aws_partition" "current" {}
data "tls_certificate" "gitlab" {
url = var.gitlab_url
}
resource "aws_iam_openid_connect_provider" "gitlab" {
url = var.gitlab_url
client_id_list = [var.aud_value]
thumbprint_list = [data.tls_certificate.gitlab.certificates.0.sha1_fingerprint]
}
data "aws_iam_policy_document" "assume-role-policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.gitlab.arn]
}
condition {
test = "StringLike"
variable = "${aws_iam_openid_connect_provider.gitlab.url}:${var.match_field}"
values = var.match_value
}
}
}
resource "aws_iam_role" "gitlab_ci" {
name = format("GitLabCI-OIDC-%s", var.name)
description = "GitLabCI with OIDC"
path = "/ci/"
assume_role_policy = data.aws_iam_policy_document.assume-role-policy.json
managed_policy_arns = formatlist(
"arn:%s:iam::aws:policy/%s",
data.aws_partition.current.partition,
var.managed_policy_names
)
}
Note the difference between StringLike
and StringEquals
StringEquals
: Exact matching, case sensitiveStringLike
: Case-sensitive matching. The values can include multi-character match wildcards (*) and single-character match wildcards (?) anywhere in the string. You must specify wildcards to achieve partial string matches.So if you are welling to use * in the match field, you must choose StringLike otherwise it will not work.
variables.tf:
variable "gitlab_url" {
type = string
default = "https://gitlab.com"
description = "GitLab URL"
}
variable "aud_value" {
type = string
default = "https://gitlab.com"
description = "GitLab Aud"
}
variable "match_field" {
type = string
default = "aud"
description = "GitLab match_field"
}
variable "match_value" {
type = list(any)
description = "GitLab match_value"
}
variable "match_value_app" {
type = list(any)
description = "GitLab match_value_app"
}
variable "managed_policy_names" {
type = list(any)
description = "IAM managed_policy_names"
}
variable "name" {
description = "IAM Name Suffix"
type = string
}
outputs.tf
output "ROLE_ARN" {
description = "INFRA Role that needs to be assumed by GitLab CI"
value = aws_iam_role.gitlab_ci.arn
}
versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "= 4.19.0"
}
tls = {
source = "hashicorp/tls"
version = "= 3.4.0"
}
}
required_version = ">= 1.1.9"
}
Let's assume ACME has an infrastructure Gitlab group and inside that group, you have multiple projects or stacks, for example:
and we want to give them access to deploy to AWS, so we can cover the following use cases:
match_value = ["project_path:ACME/infrastructure/*:ref_type:branch:ref:*"]
match_value = ["project_path:ACME/infrastructure/*:ref_type:branch:ref:main"]
match_value = ["project_path:ACME/infrastructure/app:ref_type:branch:ref:main"]
That means you can design OIDC access based on your deployment requirements and how it should be used with different branches, tags, projects or groups.
One thing to mention is in the upper example we are using managed_policy_names
to pass IAM policy that the OIDC Role should have, for example, Administrator access can be attached using:
managed_policy_names = ["AdministratorAccess"]
You should restrict project path, and managed policy used in the example
module "oidc" {
source = "../../modules/oidc"
name = "TF"
gitlab_url = "https://gitlab.com"
aud_value = "https://gitlab.com"
match_field = "sub"
match_value = ["project_path:ACME/infrastructure/*:ref_type:branch:ref:*"]
managed_policy_names = ["AdministratorAccess"]
}
output "ROLE_ARN" {
value = module.oidc.ROLE_ARN
description = "Role that needs to be assumed by GitLab CI Infra Deployment"
}
We deploy this module to the management (master) account, so we can use OIDC with the management account, then use cross-account roles, to jump to other accounts.
now we have output "ROLE_ARN"
we need to add it to the GitLab group level CICD environment variable, so all sub-project can use OIDC:
At this level, we need to configure the CI pipeline file to use ROLE ID to call OIDC on AWS and get temp access for the current runner session.
.gitlab-ci.yml:
.oidc:
before_script:
- echo "${CI_JOB_JWT_V2}" > /tmp/web_identity_token
- mkdir ~/.aws
- echo -e "[profile mgmt]\nrole_arn=${ROLE_ARN}\nweb_identity_token_file=/tmp/web_identity_token" >> ~/.aws/config
- aws sts get-caller-identity --profile mgmt
In the end, all the needed work can be done on Terraform side, to decide which account or environment you are targeting.
stack main.tf:
provider "aws" {
region = var.region
profile = "mgmt"
assume_role {
role_arn = var.assume_role_arn
session_name = "GitLab-CI-BASELINE"
}
default_tags {
tags = var.tags
}
}
assume_role_arn
will allow us to target accounts, if you enabled AWS SSO, AWS will automatically create cross-account roles that can be used here.
---
In the end, I hope this gives you some introduction how to replace long-lived credentials and use temporary ones.
If you need help with Terraform OIDC integration with GitLab or GitHub don't hesitate to reach out to us!
Thanks, until next time!