[アップデート] AWS Transfer Family の認証要素にソース IP が利用できるようになったので、ソース IP に応じた IAM ロールの切り替えをやってみた

認証要素としてソースIPが指定できて何が嬉しいんだろう?と思ったけど、なかなか使い道がありそうなアップデートでした。
2020.06.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

先日のアップデートで AWS Transfer Family のカスタム ID プロバイダー利用時に認証要素としてソース IP が利用できるようになりました。

何がうれしいのか?

セキュリティグループでえぇんちゃうの?

このアップデートを最初に見たときの感想です。

AWS Transfer for SFTP がリリースされた当初はソース IP による制限が欲しい!という声をたくさん聞きましたが VPC エンドポイントに対応、EIP に対応などのアップデートにより送信元の制限はすでに実装する方法があります。

そんな中でこのアップデートによる恩恵をあれかな?これなか?と考えてみました。

パブリックエンドポイントでも IP 制限が出来る

SFTP プロトコルを利用の場合、エンドポイントタイプは PublicVPC を選択できます。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 ロールとしています。

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 を削除したのみです)

IAMポリシー

{
  "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 クエリ文字列パラメータ] を展開すると、認証時に求める追加要素として protocolsourceIp が準備されていることが確認できます。今回は 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 IPGetObject 権限を与えたい特定 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)でした!