特定インスタンスだけにFleet Manager でRDPする権限
やりたいこと
IAM ユーザに対して Fleet Manager を使用して Windows インスタンスへ RDP 接続を行う最低限の権限を設定したい
条件
Fleet Manager でのアクセスはインスタンスのタグにて "PlayerSsmAccess" キーの値が "true" となっているものに限定されている ※ABAC での権限付与
ABAC #とは
属性ベースのアクセス制御 (ABAC) は、属性に基づいて許可を定義する認可戦略です。これらの属性は、AWS でタグと呼ばれています。タグは、IAM エンティティ (ユーザーまたはロール) を含めた IAM リソース、および AWS リソースにアタッチできます。IAM プリンシパルに対して、単一の ABAC ポリシー、または少数のポリシーのセットを作成できます。これらの ABAC ポリシーは、プリンシパルのタグがリソースタグと一致するときに操作を許可するように設計することができます。ATBAC は、急成長する環境や、ポリシー管理が煩雑になる状況で役に立ちます。
シンプルに言うとリソースにタグを付与して、アクセスするリソースに対しては特定のタグが付与している場合のみアクセスができるといった権限設定を行う管理方法です。
これを行うことで dev, prod など環境ごとにタグを付与するだけで個別のリソースに対するアクセス制限をアクセス元のリソースに行う必要なくなるといったものになります。
背景
社内 CTF イベントで使用している環境で参加者が使う権限が大きく制限されたユーザに新しくインスタンスに対する Fleet Manager での RDP の権限を付与する必要があった。
すでに Session Manager を使用した SSH 接続は上記条件で実装されているため、それに沿った形で実装する必要があった(複数のパラメータ管理で煩雑になることを避けたかった)
いきなり結論
以下3つの設定が必要
- Fleet Manager を使用する IAM ユーザに以下3つの権限があること ※すでに Session Manager でのアクセス権は持っている前提、ない場合 ssm:StartSession とかも必要なはず
- "ssm-guiconnect:StartConnection"
- "ssm-guiconnect:GetConnection"
- "ssm-guiconnect:CancelConnection"
-
Fleet Manager でアクセスしたいインスタンスに対してタグで "PlayerSsmAccess" キーを付与して値は "true" としてあること
-
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 でセッションが張れなかった際のエラー文とその対処方法は以下にとてもわかりやすくまとめられていました〜
最後に
ちょっと特殊な要件でしたがどなたかの参考になれば幸いです~
Discussion