こんにちは!AWS事業本部のおつまみです。
皆さん、Transfer Family を使用して SFTP 経由でファイルを EFS に直接アップロードしたいなぁと思ったことはありますか?私はあります。
Transfer Family は、AWS ストレージサービスとの間でファイルを送受信できる安全な転送サービスです。主にこれら4つの要素を選択し、構築します。
- プロトコル
- SFTP (SSH ファイル転送プロトコル) - Secure Shell 経由のファイル転送
- AS2 (Applicability Statement 2) - ビジネス間データ交換用のメッセージングプロトコル情報
- FTPS (File Transfer Protocol Secure) - TLS 暗号化を使用したファイル転送プロトコル
- FTP (File Transfer Protocol) - 暗号化されていないファイル転送プロトコル
- ID プロバイダー
- サービスマネージド - サービス内でユーザーを作成および管理
- AWS Directory Service情報 - AWS Managed AD でユーザーを有効にする、またはオンプレミス環境または AWS で独自のセルフマネージド AD を使用する
- カスタム ID プロバイダー - 情報任意の ID プロバイダーとの統合によりユーザーを管理する
- エンドポイント
- パブリックアクセス - インターネット経由でアクセス可能
- VPC(内部) - セキュリティグループを使用したアクセスコントロール可能、VPC内のリソースからのみアクセスする場合
- VPC(インターネット) - セキュリティグループを使用したアクセスコントロール可能、VPC外からアクセスする場合
- ドメイン
- Amazon S3
- Amazon EFS
ID プロバイダーにサービスマネージドを使用したいなぁと思った、そこのあなた!
サービスマネージドでは、鍵認証しか行えません。
ただシステム要件によっては、パスワード認証が必須な場合があると思います。
そこで今回はカスタム ID プロバイダーを使用し、鍵とパスワード両方の認証を有効にする方法をご紹介します。
そのため今回は以下を選択した構築方法の作業手順となります。
- プロトコル
- SFTP (SSH ファイル転送プロトコル) - Secure Shell 経由のファイル転送
- ID プロバイダー
- カスタム ID プロバイダー - 情報任意の ID プロバイダーとの統合によりユーザーを管理する
- エンドポイント
- VPC(インターネット) - セキュリティグループを使用したアクセスコントロール可能、VPC外からアクセスする場合
- ドメイン
- Amazon EFS
構成図
今回構築するアーキテクチャの構成図です。
ID管理はSecretsManagerのシークレットで設定します。
また不正にアクセスされないようセキュリティグループでIP制限を行います。
こちらのAWSにより公開されている方式を参考に構築しました。
作業手順
0.前提
下記ブログの「0. 事前準備 鍵の作成」~「4. EFSの作成」を参考に、以下のリソースをあらかじめ作成しておきます。
必要なリソース
- 公開鍵・秘密鍵
- VPC
- EIP
- セキュリティグループ
- EFS
- IAM
1. 公開されているSAMテンプレートの修正
先ほど紹介したAWSブログのthis package
を押下して、構築に必要なSAMテンプレートをダウンロードします。
ダウンロードされたテンプレートを今回の構築用に修正します。
1.1 SAMテンプレート(template.yml)の修正
変更箇所
- Parameterセクション
- TransferEndpointTypeをコメントアウト(26~33行目)
- SecurityGroupIdsを追加(46~49行目下)
- EIPを追加(50~53行目下)
- TransferVPCEndpointをコメントアウト(72~75行目)
- Resourceセクション
- TransferServerをEFSに対応するように修正(102~124行目)
修正前 template.yml
template.yml
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A basic template for creating a Lambda-backed API Gateway for use as
a custom identity provider in AWS Transfer. It authenticates against an
entry in AWS Secrets Manager of the format server-id/username. Additionaly, the secret
must hold the key-value pairs for all user properties returned to AWS Transfer.
If you choose to also launch the AWS Transfer endpoint it will be provisioned with the SFTP protocol.
This can be changed after launch. Note that FTP is only supported on VPC endpoints
You can modify the Lambda function code to do something different after deployment.
Parameters:
CreateServer:
AllowedValues:
- 'true'
- 'false'
Type: String
Description: Whether this stack creates an AWS Transfer endpoint or not. If the endpoint is created as
part of the stack, the custom identity provider is automatically associated with it.
Default: 'true'
SecretsManagerRegion:
Type: String
Description: (Optional) The region the secrets are stored in. If this value is not provided, the
region this stack is deployed in will be used. Use this field if you are deploying this stack in
a region where SecretsMangager is not available.
Default: ''
TransferEndpointType:
AllowedValues:
- 'PUBLIC'
- 'VPC'
Type: String
Default: 'PUBLIC'
Description: Select PUBLIC if you want a public facing AWS Transfer endpoint or VPC if you want a VPC
based endpoint. Note that only SFTP and FTPS are supported on public endpoints.
TransferSubnetIDs:
Type: String
Default: ''
Description: Required if launching a VPC endpoint. Comma-seperated list of subnets that you would like
the AWS Transfer endpoint to be provisioned into.
TransferVPCID:
Type: String
Default: ''
Description: Required if launching a VPC endpoint. The VPC ID that you would like the AWS Transfer endpoint
to be provisioned into.
Conditions:
CreateServer:
Fn::Equals:
- Ref: CreateServer
- 'true'
NotCreateServer:
Fn::Not:
- Condition: CreateServer
SecretsManagerRegionProvided:
Fn::Not:
- Fn::Equals:
- Ref: SecretsManagerRegion
- ''
TransferVPCEndpoint:
Fn::Equals:
- Ref: TransferEndpointType
- 'VPC'
Outputs:
ServerId:
Value:
Fn::GetAtt: TransferServer.ServerId
Condition: CreateServer
StackArn:
Value:
Ref: AWS::StackId
TransferIdentityProviderUrl:
Description: URL to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
Value:
Fn::Join:
- ''
- - https://
- Ref: CustomIdentityProviderApi
- .execute-api.
- Ref: AWS::Region
- .amazonaws.com/
- Ref: ApiStage
Condition: NotCreateServer
TransferIdentityProviderInvocationRole:
Description: IAM Role to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
Value:
Fn::GetAtt: TransferIdentityProviderRole.Arn
Condition: NotCreateServer
Resources:
TransferServer:
Type: AWS::Transfer::Server
Condition: CreateServer
Properties:
EndpointType:
Ref: TransferEndpointType
EndpointDetails:
Fn::If:
- TransferVPCEndpoint
- SubnetIds:
Fn::Split: [',', Ref: TransferSubnetIDs]
VpcId:
Ref: TransferVPCID
- Ref: AWS::NoValue
IdentityProviderDetails:
InvocationRole:
Fn::GetAtt: TransferIdentityProviderRole.Arn
Url:
Fn::Join:
- ''
- - https://
- Ref: CustomIdentityProviderApi
- .execute-api.
- Ref: AWS::Region
- .amazonaws.com/
- Ref: ApiStage
IdentityProviderType: API_GATEWAY
LoggingRole:
Fn::GetAtt: TransferCWLoggingRole.Arn
TransferCWLoggingRole:
Description: IAM role used by Transfer to log API requests to CloudWatch
Type: AWS::IAM::Role
Condition: CreateServer
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- transfer.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSTransferLoggingAccess
CustomIdentityProviderApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: Transfer Family Secrets Manager Integration API
Description: API used for Transfer Family to access user information in Secrets Manager
FailOnWarnings: true
EndpointConfiguration:
Types:
- REGIONAL
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: LambdaSecretsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
Fn::Sub:
- arn:${AWS::Partition}:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:s-*
- SecretsRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
ApiCloudWatchLogsRole:
Description: IAM role used by API Gateway to log API requests to CloudWatch
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: ApiGatewayLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:DescribeLogGroups
- logs:DescribeLogStreams
- logs:PutLogEvents
- logs:GetLogEvents
- logs:FilterLogEvents
Resource: "*"
ApiLoggingAccount:
Type: AWS::ApiGateway::Account
DependsOn:
- CustomIdentityProviderApi
Properties:
CloudWatchRoleArn:
Fn::GetAtt: ApiCloudWatchLogsRole.Arn
ApiStage:
Type: AWS::ApiGateway::Stage
Properties:
DeploymentId:
Ref: ApiDeployment202008
MethodSettings:
- DataTraceEnabled: false
HttpMethod: "*"
LoggingLevel: INFO
ResourcePath: "/*"
RestApiId:
Ref: CustomIdentityProviderApi
StageName: prod
ApiDeployment202008:
DependsOn:
- GetUserConfigRequest
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
TransferIdentityProviderRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: transfer.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: TransferCanInvokeThisApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${CustomIdentityProviderApi}/prod/GET/*
- PolicyName: TransferCanReadThisApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- apigateway:GET
Resource: "*"
GetUserConfigLambda:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Description: A function to lookup and return user data from AWS Secrets Manager.
Handler: index.lambda_handler
Role:
Fn::GetAtt: LambdaExecutionRole.Arn
Runtime: python3.7
Environment:
Variables:
SecretsManagerRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
GetUserConfigLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
FunctionName:
Fn::GetAtt: GetUserConfigLambda.Arn
Principal: apigateway.amazonaws.com
SourceArn:
Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${CustomIdentityProviderApi}/*
ServersResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Fn::GetAtt:
- CustomIdentityProviderApi
- RootResourceId
PathPart: servers
ServerIdResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: ServersResource
PathPart: "{serverId}"
UsersResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: ServerIdResource
PathPart: users
UserNameResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: UsersResource
PathPart: "{username}"
GetUserConfigResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: UserNameResource
PathPart: config
GetUserConfigRequest:
Type: AWS::ApiGateway::Method
DependsOn: GetUserConfigResponseModel
Properties:
AuthorizationType: AWS_IAM
HttpMethod: GET
Integration:
Type: AWS
IntegrationHttpMethod: POST
Uri:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":apigateway:"
- Ref: AWS::Region
- ":lambda:path/2015-03-31/functions/"
- Fn::GetAtt:
- GetUserConfigLambda
- Arn
- "/invocations"
IntegrationResponses:
- StatusCode: 200
RequestTemplates:
application/json: |
{
"username": "$util.urlDecode($input.params('username'))",
"password": "$util.escapeJavaScript($input.params('Password')).replaceAll("\\'","'")",
"protocol": "$input.params('protocol')",
"serverId": "$input.params('serverId')",
"sourceIp": "$input.params('sourceIp')"
}
RequestParameters:
method.request.header.Password: false
ResourceId:
Ref: GetUserConfigResource
RestApiId:
Ref: CustomIdentityProviderApi
MethodResponses:
- StatusCode: 200
ResponseModels:
application/json: UserConfigResponseModel
GetUserConfigResponseModel:
Type: AWS::ApiGateway::Model
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ContentType: application/json
Description: API response for GetUserConfig
Name: UserConfigResponseModel
Schema:
"$schema": http://json-schema.org/draft-04/schema#
title: UserUserConfig
type: object
properties:
HomeDirectory:
type: string
Role:
type: string
Policy:
type: string
PublicKeys:
type: array
items:
type: string
修正後 template.yml
template.yml
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: A basic template for creating a Lambda-backed API Gateway for use as
a custom identity provider in AWS Transfer. It authenticates against an
entry in AWS Secrets Manager of the format server-id/username. Additionaly, the secret
must hold the key-value pairs for all user properties returned to AWS Transfer.
If you choose to also launch the AWS Transfer endpoint it will be provisioned with the SFTP protocol.
This can be changed after launch. Note that FTP is only supported on VPC endpoints
You can modify the Lambda function code to do something different after deployment.
Parameters:
CreateServer:
AllowedValues:
- 'true'
- 'false'
Type: String
Description: Whether this stack creates an AWS Transfer endpoint or not. If the endpoint is created as
part of the stack, the custom identity provider is automatically associated with it.
Default: 'true'
SecretsManagerRegion:
Type: String
Description: (Optional) The region the secrets are stored in. If this value is not provided, the
region this stack is deployed in will be used. Use this field if you are deploying this stack in
a region where SecretsMangager is not available.
Default: ''
# TransferEndpointType:
# AllowedValues:
# - 'PUBLIC'
# - 'VPC'
# Type: String
# Default: 'PUBLIC'
# Description: Select PUBLIC if you want a public facing AWS Transfer endpoint or VPC if you want a VPC
# based endpoint. Note that only SFTP and FTPS are supported on public endpoints.
TransferDomain:
AllowedValues:
- 'EFS'
- 'S3'
Type: String
Default: 'EFS'
Description: Specifies the domain of the storage system that is used for file transfers.
TransferSubnetIDs:
Type: String
Default: ''
Description: Required if launching a VPC endpoint. Comma-seperated list of subnets that you would like
the AWS Transfer endpoint to be provisioned into.
SecurityGroupIds:
Type: String
Default: ''
Description: Required if launching a VPC endpoint.
ElasticIP:
Type: String
Default: ''
Description: Required if launching a VPC endpoint.
TransferVPCID:
Type: String
Default: ''
Description: Required if launching a VPC endpoint. The VPC ID that you would like the AWS Transfer endpoint
to be provisioned into.
Conditions:
CreateServer:
Fn::Equals:
- Ref: CreateServer
- 'true'
NotCreateServer:
Fn::Not:
- Condition: CreateServer
SecretsManagerRegionProvided:
Fn::Not:
- Fn::Equals:
- Ref: SecretsManagerRegion
- ''
# TransferVPCEndpoint:
# Fn::Equals:
# - Ref: TransferEndpointType
# - 'VPC'
Outputs:
ServerId:
Value:
Fn::GetAtt: TransferServer.ServerId
Condition: CreateServer
StackArn:
Value:
Ref: AWS::StackId
TransferIdentityProviderUrl:
Description: URL to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
Value:
Fn::Join:
- ''
- - https://
- Ref: CustomIdentityProviderApi
- .execute-api.
- Ref: AWS::Region
- .amazonaws.com/
- Ref: ApiStage
Condition: NotCreateServer
TransferIdentityProviderInvocationRole:
Description: IAM Role to pass to AWS Transfer CreateServer call as part of optional IdentityProviderDetails
Value:
Fn::GetAtt: TransferIdentityProviderRole.Arn
Condition: NotCreateServer
Resources:
TransferServer:
Type: AWS::Transfer::Server
Condition: CreateServer
Properties:
Domain: EFS
EndpointType: VPC
# EndpointDetails:
# Fn::If:
# - TransferVPCEndpoint
# - SubnetIds:
# Fn::Split: [',', Ref: TransferSubnetIDs]
# VpcId:
# Ref: TransferVPCID
# - Ref: AWS::NoValue
EndpointDetails:
AddressAllocationIds:
- Ref: ElasticIP
SecurityGroupIds:
- Ref: SecurityGroupIds
SubnetIds:
Fn::Split: [',', Ref: TransferSubnetIDs]
VpcId:
Ref: TransferVPCID
IdentityProviderDetails:
InvocationRole:
Fn::GetAtt: TransferIdentityProviderRole.Arn
Url:
Fn::Join:
- ''
- - https://
- Ref: CustomIdentityProviderApi
- .execute-api.
- Ref: AWS::Region
- .amazonaws.com/
- Ref: ApiStage
IdentityProviderType: API_GATEWAY
LoggingRole:
Fn::GetAtt: TransferCWLoggingRole.Arn
TransferCWLoggingRole:
Description: IAM role used by Transfer to log API requests to CloudWatch
Type: AWS::IAM::Role
Condition: CreateServer
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- transfer.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSTransferLoggingAccess
CustomIdentityProviderApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: Transfer Family Secrets Manager Integration API
Description: API used for Transfer Family to access user information in Secrets Manager
FailOnWarnings: true
EndpointConfiguration:
Types:
- REGIONAL
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: LambdaSecretsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
Fn::Sub:
- arn:${AWS::Partition}:secretsmanager:${SecretsRegion}:${AWS::AccountId}:secret:s-*
- SecretsRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
ApiCloudWatchLogsRole:
Description: IAM role used by API Gateway to log API requests to CloudWatch
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: ApiGatewayLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:DescribeLogGroups
- logs:DescribeLogStreams
- logs:PutLogEvents
- logs:GetLogEvents
- logs:FilterLogEvents
Resource: "*"
ApiLoggingAccount:
Type: AWS::ApiGateway::Account
DependsOn:
- CustomIdentityProviderApi
Properties:
CloudWatchRoleArn:
Fn::GetAtt: ApiCloudWatchLogsRole.Arn
ApiStage:
Type: AWS::ApiGateway::Stage
Properties:
DeploymentId:
Ref: ApiDeployment202008
MethodSettings:
- DataTraceEnabled: false
HttpMethod: "*"
LoggingLevel: INFO
ResourcePath: "/*"
RestApiId:
Ref: CustomIdentityProviderApi
StageName: prod
ApiDeployment202008:
DependsOn:
- GetUserConfigRequest
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
TransferIdentityProviderRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: transfer.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: TransferCanInvokeThisApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${CustomIdentityProviderApi}/prod/GET/*
- PolicyName: TransferCanReadThisApi
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- apigateway:GET
Resource: "*"
GetUserConfigLambda:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/
Description: A function to lookup and return user data from AWS Secrets Manager.
Handler: index.lambda_handler
Role:
Fn::GetAtt: LambdaExecutionRole.Arn
Runtime: python3.7
Environment:
Variables:
SecretsManagerRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
GetUserConfigLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
FunctionName:
Fn::GetAtt: GetUserConfigLambda.Arn
Principal: apigateway.amazonaws.com
SourceArn:
Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${CustomIdentityProviderApi}/*
ServersResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Fn::GetAtt:
- CustomIdentityProviderApi
- RootResourceId
PathPart: servers
ServerIdResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: ServersResource
PathPart: "{serverId}"
UsersResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: ServerIdResource
PathPart: users
UserNameResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: UsersResource
PathPart: "{username}"
GetUserConfigResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ParentId:
Ref: UserNameResource
PathPart: config
GetUserConfigRequest:
Type: AWS::ApiGateway::Method
DependsOn: GetUserConfigResponseModel
Properties:
AuthorizationType: AWS_IAM
HttpMethod: GET
Integration:
Type: AWS
IntegrationHttpMethod: POST
Uri:
Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- ":apigateway:"
- Ref: AWS::Region
- ":lambda:path/2015-03-31/functions/"
- Fn::GetAtt:
- GetUserConfigLambda
- Arn
- "/invocations"
IntegrationResponses:
- StatusCode: 200
RequestTemplates:
application/json: |
{
"username": "$util.urlDecode($input.params('username'))",
"password": "$util.escapeJavaScript($input.params('Password')).replaceAll("\\'","'")",
"protocol": "$input.params('protocol')",
"serverId": "$input.params('serverId')",
"sourceIp": "$input.params('sourceIp')"
}
RequestParameters:
method.request.header.Password: false
ResourceId:
Ref: GetUserConfigResource
RestApiId:
Ref: CustomIdentityProviderApi
MethodResponses:
- StatusCode: 200
ResponseModels:
application/json: UserConfigResponseModel
GetUserConfigResponseModel:
Type: AWS::ApiGateway::Model
Properties:
RestApiId:
Ref: CustomIdentityProviderApi
ContentType: application/json
Description: API response for GetUserConfig
Name: UserConfigResponseModel
Schema:
"$schema": http://json-schema.org/draft-04/schema#
title: UserUserConfig
type: object
properties:
HomeDirectory:
type: string
Role:
type: string
Policy:
type: string
PublicKeys:
type: array
items:
type: string
1.2 Lambda(index.py)の修正
変更箇所
- check_ipaddressメソッド
- lookupの引数を
AcceptedIpNetwork
→AcceptedIpNetwork
に変更(72~73行目)
- lookupの引数を
- build_responseメソッド
- response_dataに
PosixProfile
を追加(151~154行目)
- response_dataに
修正前 index.py
index.py
import os
import json
import boto3
import base64
from ipaddress import ip_network, ip_address
from botocore.exceptions import ClientError
def lambda_handler(event, context):
# Get the required parameters
required_param_list = ["serverId", "username", "protocol", "sourceIp"]
for parameter in required_param_list:
if parameter not in event:
print("Incoming " + parameter + " missing - Unexpected")
return {}
input_serverId = event["serverId"]
input_username = event["username"]
input_protocol = event["protocol"]
input_sourceIp = event["sourceIp"]
input_password = event.get("password", "")
print("ServerId: {}, Username: {}, Protocol: {}, SourceIp: {}"
.format(input_serverId, input_username, input_protocol, input_sourceIp))
# Check for password and set authentication type appropriately. No password means SSH auth
print("Start User Authentication Flow")
if input_password != "":
print("Using PASSWORD authentication")
authentication_type = "PASSWORD"
else:
if input_protocol == 'FTP' or input_protocol == 'FTPS':
print("Empty password not allowed for FTP/S")
return {}
print("Using SSH authentication")
authentication_type = "SSH"
# Retrieve our user details from the secret. For all key-value pairs stored in SecretManager,
# checking the protocol-specified secret first, then use generic ones.
# e.g. If SFTPPassword and Password both exists, will be using SFTPPassword for authentication
secret = get_secret(input_serverId + "/" + input_username)
if secret is not None:
secret_dict = json.loads(secret)
# Run our password checks
user_authenticated = authenticate_user(authentication_type, secret_dict, input_password, input_protocol)
# Run sourceIp checks
ip_match = check_ipaddress(secret_dict, input_sourceIp, input_protocol)
if user_authenticated and ip_match:
print("User authenticated, calling build_response with: " + authentication_type)
return build_response(secret_dict, authentication_type, input_protocol)
else:
print("User failed authentication return empty response")
return {}
else:
# Otherwise something went wrong. Most likely the object name is not there
print("Secrets Manager exception thrown - Returning empty response")
# Return an empty data response meaning the user was not authenticated
return {}
def lookup(secret_dict, key, input_protocol):
if input_protocol + key in secret_dict:
print("Found protocol-specified {}".format(key))
return secret_dict[input_protocol + key]
else:
return secret_dict.get(key, None)
def check_ipaddress(secret_dict, input_sourceIp, input_protocol):
# accepted_ip_network = lookup(secret_dict, "AcceptedIpNetwork", input_protocol)
accepted_ip_network = lookup(secret_dict, "AcceptedIPNetwork", input_protocol)
if not accepted_ip_network:
# No IP provided so skip checks
print("No IP range provided - Skip IP check")
return True
net = ip_network(accepted_ip_network)
if ip_address(input_sourceIp) in net:
print("Source IP address match")
return True
else:
print("Source IP address not in range")
return False
def authenticate_user(auth_type, secret_dict, input_password, input_protocol):
# Function returns True if: auth_type is password and passwords match or auth_type is SSH. Otherwise returns False
if auth_type == "SSH":
# Place for additional checks in future
print("Skip password check as SSH login request")
return True
# auth_type could only be SSH or PASSWORD
else:
# Retrieve the password from the secret if exists
password = lookup(secret_dict, "Password", input_protocol)
if not password:
print("Unable to authenticate user - No field match in Secret for password")
return False
if input_password == password:
return True
else:
print("Unable to authenticate user - Incoming password does not match stored")
return False
# Build out our response data for an authenticated response
def build_response(secret_dict, auth_type, input_protocol):
response_data = {}
# Check for each key value pair. These are required so set to empty string if missing
role = lookup(secret_dict, "Role", input_protocol)
if role:
response_data["Role"] = role
else:
print("No field match for role - Set empty string in response")
response_data["Role"] = ""
# These are optional so ignore if not present
policy = lookup(secret_dict, "Policy", input_protocol)
if policy:
response_data["Policy"] = policy
# External Auth providers support chroot and virtual folder assignments so we'll check for that
home_directory_details = lookup(secret_dict, "HomeDirectoryDetails", input_protocol)
if home_directory_details:
print("HomeDirectoryDetails found - Applying setting for virtual folders - "
"Note: Cannot be used in conjunction with key: HomeDirectory")
response_data["HomeDirectoryDetails"] = home_directory_details
# If we have a virtual folder setup then we also need to set HomeDirectoryType to "Logical"
print("Setting HomeDirectoryType to LOGICAL")
response_data["HomeDirectoryType"] = "LOGICAL"
# Note that HomeDirectory and HomeDirectoryDetails / Logical mode
# can't be used together but we're not checking for this
home_directory = lookup(secret_dict, "HomeDirectory", input_protocol)
if home_directory:
print("HomeDirectory found - Note: Cannot be used in conjunction with key: HomeDirectoryDetails")
response_data["HomeDirectory"] = home_directory
if auth_type == "SSH":
public_key = lookup(secret_dict, "PublicKey", input_protocol)
if public_key:
response_data["PublicKeys"] = [public_key]
else:
# SSH Auth Flow - We don't have keys so we can't help
print("Unable to authenticate user - No public keys found")
return {}
PosixProfile = lookup(secret_dict, "PosixProfile", input_protocol)
if PosixProfile:
print("Uid found")
response_data["PosixProfile"] = [PosixProfile]
return response_data
def get_secret(id):
region = os.environ["SecretsManagerRegion"]
print("Secrets Manager Region: " + region)
print("Secret Name: " + id)
# Create a Secrets Manager client
client = boto3.session.Session().client(service_name="secretsmanager", region_name=region)
try:
resp = client.get_secret_value(SecretId=id)
# Decrypts secret using the associated KMS CMK.
# Depending on whether the secret is a string or binary, one of these fields will be populated.
if "SecretString" in resp:
print("Found Secret String")
return resp["SecretString"]
else:
print("Found Binary Secret")
return base64.b64decode(resp["SecretBinary"])
except ClientError as err:
print("Error Talking to SecretsManager: " + err.response["Error"]["Code"] + ", Message: " +
err.response["Error"]["Message"])
return None
修正後 index.py
index.py
import os
import json
import boto3
import base64
from ipaddress import ip_network, ip_address
from botocore.exceptions import ClientError
def lambda_handler(event, context):
# Get the required parameters
required_param_list = ["serverId", "username", "protocol", "sourceIp"]
for parameter in required_param_list:
if parameter not in event:
print("Incoming " + parameter + " missing - Unexpected")
return {}
input_serverId = event["serverId"]
input_username = event["username"]
input_protocol = event["protocol"]
input_sourceIp = event["sourceIp"]
input_password = event.get("password", "")
print("ServerId: {}, Username: {}, Protocol: {}, SourceIp: {}"
.format(input_serverId, input_username, input_protocol, input_sourceIp))
# Check for password and set authentication type appropriately. No password means SSH auth
print("Start User Authentication Flow")
if input_password != "":
print("Using PASSWORD authentication")
authentication_type = "PASSWORD"
else:
if input_protocol == 'FTP' or input_protocol == 'FTPS':
print("Empty password not allowed for FTP/S")
return {}
print("Using SSH authentication")
authentication_type = "SSH"
# Retrieve our user details from the secret. For all key-value pairs stored in SecretManager,
# checking the protocol-specified secret first, then use generic ones.
# e.g. If SFTPPassword and Password both exists, will be using SFTPPassword for authentication
secret = get_secret(input_serverId + "/" + input_username)
if secret is not None:
secret_dict = json.loads(secret)
# Run our password checks
user_authenticated = authenticate_user(authentication_type, secret_dict, input_password, input_protocol)
# Run sourceIp checks
ip_match = check_ipaddress(secret_dict, input_sourceIp, input_protocol)
if user_authenticated and ip_match:
print("User authenticated, calling build_response with: " + authentication_type)
return build_response(secret_dict, authentication_type, input_protocol)
else:
print("User failed authentication return empty response")
return {}
else:
# Otherwise something went wrong. Most likely the object name is not there
print("Secrets Manager exception thrown - Returning empty response")
# Return an empty data response meaning the user was not authenticated
return {}
def lookup(secret_dict, key, input_protocol):
if input_protocol + key in secret_dict:
print("Found protocol-specified {}".format(key))
return secret_dict[input_protocol + key]
else:
return secret_dict.get(key, None)
def check_ipaddress(secret_dict, input_sourceIp, input_protocol):
# accepted_ip_network = lookup(secret_dict, "AcceptedIpNetwork", input_protocol)
accepted_ip_network = lookup(secret_dict, "AcceptedIPNetwork", input_protocol)
if not accepted_ip_network:
# No IP provided so skip checks
print("No IP range provided - Skip IP check")
return True
net = ip_network(accepted_ip_network)
if ip_address(input_sourceIp) in net:
print("Source IP address match")
return True
else:
print("Source IP address not in range")
return False
def authenticate_user(auth_type, secret_dict, input_password, input_protocol):
# Function returns True if: auth_type is password and passwords match or auth_type is SSH. Otherwise returns False
if auth_type == "SSH":
# Place for additional checks in future
print("Skip password check as SSH login request")
return True
# auth_type could only be SSH or PASSWORD
else:
# Retrieve the password from the secret if exists
password = lookup(secret_dict, "Password", input_protocol)
if not password:
print("Unable to authenticate user - No field match in Secret for password")
return False
if input_password == password:
return True
else:
print("Unable to authenticate user - Incoming password does not match stored")
return False
# Build out our response data for an authenticated response
def build_response(secret_dict, auth_type, input_protocol):
response_data = {}
# Check for each key value pair. These are required so set to empty string if missing
role = lookup(secret_dict, "Role", input_protocol)
if role:
response_data["Role"] = role
else:
print("No field match for role - Set empty string in response")
response_data["Role"] = ""
# These are optional so ignore if not present
policy = lookup(secret_dict, "Policy", input_protocol)
if policy:
response_data["Policy"] = policy
# External Auth providers support chroot and virtual folder assignments so we'll check for that
home_directory_details = lookup(secret_dict, "HomeDirectoryDetails", input_protocol)
if home_directory_details:
print("HomeDirectoryDetails found - Applying setting for virtual folders - "
"Note: Cannot be used in conjunction with key: HomeDirectory")
response_data["HomeDirectoryDetails"] = home_directory_details
# If we have a virtual folder setup then we also need to set HomeDirectoryType to "Logical"
print("Setting HomeDirectoryType to LOGICAL")
response_data["HomeDirectoryType"] = "LOGICAL"
# Note that HomeDirectory and HomeDirectoryDetails / Logical mode
# can't be used together but we're not checking for this
home_directory = lookup(secret_dict, "HomeDirectory", input_protocol)
if home_directory:
print("HomeDirectory found - Note: Cannot be used in conjunction with key: HomeDirectoryDetails")
response_data["HomeDirectory"] = home_directory
if auth_type == "SSH":
public_key = lookup(secret_dict, "PublicKey", input_protocol)
if public_key:
response_data["PublicKeys"] = [public_key]
else:
# SSH Auth Flow - We don't have keys so we can't help
print("Unable to authenticate user - No public keys found")
return {}
PosixProfile = lookup(secret_dict, "PosixProfile", input_protocol)
if PosixProfile:
print("Uid found")
response_data["PosixProfile"] = [PosixProfile]
return response_data
def get_secret(id):
region = os.environ["SecretsManagerRegion"]
print("Secrets Manager Region: " + region)
print("Secret Name: " + id)
# Create a Secrets Manager client
client = boto3.session.Session().client(service_name="secretsmanager", region_name=region)
try:
resp = client.get_secret_value(SecretId=id)
# Decrypts secret using the associated KMS CMK.
# Depending on whether the secret is a string or binary, one of these fields will be populated.
if "SecretString" in resp:
print("Found Secret String")
return resp["SecretString"]
else:
print("Found Binary Secret")
return base64.b64decode(resp["SecretBinary"])
except ClientError as err:
print("Error Talking to SecretsManager: " + err.response["Error"]["Code"] + ", Message: " +
err.response["Error"]["Message"])
return None
2. SAMの実行
いよいよ修正したテンプレートを実行します。
「SAM実行したことないから、よくわからない...」という方はこちらのブログが参考になります。(私もはじめてSAMを使いました。)
今回はAWS SAMをインストールするのがめんどうだったので、CloudShellから実行します。
※自端末にAWS SAMがインストールされている方は[2.1. SAMの実行環境作成]はとばして下さい。
2.1. SAMの実行環境作成
CloudShell起動後、sam init
コマンドを実行し、SAMの実行環境を作成します。
以下の内容が対話方式で聞かれるため順に答えていきます。
Which template source would you like to use?
1 - AWS Quick Start Templates
2 - Custom Template Location
1 - AWS Quick Start Templates
を選択。
Choose an AWS Quick Start application template
1 - Hello World Example
2 - Multi-step workflow
3 - Serverless API
4 - Scheduled task
5 - Standalone function
6 - Data processing
7 - Infrastructure event management
8 - Lambda EFS example
9 - Machine Learning
1 - Hello World Example
を選択。
Use the most popular runtime and package type? (Python and zip) [y/N]:
y
を選択。
Would you like to enable X-Ray tracing on the function(s) in your application? [y/N]:
N
を選択。
Project name [sam-app]:
そのままEnterを押下。ここまででSAMの実行環境が作成されます。
次に修正した以下のファイルをCloudShellにアップロードします。
- template.yml
- index.py
アップロードしたファイルを下記の場所に移動します。
sam-app
∟ template.yml
∟ src
∟ index.py
2.2. SAMの実行
sam app
配下でsam deploy -g
コマンドを実行します。
デプロイ中にSAM CLI によるプロンプトを用いたガイドを有効になるため、ガイドに従ってオプションを入力します。
Stack Name []: test-transferFamily-CloudFormation(任意のスタック名)
AWS Region []: ap-northeast-1
Parameter CreateServer [true]: そのままEnterを押下
Parameter SecretsManagerRegion []: そのままEnterを押下
Parameter TransferDomain [EFS]: そのままEnterを押下
Parameter TransferSubnetIDs []: subnet-xxxxxxxx(作成先のSubnetID)
Parameter SecurityGroupIds []: sg-xxxxxxxx(設定するSecurityGroupId)
Parameter EIP []: eipalloc-xxxxxxxxxx(設定するEIPの割り当てID)
Parameter TransferVPCID []: vpc-xxxxxxxx(作成先のVPCID)
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [Y/n]: Y
#SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails Disable rollback [Y/n]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]: そのままEnterを押下
SAM configuration environment [default]: そのままEnterを押下
デプロイするリソースの一覧が表示される
Deploy this changeset? [y/N]: y
ここでCloudFormationが実行されます。CloudShellにSuccessfully created/updated stack - in ap-northeast-1
が表示されることを確認します。
3. SecretsManagerでシークレットを作成
最後に接続するためのユーザーを作成します。
SecretsManagerサービスから、[新しいシークレットを保存する]を選択します。
設定ステップ 1 : シークレットのタイプを選択
以下を選択し、[次]を押下します。
- シークレットのタイプ:
その他のシークレットタイプ
- キー/値のペア: プレーンテキスト(値は下記参照)
- "Password":
"xxxxxxx"
(任意のパスワード) - "PublicKey":
"ssh-rsa xxxxxxx"
(tarnsfer_key.pub(公開鍵)の中身) - "Role":
"arn:aws:iam::xxxxxxxxxxx:role/test-transferfamily-role"
(作成したIAMロールARN) - "HomeDirectory":
"/fs-xxxxxxxxxxxxx"
("/" + efsのファイルID) - "SFTPAcceptedIPNetwork":
"0.0.0.0/0"
(今回はセキュリティグループでIPを制限しているため、0.0.0.0/0
にしています。Transfer Familyのエンドポイントタイプをパブリックエンドポイントにした場合は、ここでIP範囲を制限することができます。) - "PosixProfile":
- "Uid":
"0"
(0 に設定すると、ルートアクセスになります) - "Gid":
"0"
(0 に設定すると、ルートアクセスになります)
- "Uid":
- "Password":
プレーンテキストの設定値例
{
"Password": "xxxxxxx",
"PublicKey": "ssh-rsa xxxxxxx",
"Role": "arn:aws:iam::xxxxxxxxxxx:role/test-transferfamily-role",
"HomeDirectory": "/fs-xxxxxxxxxxxxx",
"SFTPAcceptedIPNetwork": "0.0.0.0",
"PosixProfile": {
"Uid": "0",
"Gid": "0"
}
}
- 暗号化キー:
aws/secretsmanager
設定ステップ 2 : シークレットを設定
以下を選択し、[次]を押下します。
- シークレットの名前:
s-xxxxxxxxx/test-transferfamily-user
(TransferFamilyのサーバーID/接続するユーザ名)
設定ステップ 3: ローテーションを設定 - オプション
ここではシークレットのローテーションを設定できます。
今回は設定しないためそのまま[次]を押下します。
設定ステップ 4: レビュー
設定した値に誤りがないか確認後、[保存]を押下します。
シークレットが作成されたことを確認します。
接続してみた
最後に接続できるか確認してみます。
接続前に必要なTransferFamilyのエンドポイント名を控えておきます。
今回はSFTPクライアントソフトとしてCyberduckを使用して接続してみます。
最初は鍵認証で接続します。
- プロトコル:
SFTP
- サーバ:
s-xxxxxxx.amazonaws.com
(TransferFamilyのエンドポイント) - ポート:
22
- ユーザー名:
test-transferfamily-user
(SecretManagerで設定したユーザー名) - SSH Private Key:
test-transferfamily-key
(作成した秘密鍵)
はじめて接続する場合はこのような画面が表示されます。[許可]を選択します。
問題なく接続できました!
次にパスワード認証で接続します。
- プロトコル:
SFTP
- サーバ:
s-xxxxxxx.amazonaws.com
(TransferFamilyのエンドポイント) - ポート:
22
- ユーザー名:
test-transferfamily-user
(SecretManagerで設定したユーザー名) - パスワード:
xxxxxxxxxxx
(SecretManagerで設定したパスワード)
こちらも問題なく接続できました!
ファイルのアップロードもダウンロードも問題なくできました。
おまけ:特定IPのみ接続を許可するように変更する
今回構築に使用したLambdaはIP範囲での制限をかけることができるようになってます。
特定のIPのみ接続を許可するように設定を変更する場合はLambdaとSecretManagerを以下のように修正します。
TransferFamilyをパブリックエンドポイントで構築した場合は、セキュリティグループは使用できないため、こちらを使用してみましょう。
Lambda(index.py)の修正
変更箇所
- check_ipaddressメソッド
- 配列からIPリストを取得するように変更(71~85行目をコメントアウト。88行目にコードを追記)
修正後 index.py
import os
import json
import boto3
import base64
from ipaddress import ip_network, ip_address
from botocore.exceptions import ClientError
def lambda_handler(event, context):
# Get the required parameters
required_param_list = ["serverId", "username", "protocol", "sourceIp"]
for parameter in required_param_list:
if parameter not in event:
print("Incoming " + parameter + " missing - Unexpected")
return {}
input_serverId = event["serverId"]
input_username = event["username"]
input_protocol = event["protocol"]
input_sourceIp = event["sourceIp"]
input_password = event.get("password", "")
print("ServerId: {}, Username: {}, Protocol: {}, SourceIp: {}"
.format(input_serverId, input_username, input_protocol, input_sourceIp))
# Check for password and set authentication type appropriately. No password means SSH auth
print("Start User Authentication Flow")
if input_password != "":
print("Using PASSWORD authentication")
authentication_type = "PASSWORD"
else:
if input_protocol == 'FTP' or input_protocol == 'FTPS':
print("Empty password not allowed for FTP/S")
return {}
print("Using SSH authentication")
authentication_type = "SSH"
# Retrieve our user details from the secret. For all key-value pairs stored in SecretManager,
# checking the protocol-specified secret first, then use generic ones.
# e.g. If SFTPPassword and Password both exists, will be using SFTPPassword for authentication
secret = get_secret(input_serverId + "/" + input_username)
if secret is not None:
secret_dict = json.loads(secret)
# Run our password checks
user_authenticated = authenticate_user(authentication_type, secret_dict, input_password, input_protocol)
# Run sourceIp checks
ip_match = check_ipaddress(secret_dict, input_sourceIp, input_protocol)
if user_authenticated and ip_match:
print("User authenticated, calling build_response with: " + authentication_type)
return build_response(secret_dict, authentication_type, input_protocol)
else:
print("User failed authentication return empty response")
return {}
else:
# Otherwise something went wrong. Most likely the object name is not there
print("Secrets Manager exception thrown - Returning empty response")
# Return an empty data response meaning the user was not authenticated
return {}
def lookup(secret_dict, key, input_protocol):
if input_protocol + key in secret_dict:
print("Found protocol-specified {}".format(key))
return secret_dict[input_protocol + key]
else:
return secret_dict.get(key, None)
# def check_ipaddress(secret_dict, input_sourceIp, input_protocol):
# # accepted_ip_network = lookup(secret_dict, "AcceptedIpNetwork", input_protocol)
# accepted_ip_network = lookup(secret_dict, "AcceptedIPNetwork", input_protocol)
# if not accepted_ip_network:
# # No IP provided so skip checks
# print("No IP range provided - Skip IP check")
# return True
# net = ip_network(accepted_ip_network)
# if ip_address(input_sourceIp) in net:
# print("Source IP address match")
# return True
# else:
# print("Source IP address not in range")
# return False
def check_ipaddress(secret_dict, input_sourceIp, input_protocol):
# accepted_ip_network = lookup(secret_dict, "AcceptedIpNetwork", input_protocol)
accepted_ip_network = lookup(secret_dict, "AcceptedIPNetwork", input_protocol)
if not accepted_ip_network:
# No IP provided so skip checks
print("No IP range provided - Skip IP check")
return True
ALLOW_LIST = accepted_ip_network
print(ALLOW_LIST)
# net = ip_network(accepted_ip_network)
for wl in ALLOW_LIST:
ip_nw = wl
if input_sourceIp == ip_nw:
print(input_sourceIp)
print("Source IP address match")
return True
else:
print(input_sourceIp)
print("Source IP address not match")
return False
def authenticate_user(auth_type, secret_dict, input_password, input_protocol):
# Function returns True if: auth_type is password and passwords match or auth_type is SSH. Otherwise returns False
if auth_type == "SSH":
# Place for additional checks in future
print("Skip password check as SSH login request")
return True
# auth_type could only be SSH or PASSWORD
else:
# Retrieve the password from the secret if exists
password = lookup(secret_dict, "Password", input_protocol)
if not password:
print("Unable to authenticate user - No field match in Secret for password")
return False
if input_password == password:
return True
else:
print("Unable to authenticate user - Incoming password does not match stored")
return False
# Build out our response data for an authenticated response
def build_response(secret_dict, auth_type, input_protocol):
response_data = {}
# Check for each key value pair. These are required so set to empty string if missing
role = lookup(secret_dict, "Role", input_protocol)
if role:
response_data["Role"] = role
else:
print("No field match for role - Set empty string in response")
response_data["Role"] = ""
# These are optional so ignore if not present
policy = lookup(secret_dict, "Policy", input_protocol)
if policy:
response_data["Policy"] = policy
# External Auth providers support chroot and virtual folder assignments so we'll check for that
home_directory_details = lookup(secret_dict, "HomeDirectoryDetails", input_protocol)
if home_directory_details:
print("HomeDirectoryDetails found - Applying setting for virtual folders - "
"Note: Cannot be used in conjunction with key: HomeDirectory")
response_data["HomeDirectoryDetails"] = home_directory_details
# If we have a virtual folder setup then we also need to set HomeDirectoryType to "Logical"
print("Setting HomeDirectoryType to LOGICAL")
response_data["HomeDirectoryType"] = "LOGICAL"
# Note that HomeDirectory and HomeDirectoryDetails / Logical mode
# can't be used together but we're not checking for this
home_directory = lookup(secret_dict, "HomeDirectory", input_protocol)
if home_directory:
print("HomeDirectory found - Note: Cannot be used in conjunction with key: HomeDirectoryDetails")
response_data["HomeDirectory"] = home_directory
if auth_type == "SSH":
public_key = lookup(secret_dict, "PublicKey", input_protocol)
if public_key:
response_data["PublicKeys"] = [public_key]
else:
# SSH Auth Flow - We don't have keys so we can't help
print("Unable to authenticate user - No public keys found")
return {}
PosixProfile = lookup(secret_dict, "PosixProfile", input_protocol)
if PosixProfile:
print("Uid found")
response_data["PosixProfile"] = [PosixProfile]
return response_data
def get_secret(id):
region = os.environ["SecretsManagerRegion"]
print("Secrets Manager Region: " + region)
print("Secret Name: " + id)
# Create a Secrets Manager client
client = boto3.session.Session().client(service_name="secretsmanager", region_name=region)
try:
resp = client.get_secret_value(SecretId=id)
# Decrypts secret using the associated KMS CMK.
# Depending on whether the secret is a string or binary, one of these fields will be populated.
if "SecretString" in resp:
print("Found Secret String")
return resp["SecretString"]
else:
print("Found Binary Secret")
return base64.b64decode(resp["SecretBinary"])
except ClientError as err:
print("Error Talking to SecretsManager: " + err.response["Error"]["Code"] + ", Message: " +
err.response["Error"]["Message"])
return None
SecretManagerのシークレットの値
変更箇所
- "SFTPAcceptedIPNetwork"
- 配列をIPアドレスで記載する(※/32といったCidrの記載は不要)
プレーンテキストの設定値
{
"Password": "xxxxxxx",
"PublicKey": "ssh-rsa xxxxxxx",
"Role": "arn:aws:iam::xxxxxxxxxxx:role/test-transferfamily-role",
"HomeDirectory": "/fs-xxxxxxxxxxxxx",
"SFTPAcceptedIPNetwork": [
"特定のIPアドレス",
"特定のIPアドレス"
],
"PosixProfile": {
"Uid": "0",
"Gid": "0"
}
}
リソースの削除
検証後は作成したリソースを削除しましょう。
特にTransfer Familyはサーバ停止しても、料金が発生してしまいます。
こちらの順番で削除すると、削除時にエラーが発生せず、スムーズに削除できます。
- test-transferFamily-CloudFormationの削除(CloudShellで
sam delete
を実行します) - EFSの削除
- EIPの解放
- セキュリティグループの削除
- VPCの削除
- IAMロールの削除
最後に
今回は鍵・パスワード認証によるTransfer Family を使用してSFTP 経由でファイルを EFS に直接アップロードする方法をご紹介しました。
EC2にマウントしていないEFSに直接ファイルをアップロードする方法の記事があまりなく、苦戦しながら構築したため、今回記事にしました。
どなたかのお役に立てれば幸いです。 最後までお読みいただきありがとうございました!
以上、おつまみ(@AWS11077)でした!
関連記事
[アップデート] AWS Transfer Family の対象ストレージに Amazon EFS が追加サポートされました | DevelopersIO
【2022年10月版】AWS Transfer Family の SFTP を使ってみた | DevelopersIO
AWS Transfer Family のSFTPをパスワード方式で接続してみた(IP制限有り) | DevelopersIO