使用AWS和Terraform进行角色切换(假定角色)

4

背景:

我们已经开始了使用Terraform而不是直接使用Cloudformation来生成基础设施的尝试。

我们有多个AWS账户,分别为Live、QA和Dev环境(由于堆栈的复杂性和客户服务遭受灾难性破坏的潜在可能,因此完全分离关注点)。我们的账户已经开启了MFA。

使用Cloudformation时,我们进行角色切换以对一个主要的AWS账户进行身份验证,然后使用一个假定角色在正确的账户中建立我们的堆栈。

问题的核心:

请问在Terraform中是否可以实现这一点(请勿使用大规模的不正当手段!)?我们一直在尝试这个过程,但在尝试运行Terraform计划或构建时出现以下错误:

"The role 'arn:aws:iam::ACCOUNTID:role/ASSUMEDROLE" cannot be assumed.'

我们的提供商切换代码如下:

# Configure the AWS Provider
provider "aws" {
  region = "${var.aws_region}"
  profile = "${var.profile}"
  assume_role {
    role_arn = "arn:aws:iam::${lookup(var.aws_account_id, var.tag_environment)}:role/MYASSUMEROLE"
  }
}

从搜索多个博客和阅读 Terraform 的开放 bug 列表来看,似乎还不支持这一点?
我们发现至少有一个人正在创建 shell 脚本尝试进行身份验证并实现自动化,但是这种方法看起来很丑陋。 有人成功地在启用帐户的 MFA 的情况下使用过吗? 当我们在 Cons 和研讨会上与 HashiCorp 团队交流时,他们的回答非常模糊。

1
我们最终使用_make_封装了terraform。 - Dusan Bajic
我见过那种基础设施的运作方式。我建议你看一下https://www.gruntwork.io/ - 他们有一些教程和参考资料。 - pcurry
1个回答

4
我管理一个AWS组织,拥有100多个账户。每个人都在我们称之为“身份(identity)”的账户中拥有一个IAM用户。然后他们会sts:AssumeRole到其他账户中的IAM角色,这些角色具有信任关系,并将“身份(identity)”账户命名为受信任方。用户负责运行我提供的脚本来生成MFA AWS配置文件。Terraform本身并没有做这件事,因为需要手动输入代码。
设置角色的提示:
在“身份(identity)”中创建IAM组,并赋予权限以承担期望账户中对应角色的角色。确保还授予用户权限,在“身份(identity)”账户中自我管理密码和MFA设置。确保自我管理权限上没有MFA条件,因为如果由于条件而没有权限,则无法添加MFA设备。这是一个先有鸡还是先有蛋的问题。设置MFA后,人们需要使用MFA注销并重新登录以满足IAM策略中的MFA条件。
在其他账户中创建角色时,必须创建一个信任策略以信任“身份(identity)”账户。在这样做时,我建议将以下条件设置为true:MultiFactorAuthPresent
设置AWS配置和凭证文件:
我的建议是制定一个必须在您的组织中设置的profile名称模式。您的配置文件中可以有许多profile。我有数百个。它们是生成的,而不是手动维护的。
[org]
aws_access_key_id     = SomeKey
aws_secret_access_key = SomeSecretKey

aws configure set profile.org.username gmiller.cli

AWS 配置设置为机构用户名 "gmiller.cli"。
[profile org]
region   = us-west-2
username = jsmith
roles    = admin,read,terraform
accounts          = identity,shared_services,dev_a,dev_b,dev_c,uat_a,uat_b,uat_c
account_numbers   = 
  identity        = 566179001270272
  shared_services = 886917640172339
  dev_a           = 505685932297420
  dev_b           = 488489750836019
  dev_c           = 695182558652006
  uat_a           = 123189319014809
  uat_b           = 705170270846976
  uat_c           = 608206892249907

通过脚本生成MFA配置文件

我的脚本使用您的非MFA AccessKey和SecretAccessKey请求MFA身份验证键。为此,您需要在aws cli中调用mfa命令并传递当前的MFA代码。然后,我的脚本解析返回体并创建一个新的配置文件,将原始配置文件名称的末尾添加_mfa。因此,每当您想要使用名为foo的配置文件但需要MFA时,只需指定配置文件foo_mfa即可。如果您收到键已过期的消息,则需要再次运行该脚本。

有关脚本的说明,我已经将其改进为Go语言的更好版本。但其中夹杂着我不想分享的内容,也许有一天我会在清理后发布那部分内容。这是我用bash编写的第一个版本。它做得很好。它还会在您指定的配置文件中轮换密钥。它会创建一个新密钥,更新您的配置文件以使用新密钥。然后它会删除旧密钥。每次执行都会这样做。因此,该脚本还会轮换您的密钥,这样您就不必记住或因组织政策而被锁定。

