Skip to content

Instantly share code, notes, and snippets.

@v-rosa
Last active January 17, 2024 13:01
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save v-rosa/aa9c8afd44d66c3a81b9920a1bc90e42 to your computer and use it in GitHub Desktop.
Save v-rosa/aa9c8afd44d66c3a81b9920a1bc90e42 to your computer and use it in GitHub Desktop.
Use private GitHub hosted terraform modules with AFT v1.5.1

I'll try to share my approach to use private GitHub hosted terraform modules with AFT v1.5.1. It relies on GH App to create ephemeral tokens during Global Customization stage which will share with the target account so it can be used during Account Customization stage.

Relates to: aws-ia/terraform-aws-control_tower_account_factory#42

Pre-requirements:

  • Create a GH APP:
    • Permissions: allow the clone of repositories
    • Set to a restricted list of terraform modules repos
  • Create parameter store entries for GH_APP pem, id and installation_id under AFT_MGT account
module "aft" {
  source                                        = "github.com/aws-ia/terraform-aws-control_tower_account_factory?ref=1.5.1"
  ....
}

provider "aws" {
  alias  = "aft_management"
  region = var.ct_home_region
  assume_role {
    role_arn     = "arn:aws:iam::${var.aft_management_account_id}:role/AWSControlTowerExecution"
    session_name = "AWSAFT-Session"
  }
}

resource "aws_ssm_parameter" "app_pem" {
  provider = aws.aft_management
  name     = "/gh/tf-modules-app/pem"
  type     = "SecureString"
  value    = "TODO"
  depends_on = [
    module.aft
  ]
}

resource "aws_ssm_parameter" "app_id" {
  provider = aws.aft_management
  name     = "/gh/tf-modules-app/id"
  type     = "SecureString"
  value    = "TODO"
  depends_on = [
    module.aft
  ]
}

resource "aws_ssm_parameter" "app_installation_id" {
  provider = aws.aft_management
  name     = "/gh/tf-modules-app/installation-id"
  type     = "SecureString"
  value    = "TODO"
  depends_on = [
    module.aft
  ]
  lifecycle {
    ignore_changes = [
      value
    ]
  }
}

Then in pre-api-helpers.sh of the global customizations we'll create the ephemeral GH token and share it with the vended account:

#!/bin/bash

set -e

echo "Executing Pre-API Helpers"

export AWS_PROFILE=aft-management

gh_app_pem_file='gh_app.pem'
aws ssm get-parameter --name "/gh/tf-modules-app/pem" --query "Parameter.Value" --with-decryption --output text > $gh_app_pem_file
gh_app_id=$(aws ssm get-parameter --name "/gh/tf-modules-app/id" --query "Parameter.Value" --with-decryption --output text)
gh_app_installation_id=$(aws ssm get-parameter --name "/gh/tf-modules-app/installation-id" --query "Parameter.Value" --with-decryption --output text)

gem install jwt

cat >jwt.rb <<EOF
require 'openssl'
require 'jwt'  # https://rubygems.org/gems/jwt

# Private key contents
private_pem = File.read("$gh_app_pem_file")
private_key = OpenSSL::PKey::RSA.new(private_pem)

# Generate the JWT
payload = {
  # issued at time, 60 seconds in the past to allow for clock drift
  iat: Time.now.to_i - 60,
  # JWT expiration time (10 minute maximum)
  exp: Time.now.to_i + (10 * 60),
  # GitHub App's identifier
  iss: "$gh_app_id"
}

jwt = JWT.encode(payload, private_key, "RS256")
puts jwt
EOF

jwt=$(ruby jwt.rb)

github_api_url="https://api.github.com/app/installations/$gh_app_installation_id/access_tokens"

r=$(curl --fail-with-body -s -X POST \
    -H "Authorization: Bearer ${jwt}" \
    -H "Accept: application/vnd.github.v3+json" \
    "${github_api_url}")

echo $(echo "$r" | jq -r '.token') > token.txt

export AWS_PROFILE=aft-target

aws ssm put-parameter \
    --name "/gh/tf-modules-app/token" \
    --value "$(cat token.txt)" \
    --type SecureString \
    --overwrite

rm token.txt
rm jwt.rb

Finally, during the Account Customization stage we'll get the token and configure git auth with GH cli.

# #!/bin/bash

set -e

echo "Executing Pre-API Helpers"

gh_cli_version=2.13.0
wget -q https://github.com/cli/cli/releases/download/v$gh_cli_version/gh_$gh_cli_version\_linux_386.rpm
sudo rpm -i gh_$gh_cli_version\_linux_386.rpm
gh --version

