your EC2 instances need access to the AWS API either via an Internet Gateway + public IP or a Nat Gatetway / instance.
it can take up to 10 minutes until a new IAM user can log in
if you delete the IAM user / ssh public key and the user is already logged in, the SSH session will not be closed
uid's and gid's across multiple servers might not line up correctly (due to when a server was booted, and what users existed at that time). Could affect NFS mounts or Amazon EFS.
this solution will work for ~100 IAM users and ~100 EC2 instances. If your setup is much larger (e.g. 10 times more users or 10 times more EC2 instances) you may run into two issues:
IAM API limitations
Disk space issues
not all IAM user names are allowed in Linux user names (e.g. if you use email addresses as IAM user names). See section IAM user names and Linux user names for further details.
# id admin1uid=1001(admin1)gid=1002(admin1)groups=1002(admin1),1001(iam-users)# id admin2uid=1002(admin2)gid=1003(admin2)groups=1003(admin2),1001(iam-users)# id member1uid=1003(member1)gid=1004(member1)groups=1004(member1),1001(iam-users)# id member2uid=1004(member2)gid=1005(member2)groups=1005(member2),1001(iam-users)
IAM_AUTHORIZED_GROUPS=""
LOCAL_MARKER_GROUP="iam-synced-users"
LOCAL_GROUPS=""
SUDOERS_GROUPS=""
ASSUMEROLE=""
# Remove or set to 0 if you are done with configuration
# To change the interval of the sync change the file
# /etc/cron.d/import_users
DONOTSYNC=1
#!/bin/bash -efunction log(){
/usr/bin/logger -i -p auth.info -t aws-ec2-ssh "$@"}# check if AWS CLI existsif ! [ -x "$(which aws)"]; then
log "aws executable not found - exiting!"exit1fi# source configuration if it exists[ -f /etc/aws-ec2-ssh.conf ]&& . /etc/aws-ec2-ssh.conf
# Should we actually do something?
: ${DONOTSYNC:=0}if[${DONOTSYNC} -eq 1]then
log "Please configure aws-ec2-ssh by editing /etc/aws-ec2-ssh.conf"exit1fi# Which IAM groups have access to this instance# Comma seperated list of IAM groups. Leave empty for all available IAM users
: ${IAM_AUTHORIZED_GROUPS:=""}# Special group to mark users as being synced by our script
: ${LOCAL_MARKER_GROUP:="iam-synced-users"}# Give the users these local UNIX groups
: ${LOCAL_GROUPS:=""}# Specify an IAM group for users who should be given sudo privileges, or leave# empty to not change sudo access, or give it the value '##ALL##' to have all# users be given sudo rights.# DEPRECATED! Use SUDOERS_GROUPS
: ${SUDOERSGROUP:=""}# Specify a comma seperated list of IAM groups for users who should be given sudo privileges.# Leave empty to not change sudo access, or give the value '##ALL## to have all users# be given sudo rights.
: ${SUDOERS_GROUPS:="${SUDOERSGROUP}"}# Assume a role before contacting AWS IAM to get users and keys.# This can be used if you define your users in one AWS account, while the EC2# instance you use this script runs in another.
: ${ASSUMEROLE:=""}# Possibility to provide a custom useradd program
: ${USERADD_PROGRAM:="/usr/sbin/useradd"}# Possibility to provide custom useradd arguments
: ${USERADD_ARGS:="--user-group --create-home --shell /bin/bash"}# Initizalize INSTANCE variableINSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | grep region | awk -F\"'{print $4}')function setup_aws_credentials(){local stscredentials
if[[ ! -z "${ASSUMEROLE}"]]thenstscredentials=$(aws sts assume-role \
--role-arn "${ASSUMEROLE}"\
--role-session-name something \
--query '[Credentials.SessionToken,Credentials.AccessKeyId,Credentials.SecretAccessKey]'\
--output text)AWS_ACCESS_KEY_ID=$(echo"${stscredentials}" | awk '{print $2}')AWS_SECRET_ACCESS_KEY=$(echo"${stscredentials}" | awk '{print $3}')AWS_SESSION_TOKEN=$(echo"${stscredentials}" | awk '{print $1}')AWS_SECURITY_TOKEN=$(echo"${stscredentials}" | awk '{print $1}')export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_SECURITY_TOKEN
fi}# Get list of iam groups from tagfunction get_iam_groups_from_tag(){if["${IAM_AUTHORIZED_GROUPS_TAG}"]thenIAM_AUTHORIZED_GROUPS=$(\
aws --region $REGION ec2 describe-tags \
--filters "Name=resource-id,Values=$INSTANCE_ID""Name=key,Values=$IAM_AUTHORIZED_GROUPS_TAG"\
--query "Tags[0].Value" --output text \)fi}# Get all IAM users (optionally limited by IAM groups)function get_iam_users(){local group
if[ -z "${IAM_AUTHORIZED_GROUPS}"]then
aws iam list-users \
--query "Users[].[UserName]"\
--output text \
| sed "s/\r//g"elsefor group in$(echo${IAM_AUTHORIZED_GROUPS} | tr ","" "); do
aws iam get-group \
--group-name "${group}"\
--query "Users[].[UserName]"\
--output text \
| sed "s/\r//g"donefi}# Run all found iam users through clean_iam_usernamefunction get_clean_iam_users(){local raw_username
for raw_username in$(get_iam_users); do
clean_iam_username "${raw_username}" | sed "s/\r//g"done}# Get previously synced usersfunction get_local_users(){
/usr/bin/getent group ${LOCAL_MARKER_GROUP}\
| cut -d : -f4- \
| sed "s/,/ /g"}# Get list of IAM groups marked with sudo access from tagfunction get_sudoers_groups_from_tag(){if["${SUDOERS_GROUPS_TAG}"]thenSUDOERS_GROUPS=$(\
aws --region $REGION ec2 describe-tags \
--filters "Name=resource-id,Values=$INSTANCE_ID""Name=key,Values=$SUDOERS_GROUPS_TAG"\
--query "Tags[0].Value" --output text \)fi}# Get IAM users of the groups marked with sudo accessfunction get_sudoers_users(){local group
[[ -z "${SUDOERS_GROUPS}"]]||[["${SUDOERS_GROUPS}"=="##ALL##"]]||for group in$(echo"${SUDOERS_GROUPS}" | tr ","" "); do
aws iam get-group \
--group-name "${group}"\
--query "Users[].[UserName]"\
--output text
done}# Get the unix usernames of the IAM users within the sudo groupfunction get_clean_sudoers_users(){local raw_username
for raw_username in$(get_sudoers_users); do
clean_iam_username "${raw_username}"done}# Create or update a local user based on info from the IAM groupfunction create_or_update_local_user(){local username
local sudousers
local localusergroups
username="${1}"sudousers="${2}"localusergroups="${LOCAL_MARKER_GROUP}"# check that username contains only alphanumeric, period (.), underscore (_), and hyphen (-) for a safe evalif[[ ! "${username}"=~ ^[0-9a-zA-Z\._\-]{1,32}$ ]]then
log "Local user name ${username} contains illegal characters"exit1fiif[ ! -z "${LOCAL_GROUPS}"]thenlocalusergroups="${LOCAL_GROUPS},${LOCAL_MARKER_GROUP}"fiif ! id "${username}" >/dev/null 2>&1; then${USERADD_PROGRAM}${USERADD_ARGS}"${username}"
/bin/chown -R "${username}:${username}""$(evalecho ~$username)"
log "Created new user ${username}"fi
/usr/sbin/usermod -a -G "${localusergroups}""${username}"# Should we add this user to sudo ?if[[ ! -z "${SUDOERS_GROUPS}"]]thenSaveUserFileName=$(echo"${username}" | tr "."" ")SaveUserSudoFilePath="/etc/sudoers.d/$SaveUserFileName"if[["${SUDOERS_GROUPS}"=="##ALL##"]]||echo"${sudousers}" | grep "^${username}\$" > /dev/null
thenecho"${username} ALL=(ALL) NOPASSWD:ALL" > "${SaveUserSudoFilePath}"else[[ ! -f "${SaveUserSudoFilePath}"]]|| rm "${SaveUserSudoFilePath}"fifi}function delete_local_user(){# First, make sure no new sessions can be started
/usr/sbin/usermod -L -s /sbin/nologin "${1}"||true# ask nicely and give them some time to shutdown
/usr/bin/pkill -15 -u "${1}"||true
sleep 5# Dont want to close nicely? DIE!
/usr/bin/pkill -9 -u "${1}"||true
sleep 1# Remove account now that all processes for the user are gone
/usr/sbin/userdel -f -r "${1}"
log "Deleted user ${1}"}function clean_iam_username(){localclean_username="${1}"clean_username=${clean_username//"+"/".plus."}clean_username=${clean_username//"="/".equal."}clean_username=${clean_username//","/".comma."}clean_username=${clean_username//"@"/".at."}echo"${clean_username}"}function sync_accounts(){if[ -z "${LOCAL_MARKER_GROUP}"]then
log "Please specify a local group to mark imported users. eg iam-synced-users"exit1fi# Check if local marker group exists, if not, create it
/usr/bin/getent group "${LOCAL_MARKER_GROUP}" >/dev/null 2>&1|| /usr/sbin/groupadd "${LOCAL_MARKER_GROUP}"# declare and set some variableslocal iam_users
local sudo_users
local local_users
local intersection
local removed_users
local user
# init group and sudoers from tags
get_iam_groups_from_tag
get_sudoers_groups_from_tag
# setup the aws credentials if needed
setup_aws_credentials
iam_users=$(get_clean_iam_users | sort | uniq)if[[ -z "${iam_users}"]]then
log "we just got back an empty iam_users user list which is likely caused by an IAM outage!"exit1fisudo_users=$(get_clean_sudoers_users | sort | uniq)if[[ ! -z "${SUDOERS_GROUPS}"]]&&[[ ! "${SUDOERS_GROUPS}"=="##ALL##"]]&&[[ -z "${sudo_users}"]]then
log "we just got back an empty sudo_users user list which is likely caused by an IAM outage!"exit1filocal_users=$(get_local_users | sort | uniq)intersection=$(echo${local_users}${iam_users} | tr " ""\n" | sort | uniq -D | uniq)removed_users=$(echo${local_users}${intersection} | tr " ""\n" | sort | uniq -u)# Add or update the users found in IAMfor user in${iam_users}; doif["${#user}" -le "32"]then
create_or_update_local_user "${user}""$sudo_users"else
log "Can not import IAM user ${user}. User name is longer than 32 characters."fidone# Remove users no longer in the IAM group(s)for user in${removed_users}; do
delete_local_user "${user}"done}
sync_accounts