该脚本还会生成您的所有其他策略。您可以列出所有要为其生成配置文件的帐户和角色组合。然后,您需要将帐户号码放入映射account_numbers中。

不要忘记您可以使用命令如configure get profile.cde.account_numbers.identity 566179001270272来设置配置信息。我也喜欢将此脚本放在~/.aws目录中,以便与所有其他AWS配置文件一起使用。

运行: ~/.aws/mfa.sh --realm org --code 729376

从您的源配置文件org,这将生成以下内容:

[org_mfa]
aws_access_key_id     = KeyThatWillExpire
aws_secret_access_key = SecretKeyThatWillExpire
aws_session_token     = SessionTokenThatWillExpire/////////////gornucibawowovvawumekuvekorsekotworwatandencitezesodupusowoimmelavdufzocpunbofubafdofizagvuchecufihencehfejjehdaakacmudkiutmotuwwomcoejbokazejudocetbovmifwavawvilidmalwermizmurtutotabujobgajpihsoticoowitoicubukbuglahicpatjuswodiklawciredemkukudapafietwepophibtetdildewdivwizhadunantizozatohojasejorjeivirurenmajrudsopujkalahoidugacsogogojwaprildibovgabzirajimwegegupnidukogafupaniwutudtiruntuzsogucopawafuvudfimozasbitokpulduhwagjubbevamatuopijogihaj

您可以使用以下命令检查是否起作用:

aws --profile=org_mfa sts get-caller-identity

然后,您可以让所有其他配置文件都期望 org_mfa 已存在。这对于运行 cli 命令很有用,但对于 terraform,请查看下面的内容。由我的脚本生成的配置文件将自动为您执行此操作。

[profile org_some_account_terraform]
source_profile = org_mfa
role_arn       = arn:aws:iam::123otheraccount321:role/terraform
region         = us-west-2
output         = json

在Terraform中,您可以为profileassume_role属性使用变量。这就是在组织中拥有角色命名的标准模式的好处所在。不要让用户传递他们想要使用的配置文件,而是在terraform代码中指定,让用户创建与代码期望相匹配的配置文件。我没有收到关于此的投诉。这使生活变得非常容易。
指定MFA角色的Terraform提供程序:
provider "aws" {
  version = "~> 2.38.0"
  alias   = "shared_services"
  profile = format("%s_mfa", var.realm)
  region  = var.region

  assume_role {
    role_arn = "arn:aws:iam::${var.shared_services_account_number}:role/terraform"
  }
}

这个提供者为我的账户中创建AWS资源建立一个名为shared_services的会话。它使用由MFA脚本生成的配置文件,其中包含我的用户访问密钥和秘密访问密钥。

如果需要,可以利用提供者映射将特定的提供者传递给特定的模块。请参见下面的providers映射:

module "bootstrap" {
  source = "../_modules/bootstrap/global"

  providers = {
    aws                 = aws
    aws.org_identity    = aws.org_identity
    aws.shared_services = aws.shared_services
  }

  iam_alias = var.iam_alias
  realm     = var.realm
}

我已经使用这个设置至少两年了。它一直运行良好,没有任何问题。希望这个回答了你的问题。我的脚本如下:

#!/usr/bin/env bash

# TODO generate config and credentials from gomplate
# TODO test each role assumption to validate config vs reality

# https://natelandau.com/boilerplate-shell-script-template/
# ##################################################
# My Generic BASH script template
#
version="1.0.0"               # Sets version variable
#
scriptTemplateVersion="1.3.0" # Version of scriptTemplate.sh that this script is based on
#                               v.1.1.0 - Added 'debug' option
#                               v.1.1.1 - Moved all shared variables to Utils
#                                       - Added $PASS variable when -p is passed
#                               v.1.2.0 - Added 'checkDependencies' function to ensure needed
#                                         Bash packages are installed prior to execution
#                               v.1.3.0 - Can now pass CLI without an option to $args
#
# HISTORY:
#
# * DATE - v1.0.0  - First Creation
#
# ##################################################

# Provide a variable with the location of this script.
scriptPath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
scriptParentPath="${scriptPath%/*}"

# Source Scripting Utilities
# -----------------------------------
# These shared utilities provide many functions which are needed to provide
# the functionality in this boilerplate. This script will fail if they can
# not be found.
# -----------------------------------

# utilsLocation="${scriptParentPath}/lib/utils.sh" # Update this path to find the utilities.

