💬

特定インスタンスだけにFleet Manager でRDPする権限

2024/03/13に公開

やりたいこと

IAM ユーザに対して Fleet Manager を使用して Windows インスタンスへ RDP 接続を行う最低限の権限を設定したい

条件

Fleet Manager でのアクセスはインスタンスのタグにて "PlayerSsmAccess" キーの値が "true" となっているものに限定されている ※ABAC での権限付与

ABAC #とは

属性ベースのアクセス制御 (ABAC) は、属性に基づいて許可を定義する認可戦略です。これらの属性は、AWS でタグと呼ばれています。タグは、IAM エンティティ (ユーザーまたはロール) を含めた IAM リソース、および AWS リソースにアタッチできます。IAM プリンシパルに対して、単一の ABAC ポリシー、または少数のポリシーのセットを作成できます。これらの ABAC ポリシーは、プリンシパルのタグがリソースタグと一致するときに操作を許可するように設計することができます。ATBAC は、急成長する環境や、ポリシー管理が煩雑になる状況で役に立ちます。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/introduction_attribute-based-access-control.html

シンプルに言うとリソースにタグを付与して、アクセスするリソースに対しては特定のタグが付与している場合のみアクセスができるといった権限設定を行う管理方法です。

これを行うことで dev, prod など環境ごとにタグを付与するだけで個別のリソースに対するアクセス制限をアクセス元のリソースに行う必要なくなるといったものになります。

背景

社内 CTF イベントで使用している環境で参加者が使う権限が大きく制限されたユーザに新しくインスタンスに対する Fleet Manager での RDP の権限を付与する必要があった。
すでに Session Manager を使用した SSH 接続は上記条件で実装されているため、それに沿った形で実装する必要があった(複数のパラメータ管理で煩雑になることを避けたかった)

いきなり結論

以下3つの設定が必要

  1. Fleet Manager を使用する IAM ユーザに以下3つの権限があること ※すでに Session Manager でのアクセス権は持っている前提、ない場合 ssm:StartSession とかも必要なはず
  • "ssm-guiconnect:StartConnection"
  • "ssm-guiconnect:GetConnection"
  • "ssm-guiconnect:CancelConnection"
  1. Fleet Manager でアクセスしたいインスタンスに対してタグで "PlayerSsmAccess" キーを付与して値は "true" としてあること

  2. SSM Document "AWS-StartPortForwardingSession" に対してタグで "PlayerSsmAccess" キーを付与して値は "true" としてあること

CloudFormation テンプレート

この環境は CloudFormation で自動展開されるためサンプルとして該当箇所のテンプレートを載せておきます。

AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation ec2 template.

Parameters:
  PublicSubnet1ID:
    Type: String
  VpcId:
    Type: String
  PlayerPassword:
    Type: String

Resources:
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: SG to allow outbound communication
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: 0.0.0.0/0

  DefaultRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal: {
              "Service": "ec2.amazonaws.com"
            }
          Action: ['sts:AssumeRole']
      Path: /
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

  SharedInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: SharedInstanceProfile
      Path: /
      Roles:
      - !Ref DefaultRole

  Windows:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base}}' # Windows 2022
      InstanceType: m5.large
      Tags:
        - Key: PlayerSsmAccess
          Value: true
        - Key: Name
          Value: Windows-Example-Machine
      SecurityGroupIds:
        - !Ref SecurityGroup
      IamInstanceProfile: !Ref SharedInstanceProfile
      SubnetId: !Ref PublicSubnet1ID
      UserData:
        Fn::Base64: !Sub
          - |
            <powershell>
            Set-NetFirewallProfile -Enabled false
            New-LocalUser -Name Player -Password (ConvertTo-SecureString ${Pass} -AsPlainText -Force) -UserMayNotChangePassword
            Start-Sleep -Seconds 5
            Add-LocalGroupMember -Group Administrators -Member Player
            </powershell>
            <persist>true</persist>
          - Pass: !Ref PlayerPassword

  Linux:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}' # Amazon Linux 2
      InstanceType: m5.large
      Tags:
        - Key: PlayerSsmAccess
          Value: true
        - Key: Name
          Value: Linux-Example-Machine
      IamInstanceProfile: !Ref SharedInstanceProfile
      SecurityGroupIds:
        - !Ref SecurityGroup
      SubnetId: !Ref PublicSubnet1ID
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -x
          download(){
          until curl -f $@ ;
          do
          sleep 1
          done
          }

  PlayerGroup:
    Type: AWS::IAM::Group
    Properties:
      GroupName: PlayerGroup
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSServiceCatalogEndUserFullAccess

  Player:
    Type: AWS::IAM::User
    Properties:
      LoginProfile:
        Password: !Ref PlayerPassword
        PasswordResetRequired: false
      UserName: !Sub Player-${AWS::StackName}
      Groups:
        - !Ref PlayerGroup

  PlayerPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: PlayerBasePolicy
      Groups:
        - !Ref PlayerGroup
      PolicyDocument:
        Statement:
          - Sid: BasePermission
            Action:
              - ec2:DescribeInstances
              - ssm:DescribeSessions
              - ssm:GetConnectionStatus
              - ssm:DescribeInstanceProperties
              - ssm:ListDocuments
              - ssm:GetParameter
              - ssm:DescribeParameters
              - ssm:PutParameter
              - ssm:CreateAssociation
              - ssm:UpdateAssociation
              - ssm:DescribeDocumentParameters
              - servicecatalog:SearchProvisionedProducts
              - ssm-guiconnect:GetConnection
              - ssm-guiconnect:StartConnection
              - ssm-guiconnect:CancelConnection
            Resource: "*"
            Effect: Allow

          - Sid: SSMPermissionFor  #(need to check why this permission necessary)
            Action:
              - ssm:DescribeInstanceInformation
              - ssm:ListAssociations
              - ssm:DescribeAssociation
              - ssm:DeleteAssociation
            Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:*'
            Effect: Allow

          - Sid: StartSSMandFleerManagerSession
            Condition:
              StringLike:
                ssm:resourceTag/PlayerSsmAccess: 'true'
            Action:
              - ssm:StartSession
            Resource: "*"
            Effect: Allow

          - Sid: NeedFleetManagerSession
            Action:
              - ssm:GetDocument
            Resource:
              - arn:aws:ssm:::document/SSM-SessionManagerRunShell
            Effect: Allow

          - Sid: TerminateOwnSSMSessionOnly
            Action:
              - ssm:TerminateSession
            Resource: arn:aws:ssm:*:*:session/${aws:username}-*
            Effect: Allow

