aws-ec2-ssh を使うと EC2 上にある Linux ユーザ情報を IAM と同期させることが出来ます。 ログイン時に必要な SSH 公開鍵も IAM と同期出来る為、ユーザの追加・削除を (Linux では無く) IAM 上だけで完結出来るようになります。
ゴール
今回、「EC2 上の Linux」「IAM Role」「IAM Policy」の関係は以下のようにします。
制限 / 注意点
GitHub には以下の制限が記載されていました。
- 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.
また、以下の点について注意する必要があります。
- aws-ec2-ssh は AWS CLI に依存している為、予め AWS CLI がインストールされていること
- SELinux が適切に設定されていること (または SELinux が無効化されていること)
- IAM には ED25519 形式の鍵は登録出来ないこと
Step.1
IAM で以下の内容を持った新規ポリシーを作成します。 ポリシー名は AmazonEC2SshSyncIAM
にしました。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:ListSSHPublicKeys",
"iam:GetSSHPublicKey",
"iam:GetGroup"
],
"Resource": [
"arn:aws:iam::*:user/*",
"arn:aws:iam::*:group/*"
]
},
{
"Effect": "Allow",
"Action": "iam:ListUsers",
"Resource": "*"
}
]
}
Step.2
前のステップで作成した IAM Policy を参照する形で IAM Role を作成します。 今回は SshRole
という名前にしました。
Step.3
次は以下のようにグループとユーザを作成します。
ふたつグループを作成しますが、これらは以下のように使い分けます。 今回は作成したグループを aws-ec2-ssh
の制御にしか使わない為、グループには何もポリシーを紐付けませんでした。
グループ名 | sudo 可否 |
---|---|
SshAdministrators |
可能 |
SshMembers |
不可能 |
ユーザも作成します。 所属グループは以下の方針とします。 IAM の仕様上、ユーザ作成時には必ず「プログラムによるアクセス」または「AWS マネジメントコンソールへのアクセス」のいずれか、または両方を選択する必要があります。 ここは任意に設計すると良いと思います。
adminX
ユーザはSshAdministrators
とSshMembers
グループの両方に参加させるmemberX
ユーザはSshMembers
グループにのみ参加させる
No. | ユーザ名 | SshAdministratos |
SshMembers |
---|---|---|---|
1 | admin1 | ○ | ○ |
2 | admin2 | ○ | ○ |
3 | member1 | ○ | X |
4 | member2 | ○ | X |
Step.4
EC2 で Linux を 3 台、作成しました。 3 台とも、同じ SshRole
という IAM Role を関連付けています。
No. | 名前 | OS | IAM Role | AMI ID |
---|---|---|---|---|
1 | Server-1 | AmazonLinux2 | SshRole |
ami-0f310fced6141e627 |
2 | Server-2 | CentOS7 | SshRole |
ami-06a46da680048c8ae |
3 | Server-3 | Ubuntu20 | SshRole |
ami-0c1ac8728ef7f87a4 |
Step.5
OS 毎にインストールしていきます。
AmazonLinux2
RPM パッケージから簡単にインストール出来ます。
rpm -i https://s3-eu-west-1.amazonaws.com/widdix-aws-ec2-ssh-releases-eu-west-1/aws-ec2-ssh-1.9.2-1.el7.centos.noarch.rpm
CentOS7
標準状態で aws
コマンドが存在しない為、先に AWS CLI v2 をインストールします。
yum -y install unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
後は AmazonLinux2 同様、RPM パッケージからインストールします。
rpm -i https://s3-eu-west-1.amazonaws.com/widdix-aws-ec2-ssh-releases-eu-west-1/aws-ec2-ssh-1.9.2-1.el7.centos.noarch.rpm
Ubuntu20.04 LTS
CentOS7 同様、先に AWS CLi v2 をインストールします。
apt -y install unzip
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
./aws/install
Ubuntu では RPM パッケージからのインストールが出来ない為、ソースコードからインストールします。 ただ、ソースコードからインストールすると、インストールしただけで (まだ設定していないにも関わらず) IAM とユーザ情報が同期されてしまいました…??
git clone --depth 1 https://github.com/widdix/aws-ec2-ssh.git
cd aws-ec2-ssh/
./install.sh
Step.6
設定ファイルは /etc/aws-ec2-ssh.conf
に存在します。 これを以下のように書き換えます。
cat << EOF > /etc/aws-ec2-ssh.conf
IAM_AUTHORIZED_GROUPS="SshMembers"
LOCAL_MARKER_GROUP="iam-users"
SUDOERS_GROUPS="SshAdministratos"
DONOTSYNC=0
EOF
Step.7
手動で同期スクリプトを実行し、意図した通りに IAM とユーザ情報が同期されることを確認します。
インストール方法 | 同期スクリプトのファイルパス |
---|---|
RPM パッケージから | /usr/bin/import_users.sh |
ソースコードから | /opt/import_users.sh |
以下は手動同期が正常終了した後にユーザ情報を確認した実行例です。
# id admin1
uid=1001(admin1) gid=1002(admin1) groups=1002(admin1),1001(iam-users)
# id admin2
uid=1002(admin2) gid=1003(admin2) groups=1003(admin2),1001(iam-users)
# id member1
uid=1003(member1) gid=1004(member1) groups=1004(member1),1001(iam-users)
# id member2
uid=1004(member2) gid=1005(member2) groups=1005(member2),1001(iam-users)
Step.8
IAM 上で以下のようにユーザを削除・作成します。
admin2
を削除するmember3
を追加する
結果としてユーザは以下となりました。
ユーザ情報は /etc/cron.d/import_users
の cron 設定により、デフォルトでは 10 分間隔で IAM と同期します。 最長 10 分待ってユーザ情報が IAM と同期することを確認します。
参考
各ファイルの初期状態 (メモ作成時点)
/etc/cron.d/import_users
*/10 * * * * root /usr/bin/import_users.sh
/etc/aws-ec2-ssh.conf
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
/usr/bin/import_users.sh
#!/bin/bash -e
function log() {
/usr/bin/logger -i -p auth.info -t aws-ec2-ssh "$@"
}
# check if AWS CLI exists
if ! [ -x "$(which aws)" ]; then
log "aws executable not found - exiting!"
exit 1
fi
# 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"
exit 1
fi
# 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 variable
INSTANCE_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}" ]]
then
stscredentials=$(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 tag
function get_iam_groups_from_tag() {
if [ "${IAM_AUTHORIZED_GROUPS_TAG}" ]
then
IAM_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"
else
for group in $(echo ${IAM_AUTHORIZED_GROUPS} | tr "," " "); do
aws iam get-group \
--group-name "${group}" \
--query "Users[].[UserName]" \
--output text \
| sed "s/\r//g"
done
fi
}
# Run all found iam users through clean_iam_username
function 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 users
function 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 tag
function get_sudoers_groups_from_tag() {
if [ "${SUDOERS_GROUPS_TAG}" ]
then
SUDOERS_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 access
function 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 group
function 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 group
function 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 eval
if [[ ! "${username}" =~ ^[0-9a-zA-Z\._\-]{1,32}$ ]]
then
log "Local user name ${username} contains illegal characters"
exit 1
fi
if [ ! -z "${LOCAL_GROUPS}" ]
then
localusergroups="${LOCAL_GROUPS},${LOCAL_MARKER_GROUP}"
fi
if ! id "${username}" >/dev/null 2>&1; then
${USERADD_PROGRAM} ${USERADD_ARGS} "${username}"
/bin/chown -R "${username}:${username}" "$(eval echo ~$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}" ]]
then
SaveUserFileName=$(echo "${username}" | tr "." " ")
SaveUserSudoFilePath="/etc/sudoers.d/$SaveUserFileName"
if [[ "${SUDOERS_GROUPS}" == "##ALL##" ]] || echo "${sudousers}" | grep "^${username}\$" > /dev/null
then
echo "${username} ALL=(ALL) NOPASSWD:ALL" > "${SaveUserSudoFilePath}"
else
[[ ! -f "${SaveUserSudoFilePath}" ]] || rm "${SaveUserSudoFilePath}"
fi
fi
}
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() {
local clean_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"
exit 1
fi
# 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 variables
local 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!"
exit 1
fi
sudo_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!"
exit 1
fi
local_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 IAM
for user in ${iam_users}; do
if [ "${#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."
fi
done
# Remove users no longer in the IAM group(s)
for user in ${removed_users}; do
delete_local_user "${user}"
done
}
sync_accounts
コメント