次の方法で共有


Dynamic IP Restrictions を使用して FTP 認証プロバイダーを作成する

作成者: Robert McMurray

Microsoft は、Windows Server® 2008 用に完全に書き換えられた新しい FTP サービスを作成しました。 この新しい FTP サービスには、Web 作成者が以前よりも簡単にコンテンツを公開できる多くの新機能が組み込まれており、Web 管理者により多くのセキュリティとデプロイのオプションが提供されます。

新しい FTP 7.5 サービスでは、FTP サービスに含まれる組み込み機能を拡張できる拡張性がサポートされています。 具体的には、FTP 7.5 では、独自の認証プロバイダーの作成がサポートされます。 また、カスタム FTP ログのため、および FTP ユーザーのホーム ディレクトリ情報を決定するためのプロバイダーも作成できます。

このチュートリアルでは、SQL Server データベースを使用してアカウント情報を格納する動的 IP 制限のサポートを提供する FTP 認証プロバイダーにマネージド コードを使用する手順について説明します。 このプロバイダーは、リモート IP アドレスからのエラーの数をログに記録し、この情報を使用して、特定の期間内にサーバーへのログインに失敗した IP アドレスをブロックすることによって、このロジックを実装します。

重要

このチュートリアルでプロバイダーを使用するには、最新バージョンの FTP 7.5 サービスをインストールする "必要があります"。 FTP バージョン 7.5 は 2009 年 8 月 3 日にリリースされ、IFtpLogProvider.Log() メソッドのローカル IP アドレスとリモート IP アドレスが正しくない問題に対処しました。 このため、以前のバージョンの FTP サービスを使用すると、このプロバイダーが動作しません。

前提条件

この記事の手順を完了するには、次の項目が必要です。

  1. IIS 7.0 以降を Windows Server 2008 サーバーにインストールし、インターネット インフォメーション サービス (IIS) マネージャーもインストールする必要があります。

  2. 新しい FTP 7.5 サービスをインストールする必要があります。

    重要

    前述のとおり、このチュートリアルでプロバイダーを使用するには、最新バージョンの FTP 7.5 サービスをインストールする "必要があります"。 FTP バージョン 7.5 は 2009 年 8 月 3 日にリリースされ、IFtpLogProvider.Log() メソッドのローカル IP アドレスとリモート IP アドレスが正しくない問題に対処しました。 このため、以前のバージョンの FTP サービスを使用すると、このプロバイダーが動作しません。

  3. サイトに対して FTP 発行を有効にする必要があります。

  4. Visual Studio 2008 を使用する必要があります。

    Note

    以前のバージョンの Visual Studio を使用している場合、このチュートリアルの一部の手順が正しくない可能性があります。

  5. ユーザー アカウントのリストと関連する制限リストには、SQL Server データベースを使用する必要があります。この例は FTP 基本認証では使用できません。 このチュートリアルの「追加情報」セクションには、このサンプルに必要なテーブルを作成する SQL Server 用のスクリプトが用意されています。

  6. IIS コンピューターに Gacutil.exe が必要です。これは、アセンブリをグローバル アセンブリ キャッシュ (GAC) に追加するために必要です。

重要

認証要求のパフォーマンスを向上させるために、FTP サービスは、成功したログインの資格情報を既定で 15 分間キャッシュします。 この認証プロバイダーは、攻撃者からの要求をすぐに拒否しますが、最近ログインしたユーザーのパスワードを攻撃者が正しく推測できた場合は、キャッシュされた資格情報を介してアクセスできる可能性があります。 これにより、このプロバイダーが悪意のあるユーザーの IP アドレスをブロックした後に、悪意のあるユーザーがサーバーを攻撃することを意図せずに可能にしてしまう可能性があります。 この潜在的な攻撃手段を軽減するには、FTP サービスの資格情報キャッシュを無効にする必要があります。 そのためには、次の手順を行ってください。

  1. コマンド プロンプトを開きます。

  2. 次のコマンドを入力します。

    cd /d "%SystemRoot%\System32\Inetsrv"
    Appcmd.exe set config -section:system.ftpServer/caching /credentialsCache.enabled:"False" /commit:apphost
    Net stop FTPSVC
    Net start FTPSVC
    
  3. コマンド プロンプトを閉じます。

