【问题标题】:Role Switching ( assume role ) with AWS and Terraform使用 AWS 和 Terraform 进行角色切换(担任角色)
【发布时间】:2018-03-06 06:13:06
【问题描述】:

背景:

我们已经开始对使用 Terraform 而不是直接使用 Cloudformation 生成基础架构进行峰值调查。

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

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

问题的症结:

这在 Terraform 中是否可能(请不要进行大量肮脏的黑客攻击!)?我们一直在尝试此过程,但在尝试运行 Terraform Plan 或 Build 时遇到以下错误

" 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 的开放错误列表,这似乎是尚不支持的东西?

我们已经看到,至少有一个人正在创建 shell 脚本来尝试进行身份验证,然后进行传递。这似乎是一个非常丑陋的黑客让它工作。

有没有人真的通过帐户启用了 MFA 的这项工作?

在 Cons 和研讨会上交谈时,HashiCorp 团队的反应非常模糊。

【问题讨论】:

  • 我们最终用 make 包装了 terraform。
  • 我见过这种基础设施在工作。我建议你看看gruntwork.io - 他们有一些教程和参考资料。

标签: amazon-web-services amazon-cloudformation terraform


【解决方案1】:

我管理一个拥有 100 多个账户的 AWS 组织。每个人在我们称为 identity 的账户中都有一个 IAM 用户。然后他们 sts:AssumeRole 到具有信任关系的其他账户中的 IAM 角色,将 identity 账户命名为受信任。用户负责运行我提供的脚本以生成 MFA aws 配置文件。 terraform 本身并没有这样做,因为需要输入手动代码。

设置角色的提示

以身份创建 IAM 组,并授予他们在所需账户中担任相应角色的权限。确保还授予用户权限,以便能够自行管理身份帐户中的密码和 MFA 设置。确保自我管理权限上没有 MFA 条件,因为如果他们没有权限,则无法添加 MFA 设备。这是先有鸡还是先有蛋的问题。设置 MFA 后,人们需要注销并重新登录 MFA 才能满足 IAM 策略上的 MFA 条件。

当您在其他帐户中创建角色时,您必须创建信任策略以信任 identity 帐户。执行此操作时,我建议将以下条件添加到 true:MultiFactorAuthPresent

设置 aws 配置和凭证文件

我的建议是制定必须在您的组织内设置的配置文件名称模式。您的配置中可以有很多很多配置文件。我有数百个。它们是生成的,而不是手动维护的。

[org]
aws_access_key_id     = SomeKey
aws_secret_access_key = SomeSecretKey

aws configure set profile.org.username 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。如果您收到消息说他们的密钥已过期,您需要再次运行脚本。

关于脚本的注释,我已经在 golang 中将它改造成更好的版本。但它与我还不想分享的东西混合在一起,也许有一天我会在清理它时发布那部分。这是我用 bash 编写的第一个版本。它做得很好。它还会在您指定的配置文件中轮换您的密钥。它会生成一个新密钥,更新您的个人资料以使用新密钥。然后它会删除您的旧密钥。它在每次执行时都这样做。因此,此脚本还会轮换您的密钥,因此您不必记住或因组织政策而被锁定。

该脚本还会为您生成所有其他策略。您可以列出您想要配置文件的所有帐户和角色组合。然后你要在地图上输入账号account_numbers

别忘了您可以使用configure get profile.cde.account_numbers.identity 566179001270272 之类的命令来设置配置。我还喜欢将此 scipt 与所有其他 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"
  }
}

此提供程序在我称为 shared_services 的帐户中为创建 aws 资源建立会话。它使用 mfa 脚本通过我的 org 配置文件生成的配置文件来执行此操作,该配置文件包含我的用户的访问密钥和秘密访问密钥。

然后,如果需要,可以利用提供程序映射将特定提供程序传递给特定模块。请参阅下面的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
}

我已经运行此设置至少 2 年了。它没有任何失望或问题。我希望这回答了你的问题。我的脚本如下:

#!/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

【讨论】:

    猜你喜欢
    • 2020-04-29
    • 2021-03-03
    • 1970-01-01
    • 1970-01-01
    • 2018-01-15
    • 1970-01-01
    • 2018-09-10
    • 1970-01-01
    • 2018-12-18
    相关资源
    最近更新 更多