# if [ -f "${utilsLocation}" ]; then
#   source "${utilsLocation}"
# else
#   echo "Please find the file util.sh and add a reference to it in this script. Exiting."
#   exit 1
# fi

# trapCleanup Function
# -----------------------------------
# Any actions that should be taken if the script is prematurely
# exited.  Always call this function at the top of your script.
# -----------------------------------
# function trapCleanup() {
#   echo ""
#   if is_dir "${tmpDir}"; then
#     rm -r "${tmpDir}"
#   fi
#   die "Exit trapped."  # Edit this if you like.
# }

# Set Flags
# -----------------------------------
# Flags which can be overridden by user input.
# Default values are below
# -----------------------------------
quiet=0
printLog=0
verbose=0
force=0
strict=0
debug=0
args=()

# args
code=""
realm=""
region="us-west-2"
mfa_arn=""
username=""
account_number=""
skip_key_rotate=0
skip_realm_config=0
duration_seconds=129600

# scratch vars
exit_do_to_missing_required_vars=0
return_body=""
aws_session_token=""
secret_access_key=""
access_key_id=""
old_key_id=""
new_key_id=""
old_secret=""
new_secret=""
declare -a accounts
declare -a roles

# Set Temp Directory
# -----------------------------------
# Create temp directory with three random numbers and the process ID
# in the name.  This directory is removed automatically at exit.
# -----------------------------------
tmpDir="/tmp/${scriptName}.$RANDOM.$RANDOM.$RANDOM.$$"
(umask 077 && mkdir "${tmpDir}") || {
  echo "Could not create temporary directory! Exiting."
  exit 1
}

# Logging
# -----------------------------------
# Log is only used when the '-l' flag is set.
#
# To never save a logfile change variable to '/dev/null'
# Save to Desktop use: $HOME/Desktop/${scriptBasename}.log
# Save to standard user log location use: $HOME/Library/Logs/${scriptBasename}.log
# -----------------------------------
logFile="$HOME/Library/Logs/${scriptBasename}.log"

# Check for Dependencies
# -----------------------------------
# Arrays containing package dependencies needed to execute this script.
# The script will fail if dependencies are not installed.  For Mac users,
# most dependencies can be installed automatically using the package
# manager 'Homebrew'.
# -----------------------------------
homebrewDependencies=()

function verbose() {
  if [[ $verbose -eq 1 ]]; then
    echo $1
  fi
}

