[アップデート] AWS Transfer Family の認証要素にソース IP が利用できるようになったので、ソース IP に応じた IAM ロールの切り替えをやってみた
先日のアップデートで AWS Transfer Family のカスタム ID プロバイダー利用時に認証要素としてソース IP が利用できるようになりました。
何がうれしいのか?
セキュリティグループでえぇんちゃうの?
このアップデートを最初に見たときの感想です。
AWS Transfer for SFTP がリリースされた当初はソース IP による制限が欲しい!という声をたくさん聞きましたが VPC エンドポイントに対応、EIP に対応などのアップデートにより送信元の制限はすでに実装する方法があります。
そんな中でこのアップデートによる恩恵をあれかな?これなか?と考えてみました。
パブリックエンドポイントでも IP 制限が出来る
SFTP プロトコルを利用の場合、エンドポイントタイプは Public
か VPC
を選択できます。VPC
タイプの場合は先述のとおりセキュリティグループを用いて IP 制限を実装することができますが、Public
タイプの場合はセキュリティグループを持たないため IP 制限することができませんでした。
S3 側のバケットポリシーで制限できるのでは?と思うかもしれませんが、Transfer Family のエンドポイントで NAT されるため、送信元をバケットポリシーで制限することはできませんでした。
今回のアップデートで Public
エンドポイントでも IP 制限できるようになったのは 1 つのメリットかと思います。
だから VPC タイプを使えばいいのでは?
VPC
タイプの場合、言葉のとおり必ず VPC が必要となります。Public
の場合、VPC がなくとも利用できるので単純に SFTP → S3 としてだけ利用したいという場合には Public
エンドポイントのほうがシンプルに構成できるメリットがあります。
送信元に応じた IAM ロールの切り替えが出来る
特に IAM ロールに限ったことではありませんが、結局、送信元をどうやって制限するかというとカスタム ID プロバイダーとして定義した先で動作する Lambda 関数です。
公式に提供されている CloudFormation のテンプレートからカスタム ID プロバイダをデプロイすると、デフォルトだと以下のような定義になっています。ご覧のとおり ソース IP を判定するようなロジックはありません。自分で書け、ということです。
'use strict'; // GetUserConfig Lambda exports.handler = (event, context, callback) => { console.log("Username:", event.username, "ServerId: ", event.serverId); var response; // Check if the username presented for authentication is correct. This doesn't check the value of the serverId, only that it is provided. if (event.serverId !== "" && event.username == 'myuser') { response = { Role: 'arn:aws:iam::<AWS_ACCOUNT_ID>:role/<S3_BUCKET_NAME>', // The user will be authenticated if and only if the Role field is not blank Policy: '', // Optional JSON blob to further restrict this user's permissions HomeDirectory: '/' // Not required, defaults to '/' }; // Check if password is provided if (event.password == "") { // If no password provided, return the user's SSH public key response['PublicKeys'] = [ "ssh-rsa AAAAB3NzaC1yc2EXXXXXXXXXXXX" ]; // Check if password is correct } else if (event.password !== 'MySuperSecretPassword') { // Return HTTP status 200 but with no role in the response to indicate authentication failure response = {}; } } else { // Return HTTP status 200 but with no role in the response to indicate authentication failure response = {}; } callback(null, response); };
今回のアップデートのポイントは「IP 制限をサポートした」のではなく、「認証の要素としてソース IP が利用可能になった」ということです。event.souceIP
を条件式に使って ソース IP に応じて遮断すれば IP 制限になりますし、特定のソース IP 以外は IAM ロールを切り替えて権限を弱めるということも出来るでしょう。その他もろもろ Lambda 関数のなかで自由にやればいい、ということですね。
やってみる
それではさっそく試してみましょう。今回は以下のような環境を作りたいと思います。
- プロトコルは SFTP
Public
エンドポイントを使用- 認証はカスタム ID プロバイダーを使用
- 特定 IP アドレス時のみ S3 のオブジェクトを GET できる
カスタム ID プロバイダーの作成
まず、こちらのスタックテンプレートを使って、カスタム ID プロバイダーを準備します。
CloudFormation のコンソールを開き [スタックの作成] をクリックします。上記のリンクよりダウンロードしたテンプレートファイルを指定して [次へ]
[CreateServer] は後から作成しますので false
を指定しています。今回は SFTP で試したいので [UserPublicKey1] に公開鍵を指定しています。[UserRoleArn] は以下のような IAM ポリシーをアタッチした IAM ロールを指定しています。これは特定 IP のみに割り当てる GetObject
権限のある IAM ロールとしています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": ["arn:aws:s3:::<bucketname>"] }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:DeleteObjectVersion", "s3:GetObjectVersion", "s3:GetObjectACL", "s3:PutObjectACL" ], "Resource": ["arn:aws:s3:::<bucketname>/*"] } ] }
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "transfer.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
そのほかはデフォルト設定のまま、デプロイを完了させます。
GetObject 権限のない IAM ロールの作成
次に特定 IP 以外の接続に利用される IAM ロールを作成します。信頼ポリシーは上述と同じなので割愛します。以下のような IAM ポリシーをアタッチしておきます。(今回は先述の IAM ポリシーから DeleteObject
, GetObject
を削除したのみです)
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetBucketLocation" ], "Resource": ["arn:aws:s3:::<bucketname>"] }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:DeleteObjectVersion", "s3:GetObjectVersion", "s3:GetObjectACL", "s3:PutObjectACL" ], "Resource": ["arn:aws:s3:::<bucketname>/*"] } ] }
Lambda 関数の編集
デフォルトの Lambda 関数は先述のとおりソース IP を判定するロジックはありませんので編集します。Lambda 関数の一覧より <STACK_NAME>-GetUserConfigLambda-<ABC123DEF456>
という形式の関数を開き、以下のように修正します。
この例では特定 IP アドレス xx.xx.xx.xx
の場合は GetObject
権限のある IAM ロール MyUserS3AccessRole
を使い、それ以外の場合は GetObject
権限のない MyUserS3AccessRole-else
を利用するようにしました。
'use strict'; // GetUserConfig Lambda exports.handler = (event, context, callback) => { console.log("Username:", event.username, "ServerId: ", event.serverId); var response; // Check if the username presented for authentication is correct. This doesn't check the value of the serverId, only that it is provided. if (event.serverId !== "" && event.username == 'myuser') { if (event.sourceIp == 'xx.xx.xx.xx') { response = { Role: 'arn:aws:iam::<AWS_ACCOUNT_ID>:role/MyUserS3AccessRole', // The user will be authenticated if and only if the Role field is not blank Policy: '', // Optional JSON blob to further restrict this user's permissions HomeDirectory: '/' // Not required, defaults to '/' }; } else { response = { Role: 'arn:aws:iam::<AWS_ACCOUNT_ID>:role/MyUserS3AccessRole-else', // The user will be authenticated if and only if the Role field is not blank Policy: '', // Optional JSON blob to further restrict this user's permissions HomeDirectory: '/' // Not required, defaults to '/' }; } // Check if password is provided if (event.password == "") { // If no password provided, return the user's SSH public key response['PublicKeys'] = [ "ssh-rsa <PUBLIC_KEY>" ]; // Check if password is correct } else if (event.password !== 'MySuperSecretPassword') { // Return HTTP status 200 but with no role in the response to indicate authentication failure response = {}; } } else { // Return HTTP status 200 but with no role in the response to indicate authentication failure response = {}; } callback(null, response); };
ソース IP 要素の有効化
デプロイが完了しましたら作成された API Gateway 管理コンソールから Transfer Custom Identity Provider basic template API
の /servers/{serverId}/users/{username}/config
の GET メソッド を開き、メソッドリクエストをクリックします。
[URL クエリ文字列パラメータ] を展開すると、認証時に求める追加要素として protocol
と sourceIp
が準備されていることが確認できます。今回は sourceIP
を必須として設定しました。
設定を変更しましたので、[アクション] - [API のデプロイ] を開き、[デプロイされるステージ] に prod
を指定して [デプロイ] をクリックします。
Transfer サーバーの設定
AWS Transfer Familyコンソールを開き、Create server
をクリックします。今回はパブリックから接続したいので SFTP
プロトコルを選択し [Next]
[Identity provider type] は Custom
を指定します。[Custom provider] には先ほどデプロイした API Gateway prod
ステージの呼び出し URL を指定、[Invocation role] は CloudFormation で作成された <STACK_NAME>-TransferIdentityProviderRole-<ABC123DEF456GHI>
という形式の IAM ロールがあるので、それを指定し [Next]
エンドポイントタイプは Publicly accessible
を指定し、[Next]
Configure additional details はデフォルトのまま [Next] でサーバを完了します。
接続テスト
作成した SFTP サーバーを選択し [Actions] - [Test] を開きます。Username
,Password
,Server protocol
, Source IP
を指定し、[Test] をクリックします。この場合の source IP
は GetObject
権限を与えたい特定 IP アドレスとしてください。
意図した IAM ロール名が返っていることを確認します。次に、Source IP
をその他の適当な IP に変更して再度、[Test] をクリックします。
先ほどと違う IAM ロール名が返ってきていれば成功です。
いざ接続
今回は Cyberduck を使って接続しました。設定は以下のようにしています。[サーバ] に SFTP エンドポイントの URL を指定し、ポート番号は 22
です。[ユーザ名] と [SSH Private Key] を指定。[パス] にはアクセスしたい S3 バケット名を指定しています。
特定 IP からの接続
まずは特定 IP からの接続を試してみると、以下のとおり正常に GetObject
できました。
その他の IP からの接続
次にプロキシをとおして特定 IP 以外の接続として試してみると、先ほど GetObject
できたものが出来なくなっていますね!
なお PutObject
権限は与えているため、アップロードは出来ました。
ソース IP に応じた IAM ロールの切り替えが出来ていることが確認できましたね!
検証は以上です。
さいごに
VPC を必要としない Public
エンドポイントタイプでも IP 制限できることは嬉しいですが、IP 制限したいだけなら VPC エンドポイントタイプを使うのがてっとり早いし、セキュリティグループで管理できるのでオススメだとは思います。
テレワークが普及する昨今ですので、ソース IP に応じてダウンロードは制限させたい、というようなリクエストにハマりそうだったので、IAM ロールの切り替えというユースケースを検証してみました。
以上!大阪オフィスの丸毛(@marumo1981)でした!