株式会社ネットワールドのエンジニアがお届けする技術情報ブログです。
各製品のエキスパートたちが旬なトピックをご紹介します。

F5 BIG-IP APMでのMFA実装 | Google Authenticator連携手順まとめ

皆様こんにちは!ネットワールドSEの渡邊です!

本記事では、BIG-IP Access Policy Manager(APM)Google Authenticator(GA)を組み合わせた多要素認証(MFA) を実装する手順をご紹介します。

BIG-IP APMの標準機能を活用しつつ組み合わせることで、コストを抑えながらも強固な認証基盤を実現できます。
本記事では実際の構築ステップを順を追って解説いたしますので、BIG-IPをお使いの方でMFA導入を検討されている方の参考になれば幸いです。

※なお、本記事は「APM の基本操作に慣れていること」「ローカルパスワード認証などの簡易フローによる認証と接続先の設定を既に構築済みであること」を前提として進めます。

GA認証トークン生成用VSを作成

Google Authenticatorにアカウントを登録するためのQRコード(TOTP秘密鍵を含む otpauth URI)を配布する専用の仮想サーバ(Virtual ServerVSを用意します。VS 自体の作成はシンプルですが、QRコードの生成・表示機能はiRuleと、iRuleから呼び出す HTML・JavaScript に実装します。本章では次の順序で構成します。

  • JavaScriptの準備(QRコード生成ライブラリ)

  • HTMLの作成(登録ページ、TOTP シークレット・otpauth URLの生成)

  • iRuleの作成(ページ表示、及びURIによるHTMLの参照先制御)

  • 仮想サーバ(VS) の作成(ユーザーの接続先の設定)

ここで作成するVSは原則外部に公開しないか、アクセス元を厳格に制限してください

jsファイルを用意

QRコード生成機能を実装するためのJavaScript ファイルを準備します。

検証や社内での利用であれば、以下の CDN から配布されている「qrcode.min.js」をそのまま利用しても特に問題ありません。

https://cdn.jsdelivr.net/npm/qrcode/build/qrcode.min.js

※ jsDelivrはnpm公開モジュールをミラー配布するCDNです

 

一方で、本番環境での運用を前提とする場合は、一次ソースから取得する方が望ましいです。以下のコマンドを実行して取得することができます。(Node.js および npm がインストールされている必要があります)

npm install qrcode

インストール後、以下のファイルがnode_modules配下に生成されます。

  • node_modules/qrcode/build/qrcode.min.js

  • node_modules/qrcode/build/qrcode.js

このうち「qrcode.min.js」はCDNの配布ファイルと同一内容ですので、これをBIG-IP APMに配置して利用します。

 

htmlファイルを作成

認証トークン(QRコード)を発行するためのページをHTML形式で作成します。
このページはiRuleで呼び出され、「GA認証トークン生成用VS」にアクセスしたユーザーに表示されます。

以下に参考用のコードを記載しています。ほぼコピペで利用可能ですが、環境に応じたカスタマイズ(デザイン調整、参照jsファイル名など)は必要に応じて実施してください。

ポイントは以下の通りです。

  • <script src="qrcode.min.js"></script>
    で、前項で用意したJavaScriptファイルを参照します。
    これにより生成したQRコードをブラウザで描画できます。

  • フォームにメールアドレスを入力してもらい、@より前の部分をアカウント名として取得します。

  • HTMLコードでランダムなBase32シークレットを生成し、otpauth URLを作成、その結果をQRコードとして描画します。


<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Google Authenticator QR Code Generator</title>
    <script src="qrcode.min.js"></script>
    <style type="text/css">
        body {
            display:flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            margin: 50px ;
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
        }
        h1 {
            position: relative;
            margin-bottom: 40px;
            font-size: 24px;
            text-align: center;
            color: #333;
        }
        h1:after {
            content: '';
            display: block;
            width: 100%;
            height: 4px;
            background-image: linear-gradient(to right, #4285f4 25% , #db4437 25%, #db4437 50%, #f4b400 50%, #f4b400 75%, #0f9d58 75%);
            position: absolute;
            bottom: -10px;
            left: 0;
        }
        h2 { margin: 10px 0px 20px; font-size: 28px; color: #007bb8 !important; font-weight: 400; line-height: 1em; padding: 0px 0px 12px; width: 100%; border-bottom: 4px solid #ebebeb; min-height: 33px; }
        th { padding-right: 5px; }
        input[type="text"] { line-height: normal; width: 120px; margin-bottom: 5px; padding: 7px; background: #ffffff; border: 1px solid #c9c9c9; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.8), inset 0px 1px 2px 0px rgba(0, 0, 0, 0.1); box-shadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.8), inset 0px 1px 2px 0px rgba(0, 0, 0, 0.1); color: #333; font-size: 17px; }
        input[type="submit"] { background-color: #0075b8; border-radius: 4px; border: none; color: #fff; padding: 9px 10px !important; margin: 1px 0px 0px 12px; font-weight: 700; text-decoration: none; font-size: 13px; text-align: center; line-height: 16px; font-family: Arial, Helvetica, sans-serif; min-width: 75px; }
        fieldset { width: 500px; padding: 10px 20px 10px 20px; overflow: hidden; border-style: none; margin: 0 auto; }
        input[name="account"] { text-align: right; }
        input[name="domain"] { width: 150px; }
        table { float: left; }
        #qrForm {
            background-color: #eee;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
            margin-bottom: 20px;
        }
        script {
            background-color: #eee;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
            margin-bottom: 20px;
        }
        button {
            background-color: #4285f4;
            color: white;
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            width: 300px;
            display: block;
            margin: 0 auto;
        }
        button:hover {
            background-color: #357ae8;
        }
        label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #333;
        }
        input[type="text"], input[type="email"] {
            width: 100%;
            padding: 8px;
            margin-bottom: 16px;
            border: 1px solid #ccc;
            border-radius: 4px;
            box-sizing: border-box;
            font-size: 14px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h1 class="title1">Google Authenticator</h1>
    <form id="qrForm" >

        <label for="email">メールアドレス</label>
        <input type="email" id="email" name="email" placeholder="Enter your email address" required><br><br>

        <button type="submit">Generate QR Code</button>
    </form>

    <canvas id="qrcode"></canvas>
    <p id="qrText"></p>
    <script>
        function generateRandomSecret(length) {
            const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32のキャラクタセット
            let result = '';
            const bytes = new Uint8Array(length);
            window.crypto.getRandomValues(bytes); // セキュアなランダムバイトを生成
            
            for (let i = 0; i < length; i++) {
                result += charset[bytes[i] % charset.length];
            }
            return result;
        }

        document.getElementById('qrForm').addEventListener('submit', function(event) {
            event.preventDefault(); // フォームのデフォルトの送信動作をキャンセル

            //const issuer = document.getElementById('issuer').value;
            const email = document.getElementById('email').value;
            const accountName = email.split('@')[0]; // メールアドレスの@より前の部分を取得
            const secret = generateRandomSecret(16); // 16文字のランダムなBase32シークレットを生成

            //const otpAuthUrl = `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}`;
            const otpAuthUrl = `otpauth://totp/${encodeURIComponent(accountName)}?secret=${secret}`;

            // QRコードのテキストを表示
            document.getElementById('qrText').innerText = `アカウント名: ${accountName} \n 鍵: ${secret}`;
            
            // QRコードを生成
            QRCode.toCanvas(document.getElementById('qrcode'), otpAuthUrl, function(error) {
            });
        });
    </script>
</body>
</html>

BIG-IPにjsファイルとhtmlファイルを配置

ここまででJavaScriptファイルとHTMLファイルを準備しました。次のステップでは、これらをBIG-IPにiFileとしてアップロードし、iRuleで参照できるようにします。

  1. 左側メニューで「System > File Management > iFile List」を選択
  2. [Import]をクリック

  3. アップロード対象ファイルを選択し、以下を指定
    Create Newを選択
    ・iFile名を入力(iRuleで参照するオブジェクト名になります)
      ・htmlファイル:qrcode_generator.html 
      ・jsファイル:qrcode.min.js 

     

  4. [Import]をクリック
  5. もう一方のファイルも同じ手順でアップロードします。

iFile名はiRuleで直接参照されるため、。iRuleと記述を合わせる必要があることにご注意ください。手順通りの名前であれば問題ありません。

iRule作成

ここまででJavaScript ファイルとHTMLファイルをiFileとしてBIG-IP APMに登録しました。次のステップでは、ユーザーが仮想サーバ(VS)へアクセスした際にQRコード生成ページを返すiRuleを作成します。

以下手順で作成していきます。

  1. 左側メニューで「Local Traffic > iRules > iRule List」をクリック
  2. [Create...]をクリック

  3. 以下内容を入力し、[Finished]をクリック
    ・Name:任意の名前(本記事では例としてgenerate_ga_codeを使用)
    ・Definition:以下の内容を入力
when HTTP_REQUEST {
    # Required for calling Js
    if { [HTTP::path] starts_with "/qrcode.min.js" } {
        HTTP::respond 200 content [ifile get qrcode.min.js] "Content-Type" "application/javascript"
    }
    else {
        # QR Code Generate page
        HTTP::respond 200 content [ifile get qrcode_generator.html] "Content-Type" "text/html"
    }
}

 

補足

  • ifile getに指定する名称はiFile登録時のオブジェクト名と一致させる必要があります。

  • iRule名は運用管理しやすいように、役割が一目で分かる命名を推奨します。(本記事の例のままでも問題ございません)

VS作成

最後にユーザーがアクセスする、QRコード生成用の仮想サーバ(VS)を作成します。
このVSは「QRコード生成用ページの配布専用」ですので、APMへの接続に使うVSとは別系統で作成します。

・Name:任意(例:VS_GENERATE_TOKEN)

・Destination Address/Mask:任意(例:10.1.10.80)

・Service Port:443(後述の通り、9443等の別ポートも可)

・HTTP Profile (Client):http

・SSL Profile (Client):clientssl(本番では正式な証明書を推奨)

・iRules:前手順で作成したiRule

補足

同一 IP の別ポートとして動かす構成も可能です。

  • APM接続VS:10.1.10.80:443

  • 認証トークン生成VS:10.1.10.80:9443
    ※この場合は接続先URLにポート番号を付けてアクセスします。

また、さらにセキュリティを強化したい場合は「Source Address」にGA登録作業を行う管理セグメントを設定して、アクセス制限をかけることも出来ます。

 

上記VSにブラウザでアクセスすると、Google Authenticator登録用のQRコード生成ページが表示されます。
APMとの連携時にはアカウント名鍵(シークレット)両方が必要になるため、発行時に必ず控えてください。

GAとAPMを連携

ここまでで、Google Authenticator登録用のQRコード生成VSを作成しました。

次のステップでは、BIG-IP APMのアクセスポリシー(VPE)にTOTP(GA のワンタイムパスワード)検証を組み込み、多要素認証フローにしていきます。

Data Group List作成

まずは、ユーザーのアカウント名とシークレットキーを格納するData Group Listを作成します。これは TOTP検証時にiRuleから参照される「鍵ストア」の役割を担います。

  1. 左側メニューで、「Local Traffic  >  iRules > Data Group List」をクリック
  2. [Create]をクリック
  3. 以下を入力
    ・Name:google_auth_keys
    ・Type:String
    ・String:アカウント名
    ・Value:鍵(シークレット)
    ※アカウント名と鍵は先ほどの手順でQRコードを生成した際に発行されたものを入力します。
  4. [Add]をクリックし、登録内容を確認して、[Finished]をクリック

     

補足

  • 命名
    本記事では「google_auth_keys」という名前で作成します。この名前はiRuleで直接参照するため、変更する場合はiRule側も合わせて修正が必要です。

  • セキュリティ
    Data Groupには認証の根幹となるシークレットが格納されます。これは管理画面で平文で確認できる他、バックアップファイル(UCS)に含まれるため、管理画面のアクセス制限やバックアップファイルの取り扱いに注意が必要です。

  • 運用
    ユーザーの追加・削除は管理者が手動で実施する運用となります。

 

iRule作成(OTP検証用)

次に、APMの認証フロー内でGoogle Authenticatorのワンタイムパスワードを検証するiRuleを作成します。

これはユーザーが入力したワンタイムパスワードと、Data Group Listに保存されているシークレットキーを照合し、その結果を認証フローに渡す役割を持ちます。

  • Name:ga_code_verify
  • Definition:以下の内容を入力

when ACCESS_POLICY_AGENT_EVENT {

    if { [ACCESS::policy agent_id] eq "ga_code_verify" } {
        ### Google Authenticator verification settings ###

        # lock the user out after x attempts for a period of x seconds
        set static::lockout_attempts 3
        set static::lockout_period 30

        # logon page session variable name for code attempt form field
        set static::ga_code_form_field "ga_code_attempt"

        # key (shared secret) storage method: ldap, ad, or datagroup
        set static::ga_key_storage "datagroup"

        # LDAP attribute for key if storing in LDAP (optional)
        set static::ga_key_ldap_attr "google_auth_key"

        # Active Directory attribute for key if storing in AD (optional)
        set static::ga_key_ad_attr "google_auth_key"

        # datagroup name if storing key in a datagroup (optional)
        set static::ga_key_dg "google_auth_keys"


        #####################################
        ### DO NOT MODIFY BELOW THIS LINE ###
        #####################################

        # set lockout table
        set static::lockout_state_table "[virtual name]_lockout_status"

        # set variables from APM logon page
        set username [ACCESS::session data get session.logon.last.username]
        set ga_code_attempt [ACCESS::session data get session.logon.last.$static::ga_code_form_field] 

        # retrieve key from specified storage
        set ga_key ""

        switch $static::ga_key_storage {
            ldap {
                set ga_key [ACCESS::session data get session.ldap.last.attr.$static::ga_key_ldap_attr]
            }
            ad {
                set ga_key [ACCESS::session data get session.ad.last.attr.$static::ga_key_ad_attr]
            }
            datagroup {
                set ga_key [class lookup $username $static::ga_key_dg]
            }
        }

        # increment the number of login attempts for the user
        set prev_attempts [table incr -notouch -subtable $static::lockout_state_table $username]
        table timeout -subtable $static::lockout_state_table $username $static::lockout_period

        # verification result value: 
        # 0 = successful
        # 1 = failed
        # 2 = no key found
        # 3 = invalid key length
        # 4 = user locked out

        # make sure that the user isn't locked out before calculating GA code
        if { $prev_attempts <= $static::lockout_attempts } {

            # check that a valid key was retrieved, then proceed
            if { [string length $ga_key] == 16 } {
                # begin - Base32 decode to binary

                # Base32 alphabet (see RFC 4648)
                array set static::b32_alphabet {
                    A 0  B 1  C 2  D 3
                    E 4  F 5  G 6  H 7
                    I 8  J 9  K 10 L 11
                    M 12 N 13 O 14 P 15
                    Q 16 R 17 S 18 T 19
                    U 20 V 21 W 22 X 23
                    Y 24 Z 25 2 26 3 27
                    4 28 5 29 6 30 7 31
                }

                set ga_key [string toupper $ga_key]
                set l [string length $ga_key]
                set n 0
                set j 0
                set ga_key_bin ""

                for { set i 0 } { $i < $l } { incr i } {
                    set n [expr $n << 5]
                    set n [expr $n + $static::b32_alphabet([string index $ga_key $i])]
                    set j [incr j 5]

                    if { $j >= 8 } {
                        set j [incr j -8]
                        append ga_key_bin [format %c [expr ($n & (0xFF << $j)) >> $j]]
                    }
                }

                # end - Base32 decode to binary

                # begin - HMAC-SHA1 calculation of Google Auth token

                set time [binary format W* [expr [clock seconds] / 30]]

                set ipad ""
                set opad ""

                for { set j 0 } { $j < [string length $ga_key_bin] } { incr j } {
                    binary scan $ga_key_bin @${j}H2 k
                    set o [expr 0x$k ^ 0x5C]
                    set i [expr 0x$k ^ 0x36]
                    append ipad [format %c $i]
                    append opad [format %c $o]
                }

                while { $j < 64 } {
                    append ipad 6
                    append opad \\
                    incr j
                }

                binary scan [sha1 $opad[sha1 ${ipad}${time}]] H* token

                # end - HMAC-SHA1 calculation of Google Auth hex token

                # begin - extract code from Google Auth hex token

                set offset [expr ([scan [string index $token end] %x] & 0x0F) << 1]
                set ga_code [expr (0x[string range $token $offset [expr $offset + 7]] & 0x7FFFFFFF) % 1000000]
                set ga_code [format %06d $ga_code]

                # end - extract code from Google Auth hex token

                if { $ga_code_attempt eq $ga_code } {
                    # code verification successful
                    set ga_result 0
                } else {
                    # code verification failed
                    # log local0. "Code $ga_code"
                    # log local0. "Attempt $ga_code_attempt"
                    set ga_result 1
                }
            } elseif { [string length $ga_key] > 0 } {
                # invalid key length, greater than 0, but not length not equal to 16 chars
                set ga_result 3
            } else {
                # could not retrieve user's key
                set ga_result 2
            }
        } else {
            # user locked out due to too many failed attempts
            set ga_result 4
        }

        # set code verification result in session variable
        ACCESS::session data set session.custom.ga_result $ga_result
    }
}

補足

  • Data Group Listのアカウント名と、ログオンページの入力値(username)が一致している必要があります。
  • iRule内の「lockout_attempts」と「lockout_period」により、ブルートフォース攻撃対策が可能です。

  • ログ出力部分はデバッグ用途です。本番運用ではOTPコードをログに残さない ようにコメントアウトのままにするか削除してください。
    • # log local0. "Code $ga_code"
    • # log local0. "Attempt $ga_code_attempt"

認証フロー(VPE)を作成

本記事ではユーザー名+GAの6桁OTPのみを入力する最小構成で、TOTP検証をAPMに組み込みます。

補足

他の認証を併用する場合は、既存のAD認証やLocalDB認証等の後段に同じ要領で本記事で作成するフローを挿入するか、本記事で作成するフローをマクロとして作成し、後段に挿入することをおすすめします。

 

以下は本記事での完成イメージと画像です。また、MessageBoxは必須ではありませんがトラブルシュート等で役に立ちます。

Start
  └─ Logon Page(ユーザー入力ページ)
        └─ iRule Event(画像のGoogle Auth Verificationの部分)
              ├─ [ga_result==0] (OTP一致)→ Resource Assign → Allow
              ├─ [ga_result==1] (OTP不一致)→  Deny
              ├─ [ga_result==2] (鍵未登録) → Message Box → Deny
              ├─ [ga_result==3] (鍵長不正) → Message Box → Deny
              ├─ [ga_result==4] (ロックアウトユーザ) → Message Box → Deny
              └─ Fallback  → Deny

※実際にはOTP不一致の場合はFallback処理に流しています。

ログオンページ作成

[Start] の右の+マークをクリック、「Logon Page」を検索・選択して[Add Item]をクリックします。



その後以下の内容を入力して[Save]をクリックします。

  • Name:任意例:Logon Page GA)
  • 1
    • Type:text
    • Post Variable Name:username
    • Session Variable Name:username
    • Type:text
    • Post Variable Name:ga_code_attempt
    • Session Variable Name:ga_code_attempt
  • Logon Page Input Field #1:任意(例:Username)
  • Logon Page Input Field #2:任意(例:Google Authenticator Token)
  • Logon Button:任意(例:Submit)

この設定により、以下のようなログオンページとなります。

補足

  • 変数名の整合性

    • セッション変数名「ga_code_attempt」はiRule側の設定(set static::ga_code_form_field "ga_code_attempt")と一致させる必要があります。(手順通り設定していれば問題はありません)

    • 一致しない場合、セッション変数に値が格納されず認証が常に失敗するので注意してください。

  • 不要な項目

    • 本構成ではパスワード入力欄は不要のため追加しません。

  • UIのカスタマイズ

    • Logon Page Input Field、Logon Buttonは好みに応じて変更可能です
      (例:「アカウント名」「ワンタイムパスコード」など)。

ワンタイムパスワード検証・条件分岐部分を作成

次に、Google Authenticatorのワンタイムパスワードを検証し、結果に応じて認証フローを分岐させます。ここではマクロ(Macro)機能を活用して作成していきます。

 

1. マクロを作成

  • VPE のツールバーから [Add New Macro] をクリック

  • 名前を「Verify Google Token」と入力し。[Save]をクリック。

補足

マクロは複数の認証処理をまとめて部品化できる機能です。認証フローを整理・再利用できるため、本記事では採用しています。

 

2. マクロ内にiRule Eventを追加

  • 作成したマクロ内で+マークをクリックし、新しい認証フロー項目を追加します。

  • 「iRule Event」 を検索・選択し、[Add Item] をクリックします。

  • 以下内容を入力して[Save]をクリックします

Properties

  • Name:任意
  • ID:ga_code_verify
    • iRule内の「agent_id」とIDを一致させること

Branch Rules( [Add Branch Rule] を4回クリックして追加)

  • Name:Successful
    • Expression:expr { [mcget {session.custom.ga_result}] == 0 }
  • Name:No Google Auth Key Found
    • Expression:expr { [mcget {session.custom.ga_result}] == 2 }
  • Name:Invalid Google Auth Key
    • Expression:expr { [mcget {session.custom.ga_result}] == 3 }
  • Name:User locked out
    • Expression:expr { [mcget {session.custom.ga_result}] == 4 }

補足

Expressionの入力は「change」を押した後、「Advanced」タブに切り替えると、手入力できます。入力したら[Finished]ボタンをクリックすると、元の画面に戻ります。

 

3. マクロのTerminalの編集

iRule Event作成直後は以下の画像のように、全ての分岐が「Out」に繋がっており、分かり辛い上に、このままだと後段のフローに全て同じ分岐で渡すことになってしまいます。

なのでマクロの「Terminal」を編集します。

  • [Edit Terminals]をクリック
  • 以下のように設定を変更し、[Save]をクリック。
    1. OutFailureに書き換えて、色を♯2に変更 
    2. [Add Terminal]を1回クリック
    3. 新規追加したTerminalのNameをSuccessfulにし、色を♯1に変更

  • Successfulフローの終端を「Successful」に変更します。

 

↓の画像のような状態になればOKです。

完成品を組み立てていく

残りの手順は、BIG-IP APMを普段利用している方であれば馴染みのある操作です。これまでに作成したログオンページマクロを組み合わせ、完成形の認証フローに仕上げます。

  • フロー図の「Logon Page」の右側の+マークをクリック

  • 「Macros」タブをクリックし、先ほど作成したマクロを選択して、[Add Item]をクリック

↓の画像のようになります。

先ほどマクロでSuccessfulFailureで条件分岐の終端を設定していたのは、このようにSuccessfulの時のみAllowに進むフローに構成するためです。

 

  • ↑画像のフロー図の、マクロの右、Successfulの右にある+マークをクリック
  • ご自身の環境に合わせて接続先(Advanced Resource Asign等)の設定を追加

これでGoogle Authenticatorの認証に成功した場合のみ、リソースへ接続出来る認証フローが完成します。

 

動作イメ―ジ

実際にAPM接続用VSにアクセスすると、以下のようなログオンページが表示されます。

  • Username:本記事の場合はメールアドレスの@より前の部分
  • Google Authenticator Token:GAアプリに表示されたワンタイムパスワード

入力後に[Submit]ボタンを押すと、ワンタイムパスワードが正しければリソースに接続できます。

以上で、BIG-IP APMとGoogle Authenticatorを用いたMFA構成の最小構成が完成です。

運用時の注意点

ユーザー情報は管理者が手動で登録する必要がある

Data Group Listに「アカウント名」「」を管理者が手動で登録する必要があります。

登録者はQRコード生成時に発行された情報を確実に控え、管理者が台帳等を元にData Group Listに反映していく形になります。

GA認証失敗時のメッセージ表示方法

基本構成のままでは、認証が失敗した際にユーザーが理由を把握できません。
ユーザーにとって分かりやすいフィードバックを返すため、運用初期等はマクロの失敗フローに「Message Box」 を配置するとトラブルシュートに役立ちます。


Message Boxは +マークをクリックし、「Message Box」で検索・選択すると追加できます。

 

設定の参考例は以下の通りです(基本的には「Message」を編集するだけでOKです)。

  • No Key Found

Message:
No Google Auth Key Found
%{session.logon.last.username}

  • Invalid

Message:
Invalid Google Auth Key
%{session.logon.last.username}

  • Locked Out

Message:Locked out

 

 

補足

  • 実運用では、失敗理由を詳細に返しすぎると攻撃者へのヒントになり得ます。

    • 「アカウントが存在しない」「キーが不正」などのメッセージボックスはトラブルシュート時のみの追加に留め、ユーザーには「認証に失敗しました」など簡潔な表現に統一する運用が望ましいです。

  • 本記事ではマクロの終端を統一していましたが、例えばこれを「No key」「Invalid」等に変更することで、APMの「Access Report」で管理者のみが原因を追跡できるようにすることも可能です。

TOTP 検証の安定運用

NTPによる時刻同期は必須。特にBIG-IPの時刻がずれると多くのユーザーで認証エラーが頻発します。

セキュリティ上の注意点と対策まとめ

Google Authenticator(GA)との連携はMFAによる強固なセキュリティを実現可能ですが、シークレットキーの取り扱いやBIG-IP側の設定次第ではリスクも残ります。

シークレット(鍵)の取り扱い
  • TLS(HTTPS)でページを配信し、平文でのやりとりは避ける。

  • Data Group に登録したシークレットは バックアップファイル(UCS)に含まれるため、管理者端末等に保存する際は、バックアップファイルの暗号化・ファイル権限管理等を実施する。

  • ユーザーにキーを渡す経路はメールやチャット等ではなく、社内ネットワーク内での台帳管理等、安全な経路、手段にする。

アクセス制御
  • QRコード生成用VSは社内ネットワークや管理者端末限定アクセス(VSのSource Address設定等で実装)にする。インターネット公開は非推奨。

    • APM接続用VSと別IP、もしくはService Portを9443などに分け、分離して管理する。

運用
  • ユーザー追加・削除・再発行は必ず管理者が行う。

  • 認証失敗時は、ユーザー向けには「認証に失敗しました」とシンプルに返す運用を基本とし、詳細理由(キー不在、長さ不正など)は 管理者のみ確認できるログで出力する。
    • 運用初期は「認証失敗の理由を分かりやすく返す」ことでトラブルシュートを優先し、安定稼働後に詳細理由を制限する方針に切り替えるのも一つの手です。