AWS Transfer for FTPをマルチアカウントで利用する
はじめに
みなさんは、AWS Transfer Family for FTP
を使用したことはありますか?
私は最近オンプレミス環境上のシステムとAWSに開発中のシステムでFTPの要件があり、作成する機会がありました。また、このAWS上のシステムが元々独立した(オンプレミスとnon ip-reachable
な)環境だったので、ip-reachable
なアカウントからマルチアカウントで利用できるように構成したので、備忘として残しておきます。
先に結論
まずは先に結論だけ書いておきます。ここで理解できた方は読み飛ばしてください!
- Transfer FamilyのエンドポイントはFTPサーバーの役割をする
- S3・EFSへファイル転送を行うにはIAMの権限が必要
- 通常通りマルチアカウントでS3アクセスするためにバケットポリシーに許可を設定する
以降で順にご説明していきます。最終的な構成はこちらです。
Transfer Familyとは
Transfer Familyとは、FTP
やSFTP
といったファイル転送とデータ管理をAWSがマネージドに提供しているサービスです。
FTPやSFTPを使用して、S3かEFSに対してファイルを転送するためのサーバーの役割をしてくれたり、SFTPコネクタを使用して外部へのファイル送信をすることも可能です。
また、最近はTransfer Family ウェブアプリケーション
という、WebUIを提供してファイル共有をするサービスも出てきています。
今回はFTPを使用してファイル転送する際にはまったポイントをお伝えできればと思います。
なお、FTPで使用できるのはパッシブモードのみで、ポート設定も若干特殊なのでご注意ください。
プロトコル制限はこちらの記事をご確認ください。
ファイル転送プロトコル (FTP) と ではFTPS、パッシブモードのみがサポートされています。
ポート制限はこちらをご確認ださい。
FTP Transfer Family の サーバーは、ポート 21 (コントロールチャネル) とポート範囲 8192~8200 (データチャネル) で動作します。
また、FTPのユーザー認証には、IDプロバイダーとしてディレクトリサービスかLambdaやAPI Gatewayを使うカスタムIDプロバイダーのどちらかを設定可能です。SFTPではマネージドIDとしてユーザー設定ができますが、FTPではできないので注意が必要です。
検証
初期構成
まずは同一アカウント内でTransfer for FTP
を利用できるようにしていきます。
擬似的にClient-VPCにオンプレミス環境のFTPクライアントを模したEC2サーバーを配置し、Transfer for FTPが閉域環境内に構築されている状態にします。また、この時Transfer for FTP以外のエンドポイントは現時点では作成しません。
Transfer for FTPはカスタムIDプロバイダーを使用して、ID/PW認証をします。
ベース環境構築
VPCの構築は構成図の通り行います。
Client-VPCとFTP-VPCはIPリーチャブルなCIDRで構築し、VPC Peeringで接続します。
FTP-VPCのCfnテンプレートはこちらです。
Client-VPCのCfnテンプレートはこちらです。ご参考まで。
Client-EC2に関しては事前準備として、Client-VPCにはInstance Connectエンドポイントを作成し、インスタンスにSSH接続できるようにしておきます。また、Amazon Linux2023にはFTPクライアントが入っていないので、S3のGatewayエンドポイント経由でdnfインストールできるようにしています。
ここは本題と異なるので省略しています。詳しくはこちらの記事をご覧ください。
(今回はコスト優先でこの構成にしましたが、手間に感じる方はパブリックサブネット+NAT Gatewayの構成で作成してください。)
カスタムIDプロバイダーの作成
IDプロバイダーを事前に作成しておく必要があるため、AWSが公式に出しているCloudFormation Templateを使用します。
テンプレート
---
AWSTemplateFormatVersion: '2010-09-09'
Description: A basic template that uses AWS Lambda with an AWS Transfer Family server
to integrate SecretsManager as an identity provider. It authenticates against an
entry in AWS Secrets Manager of the format SFTP/username. Additionally, the secret
must hold the key-value pairs for all user properties returned to AWS Transfer Family.
You can also modify the AWS Lambda function code to update user access.
Parameters:
CreateServer:
AllowedValues:
- 'true'
- 'false'
Type: String
Description: Whether this stack creates a server internally or not. If a server is created internally,
the customer 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 SecretsManager is not available.
Default: ''
Conditions:
CreateServer:
Fn::Equals:
- Ref: CreateServer
- 'true'
NotCreateServer:
Fn::Not:
- Condition: CreateServer
SecretsManagerRegionProvided:
Fn::Not:
- Fn::Equals:
- Ref: SecretsManagerRegion
- ''
Outputs:
ServerId:
Value:
Fn::GetAtt: TransferServer.ServerId
Condition: CreateServer
StackArn:
Value:
Ref: AWS::StackId
Resources:
TransferServer:
Type: AWS::Transfer::Server
Condition: CreateServer
Properties:
EndpointType: PUBLIC
IdentityProviderDetails:
Function:
Fn::GetAtt: GetUserConfigLambda.Arn
IdentityProviderType: AWS_LAMBDA
LoggingRole:
Fn::GetAtt: CloudWatchLoggingRole.Arn
CloudWatchLoggingRole:
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
Policies:
- PolicyName: TransferLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:DescribeLogStreams
- logs:PutLogEvents
Resource:
Fn::Sub: '*'
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:aws/transfer/*
- SecretsRegion:
Fn::If:
- SecretsManagerRegionProvided
- Ref: SecretsManagerRegion
- Ref: AWS::Region
GetUserConfigLambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile:
Fn::Sub: |
import os
import json
import boto3
import base64
from botocore.exceptions import ClientError
def lambda_handler(event, context):
resp_data = {}
if 'username' not in event or 'serverId' not in event:
print("Incoming username or serverId missing - Unexpected")
return response_data
# It is recommended to verify server ID against some value, this template does not verify server ID
input_username = event['username']
input_serverId = event['serverId']
print("Username: {}, ServerId: {}".format(input_username, input_serverId));
if 'password' in event:
input_password = event['password']
if input_password == '' and (event['protocol'] == 'FTP' or event['protocol'] == 'FTPS'):
print("Empty password not allowed")
return response_data
else:
print("No password, checking for SSH public key")
input_password = ''
# Lookup user's secret which can contain the password or SSH public keys
resp = get_secret("aws/transfer/" + input_serverId + "/" + input_username)
if resp != None:
resp_dict = json.loads(resp)
else:
print("Secrets Manager exception thrown")
return {}
if input_password != '':
if 'Password' in resp_dict:
resp_password = resp_dict['Password']
else:
print("Unable to authenticate user - No field match in Secret for password")
return {}
if resp_password != input_password:
print("Unable to authenticate user - Incoming password does not match stored")
return {}
else:
# SSH Public Key Auth Flow - The incoming password was empty so we are trying ssh auth and need to return the public key data if we have it
if 'PublicKey' in resp_dict:
resp_data['PublicKeys'] = resp_dict['PublicKey'].split(",")
else:
print("Unable to authenticate user - No public keys found")
return {}
# If we've got this far then we've either authenticated the user by password or we're using SSH public key auth and
# we've begun constructing the data response. Check for each key value pair.
# These are required so set to empty string if missing
if 'Role' in resp_dict:
resp_data['Role'] = resp_dict['Role']
else:
print("No field match for role - Set empty string in response")
resp_data['Role'] = ''
# These are optional so ignore if not present
if 'Policy' in resp_dict:
resp_data['Policy'] = resp_dict['Policy']
if 'HomeDirectoryDetails' in resp_dict:
print("HomeDirectoryDetails found - Applying setting for virtual folders")
resp_data['HomeDirectoryDetails'] = resp_dict['HomeDirectoryDetails']
resp_data['HomeDirectoryType'] = "LOGICAL"
elif 'HomeDirectory' in resp_dict:
print("HomeDirectory found - Cannot be used with HomeDirectoryDetails")
resp_data['HomeDirectory'] = resp_dict['HomeDirectory']
else:
print("HomeDirectory not found - Defaulting to /")
print("Completed Response Data: "+json.dumps(resp_data))
return resp_data
def get_secret(id):
region = os.environ['SecretsManagerRegion']
print("Secrets Manager Region: "+region)
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: ' + str(err))
return None
Description: A function to lookup and return user data from AWS Secrets Manager.
Handler: index.lambda_handler
Role:
Fn::GetAtt: LambdaExecutionRole.Arn
Runtime: python3.11
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: transfer.amazonaws.com
SourceArn:
Fn::If:
- CreateServer
- Fn::GetAtt: TransferServer.Arn
- Fn::Sub: arn:${AWS::Partition}:transfer:${AWS::Region}:${AWS::AccountId}:server/*
パラメータは以下で入力します。
- スタックの名前: 任意
- CreateServer: false
- SecretsManagerRegion: ap-northeast-1
この状態でスタックを作成すると、カスタムIDプロバイダー用のLambdaが作成されます。CreateServer
をTrueにするとパブリックエンドポイントのTransfer for FTPが作られてしまうので注意です。
S3とS3へのアクセス権を持つIAMロールの作成
S3を作成し、そのS3へのアクセス権を持つTransfer for FTP用のIAMロールを作成します。
手順は省きますが、以下のポリシーを参考にしてください。
IAMポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::your-bucket-name",
"arn:aws:s3:::your-bucket-name/*"
]
},
{
"Effect": "Deny",
"Action": [
"s3:DeleteBucket",
"s3:CreateBucket"
],
"Resource": [
"*"
]
}
]
}
信頼ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"transfer.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
SecretsManagerへのシークレット登録
LambdaのカスタムIDプロバイダーでは、SecretsManagerにアクセスし、そこに格納されているPWを入力値と比較して、問題なければIAMロールを返すという挙動をします。
そのため、SecretsManagerに適切なPrefixでシークレットを登録する必要があります。
詳細はこちらのホワイトペーパーをご参照ください。
シークレットの名前だけは注意が必要です。Lambda関数内でシークレットのキー文字列を指定しているのですが、get_secret("aws/transfer/" + input_serverId + "/" + input_username)
になっているので、この形式で指定してください。任意の形式にする場合はLambdaの該当箇所を変更してください。
Roleは必須です。ID/PWで認証したい場合は、Passwordというシークレットキーを作成して、適切なパスワードを設定してください。
今回はaws/transfer/s-xxxxxxxxx/user1
の名前で、以下のように登録しました。
Transfer for FTPの構築
今回はマネジメントコンソールから作成します。
FTPのみを有効にしたサーバーを作成します。
カスタムIDプロバイダーを選択し、上記で作成したLambda関数を選択します。
あとは、VPCでホスト
を選択して内部
アクセスで構築したいVPCを選択すればOKです。ドメインの選択はS3用のサーバーかEFS用のサーバーかを選択するので、今回はS3を選択していきます。
他はデフォルトのままで作成を進めます。
作成が完了すれば、赤枠内のエンドポイントからエンドポイントのDNS名を保存しておきます。
なお、エンドポイントにはデフォルトのSecurityGroupがアタッチされているので、要件に合わせて適宜変更してください。
ユーザー情報のテスト
FTPサーバーを作成すると、右上のアクションからユーザー認証のテストができます。
以下のとおり指定したユーザーで各種レスポンスが取得できていることが分かります。
パスワード間違えの時のレスポンス
{
"Response": "{}",
"StatusCode": 200,
"Message": "Lambda IDP authentication failure"
}
EC2からのFTP実行
Client-VPC内のEC2から先ほど構築したTransfer for FTPへアクセスしてみます。
その前に、FTPクライアントが入っていないので、インストールします。
yum install lftp
lftpで接続してみます。
lftp -u user1 vpce-xxxxx.vpce-svc-xxxx.ap-northeast-1.vpce.amazonaws.com
Password: <SecretsManagerに登録しているパスワード>
lftp user1@vpce-xxxxx.vpce-svc-xxxx.ap-northeast-1.vpce.amazonaws.com:~>
対話モードになってログインできていそうです。
ファイル転送をしてみます。一度exit
で対話モードを抜け、ファイルを作成して再度ログインして、put
コマンドを実行します。
touch test.txt
lftp -u user1 vpce-xxxxx.vpce-svc-xxxx.ap-northeast-1.vpce.amazonaws.com
Password: <SecretsManagerに登録しているパスワード>
lftp user1@vpce-xxxxx.vpce-svc-xxxx.ap-northeast-1.vpce.amazonaws.com:~> put test.txt
プロンプトが返ってきたら、ls
コマンドでファイル転送できていることを見てみます。
lftp user1@vpce-xxxxx.vpce-svc-xxxx.ap-northeast-1.vpce.amazonaws.com:~> ls
-rwxr--r-- 1 - - 0 Mar 15 00:47 test.txt
FTPが機能していることが分かりました。
マルチアカウント化対応
転送先のアカウントにS3バケットを作成します。この手順は省きますが、バケットARNは保存しておいてください。
SecretsManagerへのシークレット登録
先ほど作成したシークレットと同様、転送先アカウント向けのユーザー情報を登録します。
サーバーIDは同じでユーザー名を変更したシークレット名で、HomeDirectoryDetails
を指定している場合はバケット名を更新します。
IAMポリシーへの許可追加
先ほど作成したIAMポリシーにバケットARNを追記します。ここも手順は省きます。
ポリシーごと新規に作成してアタッチしても大丈夫です。
IAMロールIDを取得する
転送先のバケットポリシーに使用するため、IAMロールIDを取得します。これはマネジメントコンソールからは取得できないため、CLIを使用します。
CloudShellなどを使用すると楽に取得できます。
aws iam get-role --role-name "先ほど作成したIAMロール名"
IAMロールIDのレスポンス
{
"Role": {
"Path": "/",
"RoleName": "<先ほど作成したIAMロール名>",
"RoleId": "<IAMロールID>",
"Arn": "arn:aws:iam::<account id>:role/<先ほど作成したIAMロール名>",
"CreateDate": "2025-02-24T07:50:03+00:00",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "transfer.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Description": "Allow AWS Transfer to call AWS services on your behalf.",
"MaxSessionDuration": 3600,
"RoleLastUsed": {
"LastUsedDate": "2025-03-15T00:35:20+00:00",
"Region": "ap-northeast-1"
}
}
}
バケットポリシー追記(転送先アカウント作業)
転送先のS3のアクセス許可
タブからバケットポリシーを修正します。以下のポリシー例に必要な情報を追記してください。
バケットポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BucketPolicyForFTP",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<転送元アカウントID>:root"
},
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:DeleteObjectVersion",
"s3:DeleteObject",
"s3:GetObjectVersion",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::転送先バケット名",
"arn:aws:s3:::転送先バケット名/*"
],
"Condition": {
"StringLike": {
"aws:userId": "<IAMロールID>:*"
}
}
}
]
}
EC2からのFTP実行
先ほど同様対象のユーザー・PWでログインとファイル転送を実行すると、問題なく実行できると思います。
最後に
この機能を使用すると、各アカウントでFTPエンドポイントを作成しなくて良いので、コストの面でもメリットが出せると思います。
FTPだけでなく、SFTPでももちろんマルチアカウントでの構成ができるので、AWSを利用していて複数アカウントを管理している方は、できるだけ集約できる部分を整理するとコストや管理が簡単になるのでぜひ利用してみてください。
Discussion