JavaScriptを有効にしてください

AWS Transfer FamilyのカスタムIDプロバイダーでAzure ADと連携する

 ·  ☕ 7 分で読めます

SheepMedical では主要な事業としてデンタル事業があり、歯科医療領域における DX を推進しています。

そのため、社内の歯科技工士の方も日々、様々なツールを利用しています。今回そのツールの一つで、FTP 連携が必要になり、相談を受けました。

AWS Trasfer Family

AWS では FTP のマネージドのサービスとして AWS Transfer Family があり、 運用負荷低く、FTP を運用することができます。

AWS Transfer Family では FTP/SFTP/FTPS をサポートしており、またストレージとして S3、EFS と連携することができます。

懸念となったのは FTP ユーザのアカウント管理になります。将来的に社内の多数の歯科技工士の方が利用する想定のため、手動で管理するのは避けたい状況でした。また使用するツールの仕様上、公開鍵認証はサポートしておらず、ユーザ名とパスワードでの認証のみ利用することができます。

AWS Transfer Family では認証方法としてディレクトリサービスをサポートしており、社内では Azure AD を導入していることもあり、当初はこの認証方法での実装を検討していました。

とはいえ、別途 AD を運用するのも大変そうだなと思っていたところ AWS のソリューションアーキテクトの方に相談する機会を頂けたので、そこで相談したところ、カスタム ID プロバイダーを利用して Azure AD の API と連携する方法をご提案いただきました。

カスタム ID プロバイダーを利用することで、下記のメリットがあります。

  • 別途 AWS 内に AD を運用する必要がなく、運用負荷が低い。
  • AWS Directory Service を利用する必要がないので、コスト面でも有利

カスタム ID プロバイダー

カスタム ID プロバイダーを用いて、認証時に Lambda や API Gateway と連携することができます。

下記の AWS Blog では SAM を使って、Secrets Manager と連携する方法が紹介されています。

今回は、認証時に Lambda と連携し、Azure AD に認証情報を問い合わせることにしました。

flow

Azure AD 側の設定

Azure AD ではアプリケーションを作成することで外部サービスから認証を連携することができます。

作成できるアプリケーションは SPA や Web アプリケーション、デスクトップアプリケーションと様々なタイプをサポートしています。詳細については、ドキュメントを参照してください。

Lambda から認証情報を問い合わせるために、アプリケーションを作成します。今回は FTP/SFTP がインターフェースとなるため、「Web API を呼び出すデスクトップ アプリ: ユーザー名とパスワードでトークンを取得する」を利用します。1

設定は下記の流れになります。

  • Azure Active Directory からアプリの登録、新規登録で新規アプリを作成します。

azure_ad_1

  • アプリ名、サポートするアカウントの種類を選択します。リダイレクトする URI は存在しないため、省略します。

azure_ad_2

  • 認証からパブリッククライアントフローを許可します。

azure_ad_3

  • API のアクセス許可から、User.Readの権限を付与します。追加後、規定のディレクトリに管理者の同意を与えますをクリックします。

azure_ad_4

以上で作成したアプリケーションを通して、ユーザ名とパスワードのみでアクセスできるようになります。2

アプリケーションを利用するための情報は概要から確認することができます。今回必要となるのは、アプリケーション(クライアントID)ディレクトリ(テナントID) のみです。

azure_ad_5

Lambda の作成

今回は runtime として node.js を利用します。

Azure AD の API にアクセスするためのライブラリとして、公式のライブラリが存在しているので、こちらを利用します。

ただしエラー時の情報については、不足している印象を受けました。当初、Azure AD 側の設定が不足しており、認証でエラーが出ていたのですが、エラー情報からでは原因がつかめず、仕方なく.NET で同様の検証コードを作成して原因調査をしました。 3

Azure AD に認証を問い合わせる箇所を含めた最低限のコードは下記のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const msal = require("@azure/msal-node");
const log4js = require("log4js");

// 環境変数から必要な情報を取得
const CLIENT_ID = process.env.CLIENT_ID;
const TENANT_ID = process.env.TENANT_ID;
const S3_BUCKET = process.env.S3_BUCKET;
const ROLE = process.env.ROLE;

// MSALライブラリ設定
const msalConfig = {
  auth: {
    clientId: CLIENT_ID,
    authority: `https://login.microsoftonline.com/${TENANT_ID}`,
  },
};

const pca = new msal.PublicClientApplication(msalConfig);

// Logger設定
log4js.configure({
  appenders: { stdout: { type: "console", layout: { type: "basic" } } },
  categories: { default: { appenders: ["stdout"], level: "all" } },
});
const logger = log4js.getLogger();
logger.level = process.env.LOG_LEVEL || "info";

// Lambdaエントリポイント
exports.handler = async (event, context) => {
  const required_param_list = ["username", "password"];

  required_param_list.forEach((element) => {
    if (!(element in event)) {
      logger.error(`Missing required parameter: ${element}`);
      return {};
    }
  });

  const inputUsername = event.username;
  const inputPassword = event.password;

  const result = await authAd(inputUsername, inputPassword);
  if (result) {
    const response = {
      HomeDirectory: `/${S3_BUCKET}`,
      Role: ROLE,
    };
    return response;
  } else {
    logger.error(`Authentication failed : ${inputUsername}`);
    return {};
  }
};

