Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
64
Help us understand the problem. What is going on with this article?
@noda_sin

IAMで踏み台ホストのユーザーとSSH公開鍵を一元管理する

More than 1 year has passed since last update.

公式でAWS EC2 Connectという機能が入ったので、そちらを使いましょう。
https://dev.classmethod.jp/cloud/aws/ec2-instance-connect/

−−−−−−−−−−−−

AWSの踏み台ホストのユーザー管理ってどうしていますか?IAMユーザーとホストユーザーで管理が2重になり、大変‥なんとかならないかなぁと思っていたら、IAMユーザーごとにCodeCommit用のSSH公開鍵が登録できるのを見つけたので、今回はこれを使ってホストユーザーを管理をする機構を作ってみました。

やりたいこと

  1. 自動でIAMに登録されているユーザーと同じユーザーをホストに作成する
  2. IAMユーザーごとに登録しているCodeCommitのSSH公開鍵を取得し,~/.ssh/authorized_keysにセットする

主な部品の検証

ホストに登録されているユーザー一覧を取得する

直接ホストに作成されているユーザーの取得方法がわからなかったので、sedを駆使して/home直下に作成されているユーザーディレクトリの一覧を取得するようにしました。

find /home -maxdepth 1 -type d | grep '^/home/' | sed -e 's/^\/home\/\(.\)/\1/'

IAMユーザーの一覧を取得する

aws-cliで,IAMユーザーの名前一覧を取得できました。

aws iam list-users | jq -r '.Users[].UserName'

IAMユーザーのCodeCommit用SSH公開鍵を取得する

ユーザーごとに鍵のID一覧を取得して、IDから鍵の実体が取れました。

key_ids=$(aws iam list-ssh-public-keys --user-name $iam_user | jq -r 'select(.SSHPublicKeys[].Status == "Active") | .SSHPublicKeys[].SSHPublicKeyId')
for key_id in $key_ids; do
  aws iam get-ssh-public-key --user-name $iam_user --ssh-public-key-id $key_id --encoding SSH | jq -r '.SSHPublicKey.SSHPublicKeyBody' >> authorized_keys
done

完成版のスクリプト

最終的に以下のようなフローを実行させるようにしました。

  1. ホストにIAMユーザーと同じ名前でユーザーを作成
  2. IAMユーザーごとに登録しているCodeCommitのSSH公開鍵を取得し、更新がある場合は~/.ssh/authorized_keysにセット
  3. IAMユーザーから削除されたユーザーはホストからも削除
  4. 鍵の更新やIAMから削除があったユーザーが現在SSHで接続している場合は、コネクションを強制切断
#!/bin/bash
PATH=$PATH:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin
function contains() {
  for row in $1; do
    if [ "$row" == "$2" ]; then
      echo "y"
      return 0
    fi
  done
  echo "n"
  return 1
}
# current users in host.
host_users=$(find /home -maxdepth 1 -type d | grep '^/home/' | sed -e 's/^\/home\/\(.\)/\1/')
# create iam users
iam_users=$(aws iam list-users | jq -r '.Users[].UserName')
for iam_user in $iam_users; do
  user_home="/home/$iam_user"
  user_ssh_dir="$user_home/.ssh"
  useradd "$iam_user"
  # setup ssh directory
  mkdir -p "$user_ssh_dir"
  chown -R "$iam_user:$iam_user" "$user_home"
  chmod -R 500 "$user_home"
  # setup ssh key
  touch "$user_ssh_dir/authorized_keys"
  touch "$user_ssh_dir/new_authorized_keys"
  key_ids=$(aws iam list-ssh-public-keys --user-name $iam_user | jq -r 'select(.SSHPublicKeys[].Status == "Active") | .SSHPublicKeys[].SSHPublicKeyId')
  for key_id in $key_ids; do
    aws iam get-ssh-public-key --user-name $iam_user --ssh-public-key-id $key_id --encoding SSH | jq -r '.SSHPublicKey.SSHPublicKeyBody' >> "$user_ssh_dir/new_authorized_keys"
  done
  keys_diff=$(diff "$user_ssh_dir/authorized_keys" "$user_ssh_dir/new_authorized_keys")
  if [ "$keys_diff" == "" ]; then
    rm -f "$user_ssh_dir/new_authorized_keys"
    continue
  fi
  rm -f "$user_ssh_dir/authorized_keys"
  mv "$user_ssh_dir/new_authorized_keys" "$user_ssh_dir/authorized_keys"
  chown "$iam_user:$iam_user" "$user_ssh_dir/authorized_keys"
  chmod 500 "$user_ssh_dir/authorized_keys"
  ps aux | grep "sshd: $iam_user@pts/" | grep -v grep | awk '{ print "kill -9", $2 }' | sh
