背景
我们已开始使用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的团队做出了非常模糊的回应。
答案 0 :(得分:1)
我管理一个拥有100多个账户的AWS组织。每个人在我们称为identity
的帐户中只有一个IAM用户。然后他们sts:AssumeRole到其他具有信任关系的帐户中的IAM角色,这些帐户将identity
帐户命名为Trusted。用户负责运行我提供的用于生成MFA aws配置配置文件的脚本。 terraform本身不执行此操作,因为需要输入手动代码。
使IAM组具有身份,并授予他们在所需帐户中承担相应角色的权限。确保还授予用户权限,以使其能够自我管理身份帐户中的密码和MFA设置。确保自我管理权限上没有MFA条件,因为如果他们由于条件而没有权限,他们将无法添加MFA设备。这是鸡和鸡蛋的问题。设置MFA后,人们需要注销并重新使用MFA才能满足IAM策略上的MFA条件。
在其他帐户中担任角色时,必须创建一个信任策略以信任identity
帐户。在执行此操作时,建议将以下条件添加为true:MultiFactorAuthPresent
。
我的建议是制作必须在您的组织内设置的配置文件名称模式。您的配置中可以有很多配置文件。我有几百个它们是生成的,而不是手动维护的。
[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 AccessKey和SecretAccessKey来请求MFA支持的身份验证密钥来工作。为此,您可以在aws cli中调用mfa命令,并传递当前的MFA代码。然后,我的脚本将解析返回正文,并创建一个新的配置文件,并在原始配置文件名称的末尾添加_mfa
。因此,只要您想使用配置文件foo
,但需要使用MFA,只需指定配置文件foo_mfa
。如果您收到一条消息,提示它们的密钥已过期,则需要再次运行该脚本。
关于脚本的注释,此后我将其重新编写成golang更好的版本。但这是我不愿分享的东西,也许有一天我会在清理时发布该部分。这是我用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中,可以将变量用于profile
和assume_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脚本通过我的组织配置文件生成的配置文件执行的,该配置文件具有用户的访问密钥和秘密访问密钥。
然后,根据需要利用提供程序映射将特定的提供程序传递给特定的模块。请参见下面的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