# ------------------------------------------------------------#
# Custom Resource
# ------------------------------------------------------------#
  CustomLambdaSSMTag:
    Type: Custom::CustomLambdaSSMTag
    Properties:
      ServiceToken: !GetAtt LambdaFunctionSSMTag.Arn"

# ------------------------------------------------------------#
#  Lmabda
# ------------------------------------------------------------#

  LambdaFunctionSSMTag:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Role: !GetAtt
        - LambdaFunctionExecutionSSMTagRole
        - Arn
      Runtime: python3.9
      Code:
        ZipFile: !Sub |
          import boto3
          import base64
          import os
          import cfnresponse

          client = boto3.client('ssm')

          def lambda_handler(event, context):
            doc_name = "AWS-StartPortForwardingSession"
            tag_key = "PlayerSsmAccess"
            tag_value = "true"
            ssmdoc_doc_tags = [{"Key": tag_key, "Value": tag_value}]

            try:
              if event['RequestType'] == 'Create':
                response = client.add_tags_to_resource(
                    ResourceType='Document',
                    ResourceId=doc_name,
                    Tags=ssmdoc_doc_tags
                )
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

            except Exception as e:
              cfnresponse.send(event, context, cfnresponse.FAILED, {})
              print(e)

            try:
              if event['RequestType'] == 'Delete':
                response = client.remove_tags_from_resource(
                    ResourceType='Document',
                    ResourceId=doc_name,
                    TagKeys=[tag_key]
                )
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

            except Exception as e:
              cfnresponse.send(event, context, cfnresponse.FAILED, {})
              print(e)

  LambdaFunctionExecutionSSMTagRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: "ssmtag-lambda-policy"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:AddTagsToResource
                  - ssm:RemoveTagsFromResource
                Resource:
                  - !Sub "arn:aws:ssm:*:*:document/AWS-StartPortForwardingSession"

はまったポイント

① IAM 権限に起因する StartSeesion 時のエラー

エラー文

StartConnection API オペレーションの呼び出し中にエラーが発生しました。AccessDeniedException: An error occurred (AccessDeniedException) when calling the StartSession operation: User: arn:aws:iam::xxxxxxxxxxxx:user/Palyer-xxx is not authorized to perform: ssm:StartSession on resource: arn:aws:ssm:ap-northeast-1::document/AWS-StartPortForwardingSession because no identity-based policy allows the ssm:StartSession action

原因

Fleet Manager での RDP のセッションを張る場合、裏側で SSM Document の "AWS-StartPortForwardingSession" を利用して行っているため今回のような ABAC ベースの権限管理を行なっている場合、 Document に対してもタグを付与して Fleet Manager でアクセスする IAM ユーザが利用できるようにする必要がある

対策

SSM Document "AWS-StartPortForwardingSession" に対してタグで "PlayerSsmAccess" キーを付与して値は "true" を設定する。
なお、"AWS-StartPortForwardingSession" は AWS が管理している Document であるため、今回の要件は CloudFormation の展開による自動で全ての設定を完了する必要があったため以下 Lambda を実行するカスタムリソースでタグの付与及び削除を実装しています。

import boto3
import base64
import os
import cfnresponse

client = boto3.client('ssm')

def lambda_handler(event, context):
  doc_name = "AWS-StartPortForwardingSession"
  tag_key = "PlayerSsmAccess"
  tag_value = "true"
  ssmdoc_doc_tags = [{"Key": tag_key, "Value": tag_value}]

try:
  if event['RequestType'] == 'Create':
    response = client.add_tags_to_resource(
        ResourceType='Document',
        ResourceId=doc_name,
        Tags=ssmdoc_doc_tags
    )
  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

except Exception as e:
  cfnresponse.send(event, context, cfnresponse.FAILED, {})
  print(e)

try:
  if event['RequestType'] == 'Delete':
    response = client.remove_tags_from_resource(
        ResourceType='Document',
        ResourceId=doc_name,
        TagKeys=[tag_key]
    )
  cfnresponse.send(event, context, cfnresponse.SUCCESS, {})

except Exception as e:
  cfnresponse.send(event, context, cfnresponse.FAILED, {})
  print(e)

② Fleet Manager でセッションを張る際のエラー

エラー文

Its taking longer than expected to render the Remote Desktop connection. Please try again.

原因

Fleet Manager でアクセスする際の Windows のユーザ名/パスワードが誤っていた

対策

正しくユーザが存在すること、パスワードがあっていることのチェックですね。
エラー文から認証ミスであることがわからなかったので最初は悩みました。

ただ調べてみると Fleet Manager でセッションが張れなかった際のエラー文とその対処方法は以下にとてもわかりやすくまとめられていました〜
https://repost.aws/ja/knowledge-center/systems-manager-ec2-windows-connection-rdp

最後に

ちょっと特殊な要件でしたがどなたかの参考になれば幸いです~

Discussion