done
# delete iam user deleted by aws from host
for host_user in $host_users; do
  # if ec2-user, not action
  if [ "$host_user" = "ec2-user" ]; then
    continue
  fi
  if [[ "$(contains "${iam_users[@]}" "$host_user")" == "y" ]]; then
    continue
  fi
  ps aux | grep "sshd: $host_user@pts/" | grep -v grep | awk '{ print "kill -9", $2 }' | sh
  userdel -r "$host_user"
  rm -rf "/home/$host_user"
  echo "delete $host_user from host."
done

あとは、cronでこれを適当な間隔でまわしてあげれば完成です。
お手物の環境に直ぐにデプロイできるようにCloudFromationのテンプレートを作ってみましたので、良ければ使ってください。

{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description" : "Bastion Instance",

  "Parameters" : {
    "KeyName" : {
      "Description"           : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
      "Type"                  : "AWS::EC2::KeyPair::KeyName",
      "ConstraintDescription" : "can contain only alphanumeric characters, spaces, dashes and underscores."
    },
    "InstanceType" : {
      "Description"           : "Instance type for node.",
      "Type"                  : "String",
      "Default"               : "t2.nano",
      "AllowedValues"         : [ "t2.nano", "t2.micro","t2.small","t2.medium","m3.medium","m3.large","m3.xlarge","m3.2xlarge","c3.large","c3.xlarge","c3.2xlarge","c3.4xlarge","c3.8xlarge" ],
      "ConstraintDescription" : "must be a valid T2, M3 or C3 instance type."
    },
    "Subnets" : {
      "Description"           : "ID of your existing subnet for launching Bastion",
      "Type"                  : "List<AWS::EC2::Subnet::Id>"
    },
    "SSHSecurityGroup" : {
      "Description"           : "SecurityGroup for accessing with SSH",
      "Type"                  : "AWS::EC2::SecurityGroup::Id"
    },
    "ElasticIpId": {
      "Description"           : "ID of ElasticIp for launching Bastion",
      "Type"                  : "String",
      "AllowedPattern"        : "eipalloc-[a-z0-9]{8}",
      "ConstraintDescription" : "Input format is eipalloc-xxxxxxxx"
    },
    "Recurrence" : {
      "Type"                  : "String",
      "Description"           : "schedule for updating keys. crontab style syntax. ex) 0 0 * * *",
      "AllowedPattern"        : "(((([*])|(((([0-5])?[0-9])((-(([0-5])?[0-9])))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))(,(((([*])|(((([0-5])?[0-9])((-(([0-5])?[0-9])))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?)))* (((([*])|(((((([0-1])?[0-9]))|(([2][0-3])))((-(((([0-1])?[0-9]))|(([2][0-3])))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))(,(((([*])|(((((([0-1])?[0-9]))|(([2][0-3])))((-(((([0-1])?[0-9]))|(([2][0-3])))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?)))* (((((((([*])|(((((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))((-(((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))|(L)|(((((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))W))))(,(((((([*])|(((((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))((-(((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))|(L)|(((((([1-2])?[0-9]))|(([3][0-1]))|(([1-9])))W)))))*)|([?])) (((([*])|((((([1-9]))|(([1][0-2])))((-((([1-9]))|(([1][0-2])))))?))|((((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OKT)|(NOV)|(DEC))((-((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OKT)|(NOV)|(DEC))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))(,(((([*])|((((([1-9]))|(([1][0-2])))((-((([1-9]))|(([1][0-2])))))?))|((((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OKT)|(NOV)|(DEC))((-((JAN)|(FEB)|(MAR)|(APR)|(MAY)|(JUN)|(JUL)|(AUG)|(SEP)|(OKT)|(NOV)|(DEC))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?)))* (((((((([*])|((([0-6])((-([0-6])))?))|((((SUN)|(MON)|(TUE)|(WED)|(THU)|(FRI)|(SAT))((-((SUN)|(MON)|(TUE)|(WED)|(THU)|(FRI)|(SAT))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))|((([0-6])L))|(W)|(([#][1-5]))))(,(((((([*])|((([0-6])((-([0-6])))?))|((((SUN)|(MON)|(TUE)|(WED)|(THU)|(FRI)|(SAT))((-((SUN)|(MON)|(TUE)|(WED)|(THU)|(FRI)|(SAT))))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))|((([0-6])L))|(W)|(([#][1-5])))))*)|([?]))((( (((([*])|((([1-2][0-9][0-9][0-9])((-([1-2][0-9][0-9][0-9])))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?))(,(((([*])|((([1-2][0-9][0-9][0-9])((-([1-2][0-9][0-9][0-9])))?)))((/(([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?([0-9])?[0-9])))?)))*))?)",
      "Default"               : "*/10 * * * *"
    }
  },

  "Mappings": {
     "AWSBastionAMI" : {
      "ap-northeast-1" : { "AMI" : "ami-383c1956" }
    }
  },

  "Resources" : {

    "BastionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
           "Statement": [ {
              "Effect": "Allow",
              "Principal": {
                 "Service": [ "ec2.amazonaws.com" ]
              },
              "Action": [ "sts:AssumeRole" ]
           } ]
        },
        "Path": "/",
        "Policies": [ {
          "PolicyName": "Bastion_Takeover",
          "PolicyDocument": {
            "Statement": [ {
               "Effect": "Allow",
               "Action": [
                  "iam:ListUsers",
                  "iam:ListSshPublicKeys",
                  "iam:GetSshPublicKey",
                  "ec2:AssociateAddress"
               ],
               "Resource": "*"
            } ]
          }
        } ]
      }
    },

    "BastionRoleProfile": {
       "Type": "AWS::IAM::InstanceProfile",
       "Properties": {
          "Path": "/",
          "Roles": [ {
             "Ref": "BastionRole"
          } ]
       }
    },

    "BastionLaunchConfig" : {
      "Type" : "AWS::AutoScaling::LaunchConfiguration",
      "Metadata" : {
        "AWS::CloudFormation::Init" : {
          "config" : {
            "packages" : {
              "yum" : {
                "jq" : []
              }
            },
            "files" : {
              "/etc/sshkey-update/cron.sh" : {
                "content" : { "Fn::Join" : [ "\n", [
                  "#!/bin/bash",
                  "PATH=$PATH:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin",
                  "function contains() {",
                  "  for row in $1; do",
                  "    if [ \"$row\" == \"$2\" ]; then",
                  "      echo \"y\"",
                  "      return 0",
                  "    fi",
                  "  done",
                  "  echo \"n\"",
                  "  return 1",
                  "}",
                  "# current users in host.",
                  "host_users=$(find /home -maxdepth 1 -type d | grep '^/home/' | sed -e 's/^\\/home\\/\\(.\\)/\\1/')",
                  "# create iam users",
                  "iam_users=$(aws iam list-users | jq -r '.Users[].UserName')",
                  "for iam_user in $iam_users; do",
                  "  user_home=\"/home/$iam_user\"",
                  "  user_ssh_dir=\"$user_home/.ssh\"",
                  "  useradd \"$iam_user\"",
                  "  # setup ssh directory",
                  "  mkdir -p \"$user_ssh_dir\"",
                  "  chown -R \"$iam_user:$iam_user\" \"$user_home\"",
                  "  chmod -R 500 \"$user_home\"",
                  "  # setup ssh key",
                  "  touch \"$user_ssh_dir/authorized_keys\"",
                  "  touch \"$user_ssh_dir/new_authorized_keys\"",
                  "  key_ids=$(aws iam list-ssh-public-keys --user-name $iam_user | jq -r 'select(.SSHPublicKeys[].Status == \"Active\") | .SSHPublicKeys[].SSHPublicKeyId')",
                  "  for key_id in $key_ids; do",
                  "    aws iam get-ssh-public-key --user-name $iam_user --ssh-public-key-id $key_id --encoding SSH | jq -r '.SSHPublicKey.SSHPublicKeyBody' >> \"$user_ssh_dir/new_authorized_keys\"",
                  "  done",
                  "  keys_diff=$(diff \"$user_ssh_dir/authorized_keys\" \"$user_ssh_dir/new_authorized_keys\")",
                  "  if [ \"$keys_diff\" == \"\" ]; then",
                  "    rm -f \"$user_ssh_dir/new_authorized_keys\"",
                  "    continue",
                  "  fi",
                  "  rm -f \"$user_ssh_dir/authorized_keys\"",
                  "  mv \"$user_ssh_dir/new_authorized_keys\" \"$user_ssh_dir/authorized_keys\"",
                  "  chown \"$iam_user:$iam_user\" \"$user_ssh_dir/authorized_keys\"",
                  "  chmod 500 \"$user_ssh_dir/authorized_keys\"",
                  "  ps aux | grep \"sshd: $iam_user@pts/\" | grep -v grep | awk '{ print \"kill -9\", $2 }' | sh",
                  "done",
                  "# delete iam user deleted by aws management console from host",
                  "for host_user in $host_users; do",
                  "  # if ec2-user, not action",
                  "  if [ \"$host_user\" = \"ec2-user\" ]; then",
                  "    continue",
                  "  fi",
                  "  if [[ \"$(contains \"${iam_users[@]}\" \"$host_user\")\" == \"y\" ]]; then",
                  "    continue",
                  "  fi",
                  "  ps aux | grep \"sshd: $host_user@pts/\" | grep -v grep | awk '{ print \"kill -9\", $2 }' | sh",
                  "  userdel -r \"$host_user\"",
                  "  rm -rf \"/home/$host_user\"",
                  "  echo \"delete $host_user from host.\"",
                  "done"
                ]]},
                "mode"  : "0755",
                "owner" : "root",
                "group" : "root"
              }
            }
          }
        }
      },
      "Properties" : {
          "InstanceType"             : { "Ref" : "InstanceType" },
          "KeyName"                  : { "Ref" : "KeyName" },
          "ImageId"                  : { "Fn::FindInMap" : [ "AWSBastionAMI", { "Ref" : "AWS::Region" }, "AMI"] },
          "IamInstanceProfile"       : { "Ref" : "BastionRoleProfile" },
          "AssociatePublicIpAddress" : "true",
          "SecurityGroups"           : [ { "Ref" : "SSHSecurityGroup" } ],
          "UserData"                 : { "Fn::Base64" : { "Fn::Join" : [ "", [
            "#!/bin/bash\n",
            "yum update -y\n",
            "export AWS_DEFAULT_REGION=", { "Ref" : "AWS::Region" }, "\n",
            "/opt/aws/bin/cfn-init -s ", { "Ref" : "AWS::StackId" }, " -r BastionLaunchConfig --region ", { "Ref" : "AWS::Region" }, "\n",
            "echo \"", { "Ref" : "Recurrence" }, " /bin/bash /etc/sshkey-update/cron.sh\" >> /tmp/$$.tmp\n",
            "crontab /tmp/$$.tmp && rm -rf /tmp/$$.tmp\n",
            "/bin/bash /etc/sshkey-update/cron.sh\n",
            "instanceId=$(curl http://169.254.169.254/latest/meta-data/instance-id)\n",
            "aws ec2 associate-address --instance-id $instanceId --allocation-id ", { "Ref" : "ElasticIpId" }, "\n"
          ]]}
        }
      }
    },
    "BastionAutoScalingGroup" : {
      "Type"                      : "AWS::AutoScaling::AutoScalingGroup",
      "Properties" : {
        "LaunchConfigurationName" : { "Ref" : "BastionLaunchConfig" },
        "MaxSize"                 : "1",
        "MinSize"                 : "1",
        "VPCZoneIdentifier"       : { "Ref" : "Subnets" },
        "Tags"                    : [
          { "Key" : "Name", "Value" : "bastion", "PropagateAtLaunch" : "true" }
        ]
      }
    }
  }
}
64
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
64
Help us understand the problem. What is going on with this article?