我管理一个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中,您可以为
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"
}
}
这个提供者为我的账户中创建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
version="1.0.0"
scriptTemplateVersion="1.3.0"
scriptPath="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
scriptParentPath="${scriptPath%/*}"
quiet=0
printLog=0
verbose=0
force=0
strict=0
debug=0
args=()
code=""
realm=""
region="us-west-2"
mfa_arn=""
username=""
account_number=""
skip_key_rotate=0
skip_realm_config=0
duration_seconds=129600
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
tmpDir="/tmp/${scriptName}.$RANDOM.$RANDOM.$RANDOM.$$"
(umask 077 && mkdir "${tmpDir}") || {
echo "Could not create temporary directory! Exiting."
exit 1
}
logFile="$HOME/Library/Logs/${scriptBasename}.log"
homebrewDependencies=()
function verbose() {
if [[ $verbose -eq 1 ]]; then
echo $1
fi
}
function mainScript() {
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
}
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
"
}
optstring=h
unset options
while (($#)); do
case $1 in
-[!-]?*)
for ((i = 1; i < ${#1}; i++)); do
c=${1:i:1}
options+=("-$c")
if [[ $optstring == *"$c:"* && ${1:i+1} ]]; then
options+=("${1:i+1}")
break
fi
done
;;
--?*=*) options+=("${1%%=*}" "${1#*=}") ;;
--) options+=(--endopts) ;;
*) options+=("$1") ;;
esac
shift
done
set -- "${options[@]}"
unset options
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
args+=("$@")
set -o errexit
if [ "${debug}" == "1" ]; then
set -x
fi
if [ "${strict}" == "1" ]; then
set -o nounset
fi
set -o pipefail
mainScript