LoginSignup
64

More than 3 years have passed since last update.

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

Last updated at Posted at 2016-04-30

公式で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" }
        ]
      }
    }
  }
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64