これらの変更を行うと、この例の認証プロバイダーは潜在的な攻撃者からの要求をすべてすぐに拒否できるようになります。

[プロバイダーの説明]

このチュートリアルには、議論が必要ないくつかのポイントがあります。 インターネットベースの攻撃は、多くの場合、システム上のアカウントのユーザー名とパスワードを取得しようとして FTP サーバーを悪用します。 この動作を検出するには、FTP アクティビティ ログを分析し、システムの攻撃に使用されている IP アドレスを調べて、それらのアドレスを以降のアクセスでブロックします。 残念ながら、これは手動プロセスであり、そのプロセスが自動化されている場合でも、リアルタイムではありません。

FTP サービスには、IP アドレスに基づいて接続を制限する機能が含まれていますが、IP アドレスの一覧は IIS 構成ファイルに格納され、更新するには管理アクセス権が必要です。 FTP サービスの拡張プロセスは、IIS 構成ファイルの必要な設定を更新するアクセス許可を持たない低特権アカウントとして実行されます。 ユーザー名のフラッディングを検出し、その情報をデータ ストアに書き込む FTP ログ プロバイダーと、IIS 構成ファイルを更新できる高い特権を持つアカウントとして実行される別のサービスを作成できますが、システム アーキテクチャに関するより高度な知識が必要であり、実装に関する多くの難しい詳細が必要です。 このため、代替データ ストアが必要になります。

データベースは、データ アクセスが容易で、データベース内のデータを操作するために使用できるツールが一般提供されているため、理想的な選択肢となります。 次の課題は、既存の FTP 拡張インターフェイスを使用して、攻撃者が使用するログイン フラッディングを検出するために必要なロジックを実装することです。 確認のために、使用可能な拡張インターフェイスを以下に示します。

セキュリティを大幅に強化するためにこれらのインターフェイスをすべて利用するプロバイダーを簡単に作成できますが、このチュートリアルのプロバイダーでは次のインターフェイスのみを使用します。

  • IFtpAuthenticationProvider - プロバイダーはこのインターフェイスを使用して、FTP サーバーへのアクセスを許可または拒否します。
  • IFtpLogProvider - プロバイダーはこのインターフェイスを汎用イベント リスナーとして使用します。

FTP サービスには、プロバイダーが登録できる実際のイベント通知はありませんが、IFtpLogProvider.Log() メソッドを使用してイベント後処理を提供するプロバイダーを記述できます。 たとえば、ログイン試行に失敗した場合、"PASS" コマンドが "230" 以外の状態コードでログに記録されます。これは、FTP ログインが成功した状態コードです。 ログインに失敗したクライアントの IP アドレスなど、失敗したログイン試行に関する追加情報をキャプチャすることで、この情報を使用して、IP アドレスが今後 FTP サーバーにアクセスするのをブロックするなどの追加機能を提供できるようになります。

プロバイダーのアーキテクチャとロジック

次の説明は、この認証プロバイダーの動作をまとめたものです。

  • システムにプロバイダーを登録するときに、IIS 構成ファイルに、使用するデータベース接続と失敗したログオン試行回数とフラッド タイムアウトの値を指定します。

  • FTP サービスによってプロバイダーが読み込まれると、IIS 構成ファイルの値がプロバイダーの Initialize() メソッドに渡されます。 これらの値がグローバル設定に格納されると、Initialize() メソッドは初期ガベージ コレクションを実行して、データベース内にある可能性のある以前の FTP セッションの情報をクリーンします。

  • FTP クライアントが FTP サーバーに接続すると、プロバイダーの Log() メソッドに FTP サービスによって "ControlChannelOpened" メッセージが送信されます。 Log() メソッドは、クライアントの IP アドレスがブロックされているかどうかを確認するためにデータベースをチェックします。ブロックされている場合は、データベース内のセッションにフラグを設定します。

  • ユーザーがユーザー名とパスワードを入力すると、FTP サービスはプロバイダーの AuthenticateUser() メソッドを呼び出し、セッションにフラグが設定されているかどうかがチェックされます。 セッションにフラグが設定されている場合、プロバイダーは false を返し、ユーザーがログインに失敗したことを示します。 セッションにフラグが設定されていない場合、ユーザー名とパスワードが有効かどうかがデータベースでチェックされます。 有効な場合、メソッドは true を返し、ユーザーが有効であり、ログインできることを示します。

  • ユーザーが有効なユーザー名とパスワードを入力できない場合、FTP サービスによって Log() メソッドが呼び出され、このメソッドは定期的なガベージ コレクションを実行して、失敗の数がフラッド タイムアウトよりも少ないことを確認します。 次に、このメソッドは、残っている失敗の回数が失敗の最大数より少ないかどうかを確認します。

    • 失敗の最大数に達していない場合、メソッドはクライアントの IP アドレスのエラー通知をデータベースに追加します。
    • 失敗の最大数に達した場合、このメソッドはクライアントの IP アドレスをデータベース内のブロックされた IP アドレスの一覧に追加します。
  • FTP クライアントがサーバーから切断されると、FTP サービスはプロバイダーの Log() メソッドを呼び出し、"ControlChannelClosed" メッセージを送信します。 Log() メソッドは、この通知を利用してセッションのガベージ コレクションを実行します。

