globaldatanetmenu

.Terraform Pipeline with Gitlab CI and OIDC for AWS

Aug 22nd 2022-4 min read

Overview

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

Blog Content

Prerequisites

Adding the IAM identity provider to AWS

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.

Create OIDC module

main.tf:

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 sensitive
  • StringLike: 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"
}

Call OIDC module

Let's assume ACME has an infrastructure Gitlab group and inside that group, you have multiple projects or stacks, for example:

  • app stack
  • data stack
  • network stack
  • baseline stack

and we want to give them access to deploy to AWS, so we can cover the following use cases:

  • allow all sub-projects and all branches to access the same OIDC
    • match_value = ["project_path:ACME/infrastructure/*:ref_type:branch:ref:*"]
  • allow all sub-projects and main branch to access the same OIDC
    • match_value = ["project_path:ACME/infrastructure/*:ref_type:branch:ref:main"]
  • allow app stack project and dev branch to access the OIDC
    • 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.

Configure GitLab settings

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:

Blog Content

Configure GitLab Pipeline

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!

globaldatanetCloud Development, Optimization & Automation

.Navigation

.Social

  • follow globaldatanet on instagram
  • follow globaldatanet on facebook
  • follow globaldatanet on twitter
  • follow globaldatanet on linkendin
  • follow globaldatanet on twitch
  •  listen to our serverless world podcast
  • follow globaldatanet's tech rss feed
  • follow globaldatanet at github
© 2022 by globaldatanet. All Right Reserved
Your privacy is important to us!

We use cookies on our website. Some of them are essential,while others help us to improve our online offer.
You can find more information in our Privacy policy