背景:
我们已经开始研究如何使用Terraform生成基础设施,而不是直接使用Cloudformation。
我们有多个AWS帐户,分别用于Live、QA和Dev环境(由于堆栈的复杂性和潜在的客户端服务灾难性破坏,完全分离了关注点)。我们的帐户已经接通了。
使用Cloudformation,我们进行角色切换,对一个主要AWS帐户进行身份验证,然后使用假定的角色在正确的帐户中建立堆栈。
问题的关键:
这有可能吗?(请不要有大量肮脏的黑客!)在Terraform?我们一直在尝试此过程,但在尝试运行Terraform或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开放的bug列表,这似乎是什么东西还不支持?
我们已经看到,至少有一个人正在创建shell脚本来尝试进行身份验证,然后再通过。这似乎是一个非常丑陋的黑客,使其发挥作用。
,有人真的让MFA启动了账户吗?
我们在HashiCorp的团队在Cons和研讨会上交谈时得到了非常模糊的回应。
发布于 2020-10-09 09:08:19
我管理一个拥有100多个帐户的AWS组织。在我们称为identity的帐户中,每个人都有一个IAM用户。然后,他们sts:AssumeRole到IAM在其他帐户中的角色具有信任关系,并将identity帐户命名为可信帐户。用户负责运行我提供的脚本来生成MFA aws配置配置文件。地形本身并不是这样做的,因为需要输入手动代码。
设置角色的技巧
使IAM组具有身份,并允许他们在所需的帐户中承担相应的角色。确保还为用户提供权限,使其能够自我管理身份帐户中的密码和MFA设置。确保在自管理权限上没有MFA条件,因为如果它们由于条件而没有权限,则无法添加MFA设备。这是鸡和蛋的问题。在建立了MFA之后,人们将需要登录并返回MFA,以满足MFA关于IAM政策的条件。
在其他帐户中创建角色时,必须创建信任策略来信任identity帐户。当您这样做时,我建议将以下条件添加到true:MultiFactorAuthPresent。
设置aws配置和凭据文件
我的建议是创建一个必须在您的组织中设置的配置文件名称模式。你可以在你的配置中有很多很多的配置文件。我有上百个。它们是生成的,而不是手动维护的。
[org]
aws_access_key_id = SomeKey
aws_secret_access_key = SomeSecretKeyaws 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支持的auth键。为此,您可以调用aws中的mfa命令,并传递当前的MFA代码。然后,我的脚本解析返回体并创建一个新的配置文件,将_mfa添加到原始配置文件名的末尾。因此,每当您想要使用profile foo但它需要MFA时,只需指定profile foo_mfa即可。如果收到消息说密钥过期,则需要再次运行脚本。
关于这个脚本的一个注释,我已经把它重新加工成了一个更好的版本。但是它和一些我还不想分享的东西混在一起,也许有一天我会在清理的时候把它放出来。这是我用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 cleanlyhttps://stackoverflow.com/questions/46402427
复制相似问题