function mainScript() {
  ############## Begin Script Here ###################
  ####################################################

  echo -n
  verbose "starting script"

  verbose "checking if required code param is set"
  if [[ $code == "" ]]; then
    verbose "exiting because required code param isn't set"
    echo "code or c is required"
    exit_do_to_missing_required_vars=1
  fi
  verbose "code param is set to ${code}"

  verbose "checking if required realm param is set"
  if [[ $realm == "" ]]; then
    verbose "exiting because required code param isn't set"
    echo "realm or r is required"
    exit_do_to_missing_required_vars=1
  fi
  verbose "realm param is set to ${realm}"

  verbose "checking to see if exit_do_to_missing_required_vars is 1"
  if [[ $exit_do_to_missing_required_vars -eq 1 ]]; then
    verbose "exit_do_to_missing_required_vars is 1 so exiting..."
    usage
    exit
  fi
  verbose "exit_do_to_missing_required_vars is not 1"

  verbose "setting region to: ${region}"
  region=$region
  aws configure set profile.${realm}.region $region

  verbose "setting username var: aws configure get username --profile $realm"
  username=$(aws configure get username --profile $realm)
  verbose "username is set to: ${username}"
  verbose "checking account number"
  account_number=$(aws configure get account_numbers.identity --profile $realm)
  verbose "account number is set to: ${account_number}"

  verbose "checking if required username aws config is set"
  if [[ $username == "" ]]; then
    verbose "exiting because required username aws config isn't set"
    echo "username is required to be set your realm's .aws/credentials profile"
    exit_do_to_missing_required_vars=1
  fi

  verbose "checking if required accounts and account_numbers aws config is set"
  if [[ $account_number == "" ]]; then
    verbose "exiting because required accounts and account_numbers aws config isn't set"
    echo "account_number is required to be set your realm's .aws/credentials profile"
    exit_do_to_missing_required_vars=1
  fi

  verbose "checking to see if exit_do_to_missing_required_vars is 1"
  if [[ $exit_do_to_missing_required_vars -eq 1 ]]; then
    verbose "exit_do_to_missing_required_vars is 1 so exiting..."
    usage
    exit
  fi

  verbose "creating MFA arn from account number and username"
  mfa_arn=arn:aws:iam::${account_number}:mfa/${username}
  verbose "mfa_arn = ${mfa_arn}"

  verbose "getting session token body by executing:"
  verbose "shell aws --profile=$realm sts get-session-token --serial-number $mfa_arn --token-code $code --duration-seconds $duration_seconds"
  return_body=$(aws --profile=$realm --region=$region sts get-session-token --serial-number $mfa_arn --token-code $code --duration-seconds $duration_seconds)
  verbose "session token body ="
  verbose $return_body
  verbose "getting keys from body"
  aws_session_token=$(echo $return_body | jq -r '.Credentials | .SessionToken')
  verbose "aws_session_token = ${aws_session_token}"
  secret_access_key=$(echo $return_body | jq -r '.Credentials | .SecretAccessKey')
  verbose "secret_access_key = ${secret_access_key}"
  access_key_id=$(echo $return_body | jq -r '.Credentials | .AccessKeyId')
  verbose "access_key_id = ${access_key_id}"

  if [[ $skip_key_rotate -eq 0 ]]; then
    verbose "skip key rotation not enabled: rotating key"
    return_body=""

    old_key_id=$(aws configure get aws_access_key_id --profile $realm)
    verbose "old key = ${old_key_id}"

    verbose "creating new access key"
    return_body=$(aws --profile=$realm iam create-access-key --user-name $username)
    verbose "return body ="
    verbose $return_body

    verbose "keys are:"
    new_key_id=$(echo $return_body | jq -r '.AccessKey | .AccessKeyId')
    verbose "new_key_id = ${new_key_id}"
    new_secret=$(echo $return_body | jq -r '.AccessKey | .SecretAccessKey')
    verbose "new_secret = ${new_secret}"

    verbose "deleting old access key"
    return_body=$(aws --profile=$realm iam delete-access-key --user-name $username --access-key-id $old_key_id)
    verbose "return body ="
    verbose $return_body

    verbose "setting aws_access_key_id"
    aws configure set profile.${realm}.aws_access_key_id $new_key_id
    verbose "setting aws_secret_access_key"
    aws configure set profile.${realm}.aws_secret_access_key $new_secret
  fi

  verbose ""
  verbose "SETTING MFA PROFILE"
  verbose "setting aws_access_key_id: aws configure set profile.${realm}_mfa.aws_access_key_id $access_key_id"
  aws configure set profile.${realm}_mfa.aws_access_key_id $access_key_id
  verbose "setting aws_secret_access_key: aws configure set profile.${realm}_mfa.aws_secret_access_key $secret_access_key"
  aws configure set profile.${realm}_mfa.aws_secret_access_key $secret_access_key
  verbose "setting aws_session_token: aws configure set profile.${realm}_mfa.aws_session_token $aws_session_token"
  aws configure set profile.${realm}_mfa.aws_session_token $aws_session_token
  verbose ""

  verbose "checking skip realm config is 0. it is = ${skip_realm_config}"
  if [[ $skip_realm_config -eq 0 ]]; then
    verbose "doing realm config"

    verbose "getting aws config for roles"
    return_body=$(aws configure get profile.${realm}.roles)
    verbose "return body ="
    verbose $return_body
    IFS=', ' read -r -a roles <<<"$return_body"

    for role in "${roles[@]}"; do
      verbose "role read: ${role}"
    done

    verbose "getting aws config for accounts"
    return_body=$(aws configure get profile.${realm}.accounts)
    verbose "return body ="
    verbose $return_body
    IFS=', ' read -r -a accounts <<<"$return_body"

    for account in "${accounts[@]}"; do
      verbose "getting account number from config for ${account}"
      account_number=$(aws configure get profile.${realm}.account_numbers.${account})
      verbose "account number is = ${account_number}"
      for role in "${roles[@]}"; do
        verbose "setting ${realm}_${account}_${role} source_profile = ${realm}_mfa"
        aws configure set profile.${realm}_${account}_${role}.source_profile ${realm}_mfa
        verbose "setting ${realm}_${account}_${role} role_arn = arn:aws:iam::${account_number}:role/${role}"
        aws configure set profile.${realm}_${account}_${role}.role_arn arn:aws:iam::${account_number}:role/${role}
      done
      if [[ $realm != "org_master" ]]; then
        verbose "linking account to org_master OrganizationAccountAccessRole profile"
        aws configure set profile.org_master_${realm}_${account}_OrganizationAccountAccessRole.source_profile org_master_mfa
        aws configure set profile.org_master_${realm}_${account}_OrganizationAccountAccessRole.role_arn arn:aws:iam::${account_number}:role/OrganizationAccountAccessRole
      fi
    done
  fi

  ####################################################
  ############### End Script Here ####################
}