// AzureAD認証
const authAd = async (userId, pass) => {
  const usernamePasswordRequest = {
    scopes: ["user.read"],
    username: userId,
    password: pass,
  };

  const result = await pca
    .acquireTokenByUsernamePassword(usernamePasswordRequest)
    .then((response) => {
      return true;
    })
    .catch((error) => {
      logger.error(error);
      return false;
    });
  return result;
};

認証自体は下記の箇所で、ユーザ名とパスワードで token が取得できれば、認証が成功したとみなしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// AzureAD認証
const authAd = async (userId, pass) => {
  const usernamePasswordRequest = {
    scopes: ["user.read"],
    username: userId,
    password: pass,
  };

  const result = await pca
    .acquireTokenByUsernamePassword(usernamePasswordRequest)
    .then((response) => {
      return true;
    })
    .catch((error) => {
      logger.error(error);
      return false;
    });
  return result;
};

AWS Transfer Family からは最低限必要な情報として、usernamepasswordを取得します。その他 SourceIP 等も取得することができます。

1
2
3
4
5
6
7
8
9
exports.handler = async (event, context) => {
  const required_param_list = ["username", "password"];

  required_param_list.forEach((element) => {
    if (!(element in event)) {
      logger.error(`Missing required parameter: ${element}`);
      return {};
    }
  });

AWS Transfer Family へのレスポンスとして最低限、HomeDirectoryと HomeDirectory に設定した S3 Bucket にアクセスする権限がある IAM Role arn をROLEとして返す必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const result = await authAd(inputUsername, inputPassword);
if (result) {
  const response = {
    HomeDirectory: `/${S3_BUCKET}`,
    Role: ROLE,
  };
  return response;
} else {
  logger.error(`Authentication failed : ${inputUsername}`);
  return {};
}

AWS Transfer Family 作成後の追加の作業として Lambda へのアクセス権限を付与する必要があります。4

permission

また合わせて上述した IAM Role も作成し、arn を環境変数に設定しておく必要があります。

  • 信頼関係
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "transfer.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
  • ポリシー
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::<bucket name>"
    },
    {
      "Sid": "",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::<bucket name>/*"
    }
  ]
}

AWS Transfer Family の作成

AWS Transfer Family の作成については、特に難しい箇所はなく、作成のウィザードに従えば問題ないかと思います。

ID プロバイダーの選択ではカスタムプロバイダーを選択肢し、作成した Lambda を指定しましょう。

trasfer

作成完了後、上述したとおり、Lambda の関数設定で、作成した AWS Transfer Family にアクセス権限を付与します。

全ての設定が完了したら、実際に FTP/SFTP アクセスして、接続できるか確認しましょう。もし問題が発生した場合は、CloudWatch Logs で AWS Transfer Family や Lambda のログ、Azure AD 上で接続ログを見て原因を調査する必要があります。

その他

上述したとおり、今回の方法では Azure AD 上で作成したアプリを通しての認証では MFA が求められません。
FTP の認証では Azure アカウントのユーザ名とパスワードのみを用いるため、悪意ある第三者にユーザ名とパスワードをブルートフォースアタックされる可能性があります。

そのため、FTP のエンドポイントをパブリックにする場合は、AzureAD 側でサインインの失敗について、適切な閾値を設定することや、API Gateway + WAF を挟む対応や、Lambda が実行されすぎていないか監視をする必要があります。

まとめ

AWS Transfer Family のカスタム ID プロバイダーを利用して Azure AD と簡易に連携する方法についてご紹介しました。

ディレクトリサービスプロバイダーを使う場合は、下記のように AWS 側と Azure 側で煩雑なネットワーク設定が必要になることもあり、簡易に Azure AD と連携する一つの方法として有用ではないかと思います。

改めまして、この方法をご紹介頂いた AWS の方々に御礼申し上げます。

最後に

SheepMedical では AWS の各種サービスを利用したデリバリー改善、サービスの安定性向上、DevOps/DevSecOps のライフサイクル改善、自動化等に興味がある方を絶賛募集しています。(時期によっては募集を停止していることがあります。)


  1. ドキュメントの通り、セキュアではないため、推奨されないフローですが、現状 Azure 側の認証画面を経由せず、ユーザ名とパスワードのみで認証できる方法が存在しない為、セキュリティ上のリスクを検討した上で採用しました。 ↩︎

  2. AD の設定で MFA を有効化している場合は、AD 側で条件で MFA を無効化するなので設定が必要なるケースがあります。 ↩︎

  3. .NET のライブラリの方では詳細なエラーが表示されます。 ↩︎

  4. 設定自体は該当の関数->設定->アクセス権限にて設定できます。 ↩︎

共有

Shinichi Morimoto
著者
Shinichi Morimoto
Software Developer