追加メモ

  • このプロバイダーは、ユーザーと IP アドレスの検証の機能を公開しますが、ロール参照の実装は提供しません。 ただし、ユーザーからロールへのマッピング用のテーブルを追加し、IFtpRoleProvider.IsUserInRole() メソッドをプロバイダーに追加するのは比較的簡単ですが、これはこのチュートリアルの範囲外です。
  • このプロバイダーは、認証プロセス中に SQL データベース サーバーに対して少数の呼び出しを行います。 いくつかの SQL ステートメントを単一の複合クエリまたはストアド プロシージャに統合することで、データベースへのラウンド トリップ数をさらに減らすことができますが、これはこのチュートリアルの範囲外です。

手順 1: プロジェクト環境の設定

この手順では、Visual Studio 2008 でデモ プロバイダー用のプロジェクトを作成します。

  1. Microsoft Visual Studio 2008 を起動します。

  2. [ファイル] メニューの [新規作成][プロジェクト] の順に選択します。

  3. 新しいプロジェクト ダイアログ ボックスで以下を実行します。

    • プロジェクトの種類として [Visual C#] を選択します。
    • テンプレートとして [クラス ライブラリ] を選択します。
    • プロジェクトの名前として「FtpAddressRestrictionAuthentication」と入力します。
    • OK をクリックします。
  4. プロジェクトが開いたら、FTP 機能拡張ライブラリへの参照パスを追加します。

    • [プロジェクト] をクリックし、[FtpAddressRestrictionAuthentication のプロパティ] をクリックします。

    • [参照パス] タブをクリックします。

    • 使用している Windows のバージョンの FTP 拡張機能アセンブリへのパスを入力します。C: はオペレーティング システム ドライブです。

      • Windows Server 2008 と Windows Vista の場合:

        C:\Windows\assembly\GAC_MSIL\Microsoft.Web.FtpServer\7.5.0.0__31bf3856ad364e35
        
      • Windows 7 の場合:

        C:\Program Files\Reference Assemblies\Microsoft\IIS
        
    • [フォルダーの追加] をクリックします。

  5. プロジェクトに厳密な名前のキーを追加します。

    • [プロジェクト] をクリックし、[FtpAddressRestrictionAuthentication のプロパティ] をクリックします。
    • [署名] タブをクリックします。
    • [アセンブリの署名] チェック ボックスをオンにします。
    • 厳密なキー名のドロップダウン ボックスから <[新規]> を選択します。
    • キー ファイル名として「FtpAddressRestrictionAuthenticationKey」と入力します。
    • 必要に応じて、キー ファイルのパスワードを入力します。それ以外の場合は、[パスワードでキー ファイルを保護する] チェック ボックスをオフにします。
    • OK をクリックします。
  6. 省略可能: カスタム ビルド イベントを追加して、開発用コンピューターのグローバル アセンブリ キャッシュ (GAC) に DLL を自動的に追加できます。

    • [プロジェクト] をクリックし、[FtpAddressRestrictionAuthentication のプロパティ] をクリックします。

    • [ビルド イベント] タブをクリックします。

    • [ビルド後に実行するコマンド ライン] ダイアログ ボックスに、次のように入力します。

      net stop ftpsvc
      call "%VS90COMNTOOLS%\vsvars32.bat">null
      gacutil.exe /if "$(TargetPath)"
      net start ftpsvc
      
  7. プロジェクトを [保存] します。

手順 2: 機能拡張クラスを作成する

この手順では、デモ プロバイダーのログ拡張インターフェイスを実装します。

  1. プロジェクトの FTP 拡張機能ライブラリへの参照を追加します。

    • [プロジェクト] をクリックし、[参照の追加...] をクリックします。
    • [.NET] タブで、[Microsoft.Web.FtpServer] をクリックします。
    • OK をクリックします。
  2. プロジェクトの System.Web への参照を追加します。

    • [プロジェクト] をクリックし、[参照の追加...] をクリックします。
    • [.NET] タブで、[System.Web] をクリックします。
    • OK をクリックします。
  3. プロジェクトの System.Configuration への参照を追加します。

    • [プロジェクト] をクリックし、[参照の追加] をクリックします。
    • [.NET] タブで、System.Configuration をクリックします。
    • OK をクリックします。
  4. プロジェクトの System.Data への参照を追加します。

    • [プロジェクト] をクリックし、[参照の追加] をクリックします。
    • [.NET] タブで、System.Data をクリックします。
    • OK をクリックします。
  5. 認証クラスのコードを追加します。

    • ソリューション エクスプローラーで、[Class1.cs] ファイルをダブルクリックします。

    • 既存のコードを削除します。

    • 以下のコードをエディターに貼り付けます。

      using System;
      using System.Collections.Generic;
      using System.Collections.Specialized;
      using System.Configuration.Provider;
      using System.Data;
      using System.Data.SqlClient;
      using System.Text;
      using Microsoft.Web.FtpServer;
      
      public class FtpAddressRestrictionAuthentication :
        BaseProvider,
        IFtpLogProvider,
        IFtpAuthenticationProvider
      {
        // Define the default values - these are only
        // used if the configuration settings are not set.
        const int defaultLogonAttempts = 5;
        const int defaultFloodSeconds = 30;
      
        // Define a connection string with no default.
        private static string _connectionString;
      
        // Initialize the private variables with the default values.
        private static int _logonAttempts = defaultLogonAttempts;
        private static int _floodSeconds = defaultFloodSeconds;
      
        // Flag the application as uninitialized.
        private static bool _initialized = false;
      
        // Define a list that will contain the list of flagged sessions.
        private static List<string> _flaggedSessions;
      
        // Initialize the provider.
        protected override void Initialize(StringDictionary config)
        {
          // Test if the application has already been initialized.
          if (_initialized == false)
          {
            // Create the flagged sessions list.
            _flaggedSessions = new List<string>();
      
            // Retrieve the connection string for the database connection.
            _connectionString = config["connectionString"];
            if (string.IsNullOrEmpty(_connectionString))
            {
              // Raise an exception if the connection string is missing or empty.
              throw new ArgumentException(
                "Missing connectionString value in configuration.");
            }
            else
            {
              // Determine whether the database is a Microsoft Access database.
              if (_connectionString.Contains("Microsoft.Jet"))
              {
                // Throw an exception if the database is a Microsoft Access database.
                throw new ProviderException("Microsoft Access databases are not supported.");
              }
            }
      
            // Retrieve the number of failures before an IP
            // address is locked out - or use the default value.
            if (int.TryParse(config["logonAttempts"], out _logonAttempts) == false)
            {
              // Set to the default if the number of logon attempts is not valid.
              _logonAttempts = defaultLogonAttempts;
            }
      
            // Retrieve the number of seconds for flood
            // prevention - or use the default value.
            if (int.TryParse(config["floodSeconds"], out _floodSeconds) == false)
            {
              // Set to the default if the number of logon attempts is not valid.
              _floodSeconds = defaultFloodSeconds;
            }
      
            // Test if the number is a positive integer and less than 10 minutes.
            if ((_floodSeconds <= 0) || (_floodSeconds > 600))
            {
              // Set to the default if the number of logon attempts is not valid.
              _floodSeconds = defaultFloodSeconds;
            }
      
            // Initial garbage collection.
            GarbageCollection(true);
            // Flag the provider as initialized.
            _initialized = true;
          }
        }
      
        // Dispose of the provider.
        protected override void Dispose(bool disposing)
        {
          base.Dispose(disposing);
      
          // Test if the application has already been uninitialized.
          if (_initialized == true)
          {
            // Final garbage collection.
            GarbageCollection(true);
            // Flag the provider as uninitialized.
            _initialized = false;
          }
        }
      
        // Authenticate a user.
        bool IFtpAuthenticationProvider.AuthenticateUser(
          string sessionId,
          string siteName,
          string userName,
          string userPassword,
          out string canonicalUserName)
        {
          // Define the canonical user name.
          canonicalUserName = userName;
      
          // Check if the session is flagged.
          if (IsSessionFlagged(sessionId) == true)
          {
            // Return false (authentication failed) if the session is flagged.
            return false;
          }
      
          // Check the user credentials and return the status.
          return IsValidUser(userName, userPassword);
        }
      
        // Implement custom actions by using the Log() method.
        void IFtpLogProvider.Log(FtpLogEntry loggingParameters)
        {
          // Test if the control channel was opened or the USER command was sent.
          if ((String.Compare(loggingParameters.Command,
            "ControlChannelOpened", true) == 0)
            || (String.Compare(loggingParameters.Command,
            "USER", true) == 0))
          {
            // Check if the IP address is banned.
            if (IsAddressBanned(loggingParameters.RemoteIPAddress) == true)
            {
              // If the IP is banned, flag the session.
              FlagSession(loggingParameters.SessionId);
              return;
            }
          }
          // Test if the PASS command was sent.
          if (String.Compare(loggingParameters.Command,
            "PASS", true) == 0)
          {
            // Check for password failures (230 is a success).
            if (loggingParameters.FtpStatus != 230)
            {
              // Periodic garbage collection - remove authentication
              // failures that are older than the flood timeout.
              GarbageCollection(false);
      
              // Test if the existing number of failures exceeds the maximum logon attempts.
              if (GetRecordCountByCriteria("[Failures]",
                "[IPAddress]='" + loggingParameters.RemoteIPAddress +
                "'") < _logonAttempts)
              {
                // Add the failure to the list of failures.
                InsertDataIntoTable("[Failures]",
                  "[IPAddress],[FailureDateTime]",
                  "'" + loggingParameters.RemoteIPAddress +
                  "','" + DateTime.Now.ToString() + "'");
              }
              else
              {
                // Ban the IP address if authentication has failed
                // from that IP more than the defined number of failures.
                BanAddress(loggingParameters.RemoteIPAddress);
                FlagSession(loggingParameters.SessionId);
              }
              return;
            }
          }
          // Test if the control channel was closed.
          if (String.Compare(loggingParameters.Command,
            "ControlChannelClosed", true) == 0)
          {
            // Session-based garbage collection - remove the
            // current session from the list of flagged sessions.
            _flaggedSessions.Remove(loggingParameters.SessionId);
            return;
          }
        }
      
        // Check for a valid username/password.
        private static bool IsValidUser(
          string userName,
          string userPassword)
        {
          // Define the initial status as the credentials are not valid.
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command = new SqlCommand())
              {
                // Specify the connection for the command object.
                command.Connection = connection;
                // Specify a text command type.
                command.CommandType = CommandType.Text;
      
                // Specify the SQL text for the command object.
                command.CommandText = "SELECT COUNT(*) AS [NumRecords] " +
                  "FROM [Users] WHERE [UID]=@UID AND [PWD]=@PWD AND [Locked]=0";
      
                // Add parameters for the user name and password.
                command.Parameters.Add("@UID", SqlDbType.NVarChar).Value = userName;
                command.Parameters.Add("@PWD", SqlDbType.NVarChar).Value = userPassword;
      
                // Open the database connection.
                connection.Open();
                // Return the valid status for the credentials.
                return ((int)command.ExecuteScalar() > 0);
              }
            }
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      
        // Check if the IP is banned.
        private bool IsAddressBanned(string ipAddress)
        {
          // Return whether the IP address was found in the banned addresses table.
          return (GetRecordCountByCriteria("[BannedAddresses]",
            "[IPAddress]='" + ipAddress + "'") != 0);
        }
      
        // Check if the session is flagged.
        private bool IsSessionFlagged(string sessionId)
        {
          // Return whether the session ID was found in the flagged sessions table.
          return _flaggedSessions.Contains(sessionId);
        }
      
        // Mark a session as flagged.
        private void FlagSession(string sessionId)
        {
          // Check if the session is already flagged.
          if (IsSessionFlagged(sessionId) == false)
          {
            // Flag the session if it is not already flagged.
            _flaggedSessions.Add(sessionId);
          }
        }
      
        // Mark an IP address as banned.
        private void BanAddress(string ipAddress)
        {
          // Check if the IP address is already banned.
          if (IsAddressBanned(ipAddress) == false)
          {
            // Ban the IP address if it is not already banned.
            InsertDataIntoTable("[BannedAddresses]",
              "[IPAddress]", "'" + ipAddress + "'");
          }
        }
      
        // Perform garbage collection tasks.
        private void GarbageCollection(bool deleteSessions)
        {
          // Remove any authentication failures that are older than the flood timeout.
          DeleteRecordsByCriteria("[Failures]",
            String.Format("DATEDIFF(second,[FailureDateTime],'{0}')>{1}",
            DateTime.Now.ToString(),_floodSeconds.ToString()));
      
          // Test if flagged sessions should be deleted.
          if (deleteSessions == true)
          {
            // Remove any sessions from the list of flagged sessions.
            _flaggedSessions.Clear();
          }
        }
      
        // Retrieve the count of records based on definable criteria.
        private int GetRecordCountByCriteria(
          string tableName,
          string criteria)
        {
          // Create a SQL string to retrieve the count of records 
          // that are found in a table based on the criteria.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append("SELECT COUNT(*) AS [NumRecords]");
          sqlString.Append(String.Format(
            " FROM {0}",tableName));
          sqlString.Append(String.Format(
            " WHERE {0}",criteria));
          // Execute the query.
          return ExecuteQuery(true, sqlString.ToString());
        }
      
        // Insert records into a database table.
        private void InsertDataIntoTable(
          string tableName,
          string fieldNames,
          string fieldValues)
        {
          // Create a SQL string to insert data into a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "INSERT INTO {0}",tableName));
          sqlString.Append(String.Format(
            "({0}) VALUES({1})",fieldNames, fieldValues));
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Remove records from a table based on criteria.
        private void DeleteRecordsByCriteria(
          string tableName,
          string queryCriteria)
        {
          // Create a SQL string to delete data from a table.
          StringBuilder sqlString = new StringBuilder();
          sqlString.Append(String.Format(
            "DELETE FROM {0}",tableName));
          // Test if any criteria is specified.
          if (string.IsNullOrEmpty(queryCriteria) == false)
          {
            // Append the criteria to the SQL string.
            sqlString.Append(String.Format(
              " WHERE {0}",queryCriteria));
          }
          // Execute the query.
          ExecuteQuery(false, sqlString.ToString());
        }
      
        // Execute SQL queries.
        private int ExecuteQuery(bool returnRecordCount, string sqlQuery)
        {
          try
          {
            // Create a new SQL connection object.
            using (SqlConnection connection =
              new SqlConnection(_connectionString))
            {
              // Create a new SQL command object.
              using (SqlCommand command =
                new SqlCommand(sqlQuery, connection))
              {
                // Open the connection.
                connection.Open();
                // Test whether the method should return a record count.
                if (returnRecordCount == true)
                {
                  // Run the database query.
                  SqlDataReader dataReader = command.ExecuteReader();
                  // Test if data reader has returned any rows.
                  if (dataReader.HasRows)
                  {
                    // Read a single row.
                    dataReader.Read();
                    // Return the number of records.
                    return ((int)dataReader["NumRecords"]);
                  }
                }
                else
                {
                  // Run the database query.
                  command.ExecuteNonQuery();
                }
              }
            }
            // Return a zero record count.
            return 0;
          }
          catch (Exception ex)
          {
            // Raise an exception if an error occurs.
            throw new ProviderException(ex.Message);
          }
        }
      }
      
  6. プロジェクトを保存してコンパイルします。

Note

オプションの手順を使用してアセンブリを GAC に登録しなかった場合は、手動でアセンブリを IIS コンピューターにコピーし、Gacutil.exe ツールを使用してアセンブリを GAC に追加する必要があります。 詳細については、「Gacutil.exe (グローバル アセンブリ キャッシュ ツール)」を参照してください。

手順 3: FTP へのデモ プロバイダーの追加

この手順では、FTP サービスと既定の Web サイトにデモ プロバイダーを追加します。

  1. 機能拡張プロバイダーのアセンブリ情報を確認します。

    • Windows エクスプローラーで C:\Windows\assembly パスを開きます。C: はオペレーティング システム ドライブです。
    • FtpAddressRestrictionAuthentication アセンブリを見つけます。
    • アセンブリを右クリックし、[プロパティ] を選択します。
    • カルチャ値 (例: Neutral) をコピーします。
    • バージョン番号 (例: 1.0.0.0) をコピーします。
    • Public Key Token 値 (例: 426f62526f636b73) をコピーします。
    • [キャンセル] をクリックします。
  2. 前の手順の情報を使用して、拡張プロバイダーを FTP プロバイダーのグローバル リストに追加し、プロバイダーのオプションを構成します。

    • 現時点では、カスタム認証モジュールのプロパティを追加できるユーザー インターフェイスがないため、次のコマンド ラインを使用する必要があります。

      cd %SystemRoot%\System32\Inetsrv
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"[name='FtpAddressRestrictionAuthentication',type='FtpAddressRestrictionAuthentication,FtpAddressRestrictionAuthentication,version=1.0.0.0,Culture=neutral,PublicKeyToken=426f62526f636b73']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='connectionString',value='Server=localhost;Database=FtpAuthentication;User ID=FtpLogin;Password=P@ssw0rd']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='logonAttempts',value='5']" /commit:apphost
      
      appcmd.exe set config -section:system.ftpServer/providerDefinitions /+"activation.[name='FtpAddressRestrictionAuthentication'].[key='floodSeconds',value='30']" /commit:apphost
      

    Note

    connectionString 属性で指定する接続文字列は、データベースの有効なログインである必要があります。

  3. カスタム プロバイダーをサイトに追加します。

    • 現時点では、サイトにカスタム機能を追加できる UI がないため、次のコマンド ラインを使用する必要があります。

      AppCmd.exe set config -section:system.applicationHost/sites /"[name='Default Web Site'].ftpServer.security.authentication.basicAuthentication.enabled:False" /commit:apphost
      
      AppCmd.exe set config -section:system.applicationHost/sites /+"[name='Default Web Site'].ftpServer.security.authentication.customAuthentication.providers.[name='FtpAddressRestrictionAuthentication',enabled='True']" /commit:apphost
      
      AppCmd set site "Default Web Site" /+ftpServer.customFeatures.providers.[name='FtpAddressRestrictionAuthentication',enabled='true'] /commit:apphost
      

    Note

    この構文により FTP 基本認証が無効になります。この認証プロバイダーを使用する場合は、基本認証を無効にすることが重要です。 そうしないと、攻撃者の IP アドレスがこの認証プロバイダーによってブロックされた場合でも、攻撃者は基本認証を使用するアカウントを攻撃できてしまいます。

  4. 認証プロバイダーの認可規則を追加します。

    • メイン ウィンドウで [FTP Authorization Rules] (FTP の認可規則) をダブルクリックします。

    • [操作] ウィンドウで [許可規則の追加] をクリックします。

    • アクセス オプションに [指定されたユーザー] を選択します。

    • ユーザー名を入力します。

      Note

      このユーザー名は、この手順リストの外部でデータベースに入力する必要があります。

    • [アクセス許可] オプションの [読み取り][書き込み] の一方または両方を選択します。

    • OK をクリックします。

手順 4: FTP 7.5 でプロバイダーを使用する

FTP クライアントが FTP サイトに接続すると、FTP サービスは、データベースに格納されているアカウントを使用して、カスタム認証プロバイダーでユーザーの認証を試みます。 FTP クライアントが認証に失敗した場合、プロバイダーは IP アドレスと失敗の日時をデータベースに記録します。 FTP クライアントが logonAttempts 設定で指定されたエラーの数に対して特定の IP アドレスからのログインに失敗し、floodSeconds 設定で指定された期間内である場合、プロバイダーはその IP アドレスが FTP サービスにログインするのをブロックします。

Note

このサンプル プロバイダーは FTP サービスの認証ロジックを実装しますが、データベース内のデータを管理するための管理モジュールは提供していません。 たとえば、このプロバイダーを使用して、FTP ユーザー アカウント、禁止 IP アドレス、および認証失敗の一覧を管理することはできません。 IIS マネージャーを使用してデータを管理する場合は、IIS データベース マネージャーを使用できます。 詳細については、次のトピックを参照してください。

https://www.iis.net/extensions/DatabaseManager

追加情報

Microsoft SQL Server 用の次の SQL スクリプトを使用して、必要なデータベースとテーブルを作成できます。 このスクリプトを使用するには、データベースの名前とデータベース ファイルの場所を更新する必要があります。 SQL Server で、このスクリプトを新しいクエリ ウィンドウで実行し、接続文字列で使用するデータベース ログインを作成します。

Note

c:\databases 以外の場所にデータベースを格納するように SQL スクリプトを変更することもできます。

/****** Create the FtpAuthentication Database ******/

USE [master]
GO
CREATE DATABASE [FtpAuthentication] ON  PRIMARY 
( NAME = N'FtpAuthentication', FILENAME = N'c:\databases\FtpAuthentication.mdf' , SIZE = 2048KB , MAXSIZE = UNLIMITED, FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'FtpAuthentication_log', FILENAME = N'c:\databases\FtpAuthentication_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%)
 COLLATE SQL_Latin1_General_CP1_CI_AS
GO
EXEC dbo.sp_dbcmptlevel @dbname=N'FtpAuthentication', @new_cmptlevel=90
GO
IF (1 = FULLTEXTSERVICEPROPERTY('IsFullTextInstalled'))
begin
EXEC [FtpAuthentication].[dbo].[sp_fulltext_database] @action = 'enable'
end
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULL_DEFAULT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_NULLS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_PADDING OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ANSI_WARNINGS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ARITHABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CLOSE OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_CREATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_SHRINK OFF 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS ON 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_CLOSE_ON_COMMIT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET CURSOR_DEFAULT  GLOBAL 
GO
ALTER DATABASE [FtpAuthentication] SET CONCAT_NULL_YIELDS_NULL OFF 
GO
ALTER DATABASE [FtpAuthentication] SET NUMERIC_ROUNDABORT OFF 
GO
ALTER DATABASE [FtpAuthentication] SET QUOTED_IDENTIFIER OFF 
GO
ALTER DATABASE [FtpAuthentication] SET RECURSIVE_TRIGGERS OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ENABLE_BROKER 
GO
ALTER DATABASE [FtpAuthentication] SET AUTO_UPDATE_STATISTICS_ASYNC OFF 
GO
ALTER DATABASE [FtpAuthentication] SET DATE_CORRELATION_OPTIMIZATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET TRUSTWORTHY OFF 
GO
ALTER DATABASE [FtpAuthentication] SET ALLOW_SNAPSHOT_ISOLATION OFF 
GO
ALTER DATABASE [FtpAuthentication] SET PARAMETERIZATION SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET READ_WRITE 
GO
ALTER DATABASE [FtpAuthentication] SET RECOVERY SIMPLE 
GO
ALTER DATABASE [FtpAuthentication] SET MULTI_USER 
GO
ALTER DATABASE [FtpAuthentication] SET PAGE_VERIFY CHECKSUM  
GO
ALTER DATABASE [FtpAuthentication] SET DB_CHAINING OFF 

/****** Create the Database Tables ******/

USE [FtpAuthentication]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[BannedAddresses]') AND type in (N'U'))
BEGIN
CREATE TABLE [BannedAddresses](
    [IPAddress] [nvarchar](50) NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Failures]') AND type in (N'U'))
BEGIN
CREATE TABLE [Failures](
    [IPAddress] [nvarchar](50) NOT NULL,
    [FailureDateTime] [datetime] NOT NULL
) ON [PRIMARY]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[Users]') AND type in (N'U'))
BEGIN
CREATE TABLE [Users](
    [UID] [nvarchar](50) NOT NULL,
    [PWD] [nvarchar](50) NOT NULL,
    [Locked] [bit] NOT NULL
) ON [PRIMARY]
END

まとめ

このチュートリアルでは、次の方法を学習しました。

  • カスタム FTP プロバイダー用のプロジェクトを Visual Studio 2008 で作成する。
  • カスタム FTP プロバイダーの機能拡張インターフェイスを実装する。
  • FTP サービスに FTP カスタム プロバイダーを追加する。