############## Begin Options and Usage ###################

# Print usage
usage() {
  echo -n "${scriptName} [OPTION]... [FILE]...
This generates ~/.aws/credentials via the aws cli for mfa authentication.
username and account_numbers must be set in your realm's .aws/credentials profile.
Also, rotates your aws_access_key_id and secret key along with it each run unless you disable it.
Also, configures an entire realm based off of your ~/.aws/config and credentials. See README.md
 Options:
  -c, --code          required: Your rotating mfa code
  -r, --realm         required: The name of the realm. will result as realm_mfa as profile name
  -r, --region        change the region from default
  --skip-key-rotate   include this flag to skip the accesss key rotation
  --skip-realm-config include this flag to skip auto config of the entire realm in your ~/.aws/credentials file
  --duration-seconds  duration seconds the mfa is valid for. default is 129600 seconds(36 hr)
  -q, --quiet         Quiet (no output)
  -l, --log           Print log to file
  -s, --strict        Exit script with null variables.  i.e 'set -o nounset'
  -v, --verbose       Output more information. (Items echoed to 'verbose')
  -d, --debug         Runs script in BASH debug mode (set -x)
  -h, --help          Display this help and exit
      --version       Output version information and exit
"
}

# Iterate over options breaking -ab into -a -b when needed and --foo=bar into
# --foo bar
optstring=h
unset options
while (($#)); do
  case $1 in
  # If option is of type -ab
  -[!-]?*)
    # Loop over each character starting with the second
    for ((i = 1; i < ${#1}; i++)); do
      c=${1:i:1}

      # Add current char to options
      options+=("-$c")

      # If option takes a required argument, and it's not the last char make
      # the rest of the string its argument
      if [[ $optstring == *"$c:"* && ${1:i+1} ]]; then
        options+=("${1:i+1}")
        break
      fi
    done
    ;;

  # If option is of type --foo=bar
  --?*=*) options+=("${1%%=*}" "${1#*=}") ;;
  # add --endopts for --
  --) options+=(--endopts) ;;
  # Otherwise, nothing special
  *) options+=("$1") ;;
  esac
  shift
done
set -- "${options[@]}"
unset options

# Print help if no arguments were passed.
# Uncomment to force arguments when invoking the script
# [[ $# -eq 0 ]] && set -- "--help"

# Read the options and set stuff
while [[ $1 == -?* ]]; do
  case $1 in
  -c | --code)
    code=$2
    shift
    ;;
  -r | --realm)
    realm=$2
    shift
    ;;
  --region)
    region=$2
    shift
    ;;
  --mfa_arn)
    mfa_arn=$2
    shift
    ;;
  --duration-seconds)
    duration_seconds=$2
    shift
    ;;
  --skip-key-rotate) skip_key_rotate=1 ;;
  --skip-realm-config) skip_realm_config=1 ;;
  -h | --help)
    usage >&2
    exit 0
    ;;
  --version)
    echo "$(basename $0) ${version}"
    exit 0
    ;;
  -v | --verbose) verbose=1 ;;
  -l | --log) printLog=1 ;;
  -q | --quiet) quiet=1 ;;
  -s | --strict) strict=1 ;;
  -d | --debug) debug=1 ;;
  --force) force=1 ;;
  --endopts)
    shift
    break
    ;;
  *)
    echo "invalid option: '$1'."
    exit 1
    ;;
  esac
  shift
done

# Store the remaining part as arguments.
args+=("$@")

############## End Options and Usage ###################

# ############# ############# #############
# ##       TIME TO RUN THE SCRIPT        ##
# ##                                     ##
# ## You shouldn't need to edit anything ##
# ## beneath this line                   ##
# ##                                     ##
# ############# ############# #############

# Trap bad exits with your cleanup function
# trap trapCleanup EXIT INT TERM

# Exit on error. Append '||true' when you run the script if you expect an error.
set -o errexit

# Run in debug mode, if set
if [ "${debug}" == "1" ]; then
  set -x
fi

# Exit on empty variable
if [ "${strict}" == "1" ]; then
  set -o nounset
fi

# Bash will remember & return the highest exitcode in a chain of pipes.
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`, for example.
set -o pipefail

# Invoke the checkDependenices function to test for Bash packages
# checkDependencies

# Run your script
mainScript

# safeExit # Exit cleanly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接