aws ssm get-parameter --name "/gh/tf-modules-app/token" --query "Parameter.Value" --with-decryption --output text > token.txt

gh auth login --with-token < token.txt
gh repo list
gh auth setup-git
export GH_TOKEN=$(cat token.txt)
rm token.txt

And thats it, now you can use private terraform modules.

@andiempettJISC
Copy link

andiempettJISC commented Jul 13, 2022

Building on the above example. Here is the same in python:

In the AFT-Management account create 3 parameters in ssm parameter store named:
/github/apps/aft_terraform_modules/app_id
/github/apps/aft_terraform_modules/app_installation_id
/github/apps/aft_terraform_modules/app_private_key

In aft-global-customizations/api_helpers/pre-api-helpers.sh

#!/bin/bash

echo "Executing Pre-API Helpers"

python $DEFAULT_PATH/api_helpers/get-github-token.py

add python deps in aft-global-customizations/api_helpers/python/requirements.txt

requests
pyjwt
cryptography
boto3

for the script itself aft-global-customizations/api_helpers/get-github-token.py

import os
from cryptography.hazmat.backends import default_backend
import jwt
import requests
import time
import boto3

# Set the aws creds to aft-management accout to fetch github creds
session = boto3.Session(profile_name='aft-management')
ssm = session.client('ssm')

github_app_id = ssm.get_parameter(Name='/github/apps/aft_terraform_modules/app_id', WithDecryption=True)['Parameter']['Value']
github_app_installation_id = ssm.get_parameter(Name='/github/apps/aft_terraform_modules/app_installation_id', WithDecryption=True)['Parameter']['Value']
github_app_private_key = ssm.get_parameter(Name='/github/apps/aft_terraform_modules/app_private_key', WithDecryption=True)['Parameter']['Value']

cert_bytes = github_app_private_key.encode()

private_key = default_backend().load_pem_private_key(cert_bytes, None)

def get_headers():

    time_since_epoch_in_seconds = int(time.time())
    
    payload = {
      # issued at time
      'iat': time_since_epoch_in_seconds,
      # JWT expiration time (10 minute maximum)
      'exp': time_since_epoch_in_seconds + (10 * 60),
      # GitHub App's identifier
      'iss': github_app_id
    }

    actual_jwt = jwt.encode(payload, private_key, algorithm='RS256')

    headers = {"Authorization": "Bearer {}".format(actual_jwt),
               "Accept": "application/vnd.github.machine-man-preview+json"}
    return headers

resp = requests.get('https://api.github.com/app', headers=get_headers())

print('Status Code: ', resp.status_code)
# print('Content: ', resp.content.decode())

resp = requests.post('https://api.github.com/app/installations/{}/access_tokens'.format(github_app_installation_id),
                     headers=get_headers())

print('Status Code: ', resp.status_code)

gh_token = resp.json()['token']

headers = {"Authorization": "token {}".format(gh_token),
           "Accept": "application/vnd.github.machine-man-preview+json"}

resp = requests.get('https://api.github.com/installation/repositories', headers=headers)

print('Status Code: ', resp.status_code)
for repo in resp.json()['repositories']:
    print('Authorised Repository: ', repo['name'])

# Set the aws creds to the target account to vend the token into
session = boto3.Session(profile_name='aft-target')
ssm = session.client('ssm')


github_app_vended_token = ssm.put_parameter(
        Name='/github/apps/aft_terraform_modules/vended_token',
        Description='A limited-time GitHub token to pull terraform modules',
        Overwrite=True,
        Value=gh_token,
        Type='SecureString'
    )

then do the same as the above examples in aft-account-customizations/<my account>/api_helpers/pre-api-helpers.sh

#!/bin/bash

set -e

echo "Executing Pre-API Helpers"

gh_cli_version=2.13.0
wget -q https://github.com/cli/cli/releases/download/v$gh_cli_version/gh_$gh_cli_version\_linux_386.rpm
sudo rpm -i gh_$gh_cli_version\_linux_386.rpm
gh --version

aws ssm get-parameter --name "/github/apps/aft_terraform_modules/vended_token" --query "Parameter.Value" --with-decryption --output text > gh_token

gh auth login --with-token < gh_token
gh repo list
gh auth setup-git
export GH_TOKEN=$(cat gh_token)
rm gh_token

@omerurhan
Copy link

What about private aft repositries? For example aft-account-request repo? How we use this private repo?

@v-rosa
Copy link
Author

v-rosa commented Oct 2, 2023

Hey @omerurhan, do you need to have access to private tf repositories when running aft-account-request pipeline?

Is my understanding correct?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment