AWS ALB のアイドルタイムアウトをターゲットより短くする理由 - プログラムコードから見てみる

前回、OS の動きや、ユーザーランドプログラムの動きから、「送信のターンにある方から切断する方がスムーズ」ということを説明しました。

今回は、ALB の動きを単純化した C 言語*1のコードに起こしてみます。

リバースプロキシとしての挙動

ALB のロードバランサーとしての機能を無視すると、中核はリバースプロキシとしての動作と考える事ができます。

さらに、通常のリバースプロキシなら当然の機能であるパスのマッピングなども省略し、

  • クライアント届いた HTTP リクエストをターゲットへ送信し、
  • ターゲットから届いたレスポンスをクライアントに送信する。

という感じにします。

更に省略

本当は、リクエスト・レスポンスがそれぞれ1行で、改行が終端となるような仮想的なプロトコルを考えた方が単純化できるのですが、そこまですると、元の話が ALB でプロトコルが HTTP であることからも離れてしまうので、一応、HTTP を扱っている ふう にします。

なので、実際には別途、コードを書く必要があるけど、省略して関数の呼び出しだけを書いている部分があります。先のそれを説明しておくと下記のようなものがあります。

  • merge_requestmerge_response
    • recv() で取得したデータをマージして HTTP リクエスト/レスポンスのデータを作り、HTTP リクエスト/レスポンスとしての終端*2に達しているかを返す。
  • pic_target_fdreturn_target_fd
    • ターゲットとのコネクションに対応するソケットのファイルディスクリプタの取得/返却をする。
    • ターゲットとの新規コネクションを作成する処理は、pic_target_fd の中でうまい具合にやっている、と思ってください。
  • send_http_response
    • クライアントへ HTTP レスポンスを送信する関数で、クライアント側のファイルディスクリプタと返すコンテンツ、 HTTP ステータスを引数に取る。
    • この関数のエラーハンドリングは勘弁してください。
  • reset_socket_timer
    • ALB のアイドルタイムアウトを実現するタイマーをリセットする。

クライアントから接続された後の処理

クライアントから接続された後に呼ばれる関数をイメージした処理です。

void http_handling(int client_fd)
{
    unsigned char recv_buf[1024];
    unsigned char http_req_buff[1024 * 1024];
    unsigned char http_response_buff[1024 * 1024];
    ssize_t received_size, req_size, sent_size, sent_total;

    int end_of_turn;
    int error_in_send;
    int target_fd; 

    /* 
     * クライアントからの受信待ちループ (ブロッキング recv)
     * クライアント側から FIN が来ると recv は 0 を返し、このループを抜けます
     */
    while ((received_size = recv(client_fd, recv_buf, sizeof(recv_buf), 0)) > 0) {
        /* 受信したデータのマージとリクエスト終端チェック */
        end_of_turn = merge_request(http_req_buff, sizeof(http_req_buff),
                                                         recv_buf, received_size);
        if (!end_of_turn) {
            /* リクエストの終端に達していないので再び recv() へ */
            continue;
        }

        req_size = strnlen((char *)http_req_buff, sizeof(http_req_buff));
        sent_total = 0;

        /* ターゲットとのコネクションを保持しているソケットを取得 */
        target_fd = pic_target_fd();

        /* ターゲットへのリクエスト送信のターン */
        error_in_send = 0;
        while (sent_total < req_size) {
            /* 
             * ターゲットとのコネクションが閉じられていたら、
             * ここで初めて書き込みエラー(EPIPE等)が発生して気づく
             */
            sent_size = send(target_fd, http_req_buff + sent_total, 
                                       req_size - sent_total, 0);
            if (sent_size < 0) {
                perror("send() 中にエラー (ターゲットが既に切断していたなど)");
                error_in_send = 1;
                break;
            }

            sent_total += sent_size;
        }

        if (error_in_send) {
            /* ターゲットへの送信中にエラーが発生 */
            send_http_response(client_fd, "ターゲットへのリクエスト送信中にエラー", 502);
            continue;
        }

        /* アイドルタイムアウト用のタイマーをリセット */
        reset_socket_timer(target_fd);

        /* ターゲットからのレスポンス受信のターン */
        while ((received_size = recv(target_fd, recv_buf, sizeof(recv_buf), 0)) > 0) {
            end_of_turn = merge_response(http_response_buff, sizeof(http_response_buff),
                                                               recv_buf, received_size); 
            if (!end_of_turn) {
                continue;
            }
            break; /* レスポンスの受信完了 */
        }

        /* ターゲットからの受信中にエラー、または0(相手からの切断)が発生した場合 */
        if (received_size <= 0) {
            send_http_response(client_fd, "ターゲットからのレスポンス受信中にエラー", 502);
            continue;
        }

        /* 
          * 正常系:クライアントへレスポンスを返し、
          * アイドルタイムアウト用のタイマーをリセットし、
          * ターゲットのソケットをプールへ返却
        */
        send_http_response(client_fd, http_response_buff, 200);
        reset_socket_timer(target_fd);
        return_target_fd(target_fd);
    }
    
    if (received_size < 0) {
        perror("client recv() エラー");
    }
}

ポイントは、前回の記事でも書きましたが、

  • ターゲットへのリクエスト送信のターンで send() を実行したときに初めて切断されていることを知る。

です*3

アイドルタイムアウトで切断

先のコードで、ターゲットに対して送受信が完了したときにタイマーをリセットする処理を書いています。じゃぁ、タイマーの時間が来てソケットをクローズしに行く処理がどこかにあるはず、ですが、おそらくは HTTP の処理をやっているのとは別のスレッドでチェックして、ソケットのクローズとコネクションをプールしている情報からの削除を実施しているのではないかと思います。

実際のコネクションプールの仕組みはもっと複雑だったり、もうちょっと効率の良い手段を使っているかもしれませんが、簡単に実装すると、下記のような物になると思います。

/* 
 * メイン処理とは並行して(別スレッド等で)常に動き続けている 
 * コネクションプールの監視・タイムアウト処理のイメージ 
 */
void connection_pool_timer_watcher(void)
{
    const int IDLE_TIMEOUT_SEC = 60;
    time_t idle_time;
    int i, fd;

    while (1) {
        /* プール内にあるすべてのターゲット用ソケットをチェック */
        for (i = 0; i < max_pool_connections; i++) {            
            fd = connection_pool[i];            
            if (fd == -1) continue;            

            /* 最後に通信した時刻からの経過時間を計算 */
            idle_time = time(NULL) - get_socket_last_active_time(fd);
       
            if (idle_time > IDLE_TIMEOUT_SEC) {
                /* 経過時間がアイドルタイムアウトの時間を超えていたのでクローズ */
                close(fd);
                connection_pool[i] = -1;
            }
        }
        sleep(1);
    }
}

まとめ

実際の ALB の動作を実装しているコードはもっと複雑だと思いますが、おそらくは Amazon Linux の上に ALB として動作する機能を実装しているのではないか、と考えると、使われていないソケットを整理するときに、ターゲットより先にクローズしたい、という意図が見えてくると思います。

ALB も魔法の箱ではなく、普通に OS 上で動くプログラムだと考えると、ドキュメントに書かれた注意書きの意味が見えてくる事があります。

*1:個人的に最近は Python で書く事が多いし、Python の方が読める人は多いと思うのですが、OS カーネルとユーザーランドの話をしているときに余計な抽象化を挟みたくない、と思って C 言語にしました。もっとも、Python の socket は、ほとんど C 言語とのインタフェースと違わないですが。

*2:連続する2つの改行(改行コードは CRLF)が終端になります。

*3:send() にはもう一つトラップがあって、送信したデータが OS カーネルのバッファに入ってしまえば、送信側としては正常に送信したことになります。なので、受信側のプログラムが正常に動作しいなくても、受信側のプログラムに問題が発生していることを知るのは、送信完了後に recv() している時、ということもあります。

AWS ALB のアイドルタイムアウトをターゲットより短くする理由 - TCP の切断をいつ知るか

前回、「アイドルタイムアウト」の意味をきちんと考えると、「クライアント側の方がタイムアウトを長くする」という話と一緒にしてはいけないことを書きました。

ところが、話の発端である

  • ALB のアイドルタイムアウト
  • ターゲットのアイドルタイムアウト

は、同じ「アイドルタイムアウト」の話です。

HTTP Keep-Alive

そもそも、ALB とターゲットのの間でコネクションを保持する仕組みは、HTTP Keep-Alive の仕組み、そのものです。

デフォルトでは ALB とターゲットの間は HTTP/1.1 のプロトコルで通信します。HTTP/1.1 では、

  • クライアント側が "Connection: keep-alive" ヘッダを付けてリクエストする
  • サーバー側がその要求に答える場合は、"Connection: keep-alive" ヘッダを付けてレスポンスを返す。
    • サーバー側が HTTP Keep-Alive を拒否する場合は "Connection: close" というヘッダを付けて返し、サーバー側から TCP コネクションを切断する。

となっていて、双方が望めば TCP コネクションはレスポンスを返した後も保持されます。

TCP とソケットインタフェース

TCP を使った通信プログラムを書くとき、「ソケット」と呼ばれるプログラミングインタフェースを使います。時々、「ソケット」をまるでプロトコル名であるかのように書いている記事を見かけますが、「ソケット」自体は、各種データストリームを扱うための UNIX 系 OS 発祥のプログラミングインタフェースであって、「ソケット」を使っているからといって、必ず TCP の通信プログラムであるわけでもありません。なんなら、ネットワークも無関係なこともあります*1

ソケットを使って TCP の通信プログラムを書くと、OS とソケットインタフェースとの責任境界で、思わぬ落とし穴にハマる事があります。

これは、必ずしもソケットだけでなくファイルディスクリプタを使った読み出しに使われる read(2) の Manpage での記述ですが、戻り値の解説にこう書かれています。

Man page of READ

成功した場合、読み込んだバイト数を返す (0 はファイルの終りを意味する)。 ファイル位置はこの数だけ進められる。 この数が要求した数より小さかったとしてもエラーではない; 例えば今すぐには実際にそれだけの数しかない場合 (ファイルの最後に近いのかも しれないし、パイプ (pipe) や端末 (terminal) から読み込んでいるかもしれない) や read() がシグナル (signal) によって割り込まれた場合にこれは起こりえる。 

(傍線筆者)

若かりし頃、バイナリーデータを TCP で送受信するプログラムを書いていたとき、read が戻ってきたときに期待するデータが揃っていることは保証されないことを指摘され、「UNIX ネットワークプログラミング」*2という分厚い本の脚注に書いてあることを指摘されたのを覚えています。

ユーザーランドのプロセスには、切断されたタイミングが分からない

ソケットインタフェースを通じて接続・切断、データの送受信をするわけですが、実際のパケットの動きは OS 側の仕事です。「ここに接続して」などと OS に「お願い」をしているだけで、実際の処理は OS 側で、ユーザーランドのプログラムからは見えません。

そのため、

  • 一度、接続した TCP コネクションが、相手から切断されたことを知るのは、データを送受信しようとした時。

という事が起きます。

で、受信しようとしているときは、相手から切断されたことをすぐに知る事ができます。

データを受信しようとする処理は普通、ブロッキングモードで「データが届くまで待たされる」状態になります。データが届くまではユーザーランドのプログラムはその場所で止まった状態になります。

このときに、相手側からコネクションを切断された場合、ブロック状態が解除されユーザーランドのプログラムが動き出し、切断されたことを知る事ができます。

具体的には recv() を呼び出してブロッキング状態に入り、戻ってきたときの戻り値によって、切断されたことを知ります。

受信待ちで切断されたときの動き

 

このことを知っていると、ALB がターゲットより先にタイムアウトしたい理由が分かります。

2つのコネクション

ALB は

  • クライアントとのコネクション
  • ターゲットとのコネクション

の2つのコネクションの間で通信の仲介者となって処理をします。

ロードバランシングなどの複雑な話をすべて取っ払って単純化すると、

  1. クライアントとのコネクションから HTTP リクエストが届くのを待つ。
  2. リクエストが届いたら、ターゲットとのコネクションへリクエストを送信する。
  3. ターゲットとのコネクションからレスポンスが届くのを待つ。
  4. レスポンスが届いたら、クライアントとのコネクションへレスポンスを送信する。

というサイクルを繰り返します。

先の4つのステップで一番長い時間はどこかというと、1. のステップでしょう。2. ~ 4. は、障害がなければスムーズに進み 1. に戻ります。

この時、

  • クライアントとのコネクションは受信待ち
  • ターゲットとのコネクションは、単に保持しているだけ

という状態になります。

このときにターゲットから切断されると、下図のようになります。

クライアントからの受信待ち時にターゲットから切断

ターゲットから切断された時の ALB の動き

ターゲットから切断されると、ALB の OS カーネルとしては

  • 保持していた TCP のコネクションは既に切断されている*3

ですが、ユーザーランドのプログラムは、

  • 保持していた TCP コネクションのソケットが切断されていることは知らない。

という状態になります。このときにクライントからリクエストが届いたとすると、

  1. 保持したコネクションを使ってターゲットにリクエストを送信する。
  2. 送信したことで、そのコネクションがすでに切断されていることを知る。
  3. しょうがないから、クライアントに 502 Bad Gateway を返す。

ということになります。3. のところはもう少し頑張って、新規にターゲットとのコネクションを作って、という戦略も考えられますが、本当にターゲットに障害があったり、キャパシティの問題があったり、というケースを考えると、シンプルにクライアントに 502 を返す方がリカバリーが効く、という判断ではないかと妄想します。

送信のターンにある方から切断するとスムーズ

TCP は全二重の通信が可能なので、極論、同時に双方がデータを送信しながら受信もする、みたいなことは可能ではありますが、多くの場合は、ある方向にデータを送信し、それを受信した方が次に応答を送信する、という、会話をするプロトコルになっています。HTTP も基本的にはリクエスト・レスポンスの繰り返しです*4

ソケットが切断されているかどうかは、送受信するタイミングにならないとわからない、ということは、送信のターンにある方から切断した方が、受信のターンにある相手方に切断された事が伝わりやすい、ということになります。

これを ALB とターゲットの関係に当てはめると、接続状態で待機しているときは、

  • ALB はクライアントからのリクエストが届いたらターゲットに送信する立場
  • ターゲットは ALB からのリクエストを受信が届くのを待っている立場

なので、

  • ALB が送信のターン
  • ターゲットが受信のターン

ということになります。

よって、ALB 側から切断した方がスムーズ、ということになり、ALB のアイドルタイムアウトはターゲットより短くすることで、送信のターンにある方からの切断を起きやすくしている、ということになります。

次回は、実際に ALB の動作を **妄想** して、コードレベルに落としてみます。

*1:同一ホスト内でソケットファイルを介してプロセス間通信をする UNIX ドメインソケットというものがあります。

*2:W. Richard Stevens の著名な翻訳本で、その第一版。厚さ4センチぐらいだったかなぁ。

*3:厳密には、FIN に対して ACK を返して CLOSE_WAIT の状態。

*4:HTTP のパイプライニングのように、応答を待たずに連続してリクエストを送信する仕組みはあります。ALB は、クライアントとの間ではパイプラインに対応していますが、ターゲットに対しては対応していません。

AWS ALB のアイドルタイムアウトをターゲットより短くする理由 - 誤解されがちな種類の違うタイムアウト

「タイムアウト」の一般論

一般に、処理をあきらめて中断するまでのタイムアウト時間は、

  • クライアントに近い方が長く
  • バックエンドの奧に向かって短く

と言われます。

その原則から考えると、AWS ALB のアイドルタイムアウトはターゲットとなる Web サーバーのタイムアウト時間より長くなるのが自然、と思いがちです。

しかし、AWS の公式ドキュメント中にこう書かれています。

Application Load Balancer の属性を編集する - Elastic Load Balancing

アプリケーションのアイドルタイムアウトは、ロードバランサーに設定されたアイドルタイムアウトよりも大きな値に設定することをお勧めします。そうしないと、アプリケーションがロードバランサーへの TCP 接続を正常に閉じない場合、ロードバランサーは、接続が閉じられたことを示すパケットを受信する前に、アプリケーションにリクエストを送信することがあります。この場合、ロードバランサーは HTTP 502 Bad Gateway エラーをクライアントに送信します。

ALB の方がターゲットよりクライアントに近い位置にあるので、タイムアウトの大小関係が逆転しているように読めます。

このことを伝えると「いや、タイムアウトはクライアント側が長くなるのが常識だ」と言って譲らない人に出会う事があります。公式ドキュメントに書いてある事を示しても、頑なに否定する人もいます。

この「ALB アイドルタイムアウト < ターゲットのアイドルタイムアウト((Apache httpd だと KeepAliveTimeout、nginx だと keepalive_timeout。))」の理解を妨げている理由として、大きく2つの理由がありそうです。

1つ目は「タイムアウト」という言葉に対する解像度です。

「アイドル」タイムアウト

勘の良い人はここで気づくでしょう。

もう一度、AWS の公式ドキュメントの文章を引用すると、

アプリケーションのアイドルタイムアウトは、ロードバランサーに設定されたアイドルタイムアウトよりも大きな値に設定することをお勧めします。

「アイドル」タイムアウトと書かれています。

この文の前には、

接続アイドルタイムアウトとは、ロードバランサーが接続を終了する前の、既存のクライアントまたはターゲット接続が非アクティブのままでデータの送受信が行われない時間のことです。

とあります。ここで言っているタイムアウトは「非アクティブのままでデータの送受信が行われない時間」、裏返すと、

  • データの送受信が行われない状態がこの時間続けばコネクションを切断する

ということです。

ALB とターゲットの間をつなぐ TCP コネクションは、ターゲットがレスポンスを返した後も切断せずに接続状態を保持します。いわゆる、コネクションプーリングのような挙動をします。

この ALB ・ターゲット間のコネクションにデータが流れていない状態が続くと、無駄なコネクションだとして切断します。この切断するまでの時間が「アイドルタイムアウト」です。

一方、「クライアント側ほどタイムアウトを長くする」と言っているときのタイムアウトは、リクエストに対する応答のタイムアウトです。

いま仮に、1つの HTTP リクエスト・レスポンスだけを考えると

  • 「クライアント側に近づくほ長く」は下図のクライアントがリクエストを送信して ALB からレスポンス返却を受けるまでの各ステップでのタイムアウトの話。
  • アイドルタイムアウトは、下図の「TCPコネクション確立(Keep-Alive有効)」から「TCP FIN (コネクション切断)」までの話

であり、2つは別のタイムアウトの話になります。

HTTP リクエストからアイドルタイムアウトまでの流れ

でも、全く無関係ではなく。もし、アプリケーション・サーバーで処理が長引き、ALB のアイドルタイムアウトが先に来てしまうのは避けたい。

レスポンス前にアイドルタイムアウトが発生

なので、

  • ALB アイドルタイムアウトは、クライアントへレスポンスを返すまでのタイムアウト時間より長くすべき。

となります。ここでもクライアント側のタイムアウトより ALB 側のタイムアウトが長いように見えます。

しかし、「クライアント側の方が...」という原則をここに当てはめるのはナンセンスです。逆転しているように見えるのは、「2つの異なるタイムアウトを一緒くたにしている」からそう見えるだけで、タイムアウトの中身を考えれば、当然の結果と言えます。

この例の登場人物でタイムアウトを考えた場合、各タイムアウトの長さは長い方から

  1. クライアントでのレスポンス待ちタイムアウト
  2. Web サーバーでのアイドルタイムアウト
  3. ALB のアイドルタイムアウト
  4. Web サーバーが応答開始するまでのタイムアウト
  5. アプリケーション・サーバーがレスポンスするまでのタイムアウト

となります。

タイムアウトの中身をきちんと考える

「アイドルタイムアウト」というものがどういうものなのか、きちんと中身を考えれば、「クライアント側に近い方のタイムアウトを長く」という方法論を杓子定規に適用してはいけない事が分かります。

ところで、出発点となった ALB とターゲットの関係は、ともに「アイドルタイムアウト」です。同じアイドルタイムアウトが、なぜ ALB 側の方を短くすべきか、OS やその上で動くユーザーランドプログラムの動きから、次の記事で説明します。

パスワード付き ZIP ファイルの話

パスワード付き ZIP ファイルの脆弱性が、といった話が、話題になっていたようです。

2019年5月18〜19日のtwitterセキュリティクラスタ – twitterセキュリティネタまとめ

どうやら、

  • 通称: Traditional PKWARE Encryption の脆弱性の話らしい。
  • もともと、Traditional PKWARE Encryption は、内部状態の 96bit を総当たりすれば解けてしまうもので、これが実質的な鍵長になる。
  • 96bit じゃ、今どきの暗号としては、圧倒的に短い。
  • ということは昔から指摘されていたけど、今回は、そこから、パスワードを生成することに成功したらしい。

のようです。

意外に、この「古い暗号化」を知らない人が多いみたいですが、私は、下記ページを読んだことがあったので、暗号化アルゴリズム自体が、古臭くてイケてない、という認識はありました。

暗号化 ZIP 書庫 の評価 - EverQuestできない日記

これが書かれたのが 11 年前。私自身がこの記事を読んだ知識を元に言及している記事を、2009 年に書いていました。

前にも書いたけど... - JULY’s diary

あれから 10 年。未だに、パスワード付き ZIP ファイルを使う習慣は駆逐されていません。

結局、パスワードをメールで送るんだから、意味がない、という文脈で批判されるのですが、そもそも、古い暗号化方式でのパスワード付き ZIP ファイルは、仮にパスワード運用をきちんとやっていても、既に復号化されやすい、非推奨の形式でした。

 

Office 系のファイルや PDF ファイルなどを、パスワード付き ZIP ファイルにする人を見かけますが、ZIP にしてパスワードを付けるぐらいなら、Office 自体でパスワード付きにする方が遥かにマシで、こっちは、パスワードさえきちんとしていれば復号化される心配は、ほとんどありません。バージョンにもよりますが、暗号化は基本的に、AES を使っています。

 

このパスワード付き ZIP ファイルの問題。Windows での ZIP ファイルの扱いが影を落としています。

 

ZIP ファイルの暗号化方式に関しては、すでに AES で暗号化するオプションが存在します。無料で使用できるソフトウェアとしては、7-zip にもそのオプションがあります。

 

圧縮・解凍ソフト 7-Zip

 

ところが、Windows の圧縮フォルダとして、パスワード付き ZIP ファイルを扱えるのは、先述の「Traditional PKWARE Encryption」だけです。

 

パスワード付き ZIP ファイルが流通している背景には、

  • Windows の標準機能で中身を取り出せる。

というのがあります。その Windows の標準機能では、AES での暗号化をした ZIP ファイルは扱えない。

 

 

まぁ、もともと、パスワード付きファイルをメールに添付して送って、結局、メールでそのパスワードを伝える(しかも、パスワード自体も分かりやすいもの)ということ自体が「気休め」にしかすぎないのだから、いまさら、あれこれ言っても、という面もありますが、社内のガイドラインに「添付ファイルを送る時は、パスワードを設定した ZIP ファイルにして...」と書かれているケースも多いでしょう。

 

せめて、Office 系ファイルは、ZIP ファイルにはせずに、Office でパスワードを設定する、としていれば、「気休め」よりは、ちょっとだけまともになると思いますが...

パスワード防御に対する風評

元記事は、期間限定で無料で読める記事だから、はてブのページを示しておく。

 

はてなブックマーク - 「ハッシュ化したから安全」と主張するのをそろそろやめようか | 日経 xTECH(クロステック)

 

元記事の内容は、パスワードをハッシュ化しているから大丈夫、というのは言いすぎだ、という内容で、それはごもっともなのだが、後半にかけて、いかに危険なのかを煽る内容になっていた。

 

大雑把に言えば、「ハッシュ化していたとしても、パスワードが判明することがある」ということを主張しているのだが、攻撃手法と防衛手法がきちんと整理されていない印象だった。

 

パスワード・クラッキングの手法には、下記のようなものがある。

 

逆引き

ハッシュ化された値が分っている「オフライン・クラッキング」の場合に可能な手法で、平文のパスワードと、ハッシュ化後の値との組み合わせをデータベース化したものを用意し、それを検索することで元のパスワード知る方法。

普通にやると、そのデータベースが簡単に巨大になってしまうので、この目的のための効率的なデータ構造として登場したのが、いわゆる「レインボーテーブル」。

この手法の特徴は、他の手法に比べて圧倒的に短い時間でパスワードが判明すること。ただし、

  • ハッシュ化のアルゴリズム毎にデータを用意する必要がある。
  • レインボーテーブルで容量を小さくできる、と言っても、パスワードが1文字増えれば、用意すべきデータは2桁ぐらい大きくなるので、長いパスワードに対応しようとすると、必要なリソースが簡単に大きくなってしまう。

というのが弱点。

辞書攻撃

人間がつけそうなパスワード文字列を準備・生成し、その文字列に対してハッシュ化の計算をしたり、実際に対象システムに送信する。

いわゆる「脆弱なパスワード」は高確率で元のパスワードを発見することができるが、本気でランダムな長いパスワードは、そもそも試行されないことになるので、発見されることは、基本的には無い。

どのくらいの確率でパスワードが判明するかは、辞書がどれだけ豊富なのか、ということになるが、最近は、すでに漏洩されているアカウント情報が広く流通していることを考えると、人間がつけやすい、だけじゃなく、一度、誰かが設定したパスワードに合致、というケースもあるかもしれない。

総当たり攻撃

この世のすべての文字列を試す、というもの。原理的には、どんなパスワードでも発見できるが、パスワード長がちょっと長くなると、あっという間にこの攻撃は破綻する。ASCII 図形文字と半角スペース合わせて 95 種の文字が使えるとしたら、n 文字のパスワードは 95n通りとなる。実際に計算して見れば分かるが、10 文字でも 59,873,693,923,837,890,625 通りに達する。1文字増えれば、95 倍になる。

一世を風靡した Ophcrack も、基本的にはレインボーテーブルを使うが、4文字までは ASCII 文字の総当たりを行う。

 

オフライン・クラッキングに対して、防御方法としてソルトとストレッチングがあるが、ソルトは上記の攻撃のうち、逆引きに対抗するものであり、辞書攻撃や総当たり攻撃には効果はない。逆に、ストレッチングは、ストレッチングの回数やアルゴリズムが分かっている場合、逆引きには効果は無い*1

  • ソルト ... 逆引き対策
  • ストレッチング ... 辞書攻撃や総当たり攻撃に対する対策

 なので、ソルト付きでストレチング済みであれば、簡単に元のパスワードが判明することはないのだが、元記事で、

ただ両方の工夫を適用しても、解析に時間がかかるようになるだけだ。解析できなくなるわけではない。

と書かれた。

この文章だとまるで、ソルト付きストレッチングが気休めのように読めてしまうが、実際はそんなことはない。きちんとソルトがついていれば逆引きは実質的に不可能だし、ストレッチングをしていれば、仮に、1時間で判明するパスワードが、1,000 回のストレッチングで 42 日後になる。パスワードを変更して漏洩の被害を回避する時間を作ることができる*2

 元記事で自分が一番気になったのは、上記の文章に続いてこう書かれていたことだった。

例えば、漏洩データの検索サービスを提供していたLeakedSourceが2016年9月、ソルトを採用していたサービスから漏洩した4350万件以上のアカウント情報を2時間で96%解析したとしている。

ソルト付きのパスワードで 4000 万件以上のパスワードが 2 時間で判明、という事に強烈な違和感を感じた。ソルト付きであれば、

  • ソルトがアカウントによらず固定
  • その固定のソルトが付いているケースのテーブルが整備済み

じゃないと、2時間で 4000 万件、毎秒 5,500 件以上のというのは、クラウドのおかげで計算リソースの調達が容易になったご時世でも、現実的な数字とは思えない。

 

記事中には、この LeakedSource の話のソースが示されていないが、おそらくこれではないか、というページを見つけた。

leakedsource.ru

これが正しいとすると、

  • 元記事で 2016 年 9 月と書いているが、上記ページが書かれたのが 2016 年 9 月であって、話の内容は 2012 年 3 月。
  • 「4350 万件」「96%」「2時間」と言っているのは、Last.fm から漏洩したもの。
  • 「Passwords were stored using unsalted MD5 hashing.」と書いているから、ソルトなしの MD5 ハッシュ値
  • 得られたパスワードを見たら、なんと安易なパスワードの多いこと。

という話になる。

安易なパスワードが多くて、ソルトなしの MD5なら*3、普通にレインボーテーブルで短時間にパスワードが見つかるのは当然である。

元記事では「ソルトを採用していた」と書かれているが、私が見つけたページが正しいとすれば、大きな事実誤認ということになる。故に、この話をもって「ソルト付きストレッチングは気休め」と風潮するのは間違いと言わざるを得ない。

 

ちょっと話は変わるが、この件のブクマコメントで、徳丸さんが

BCryptでもArgon2でも通常Saltはハッシュ値とともに保存するので辞書攻撃には対抗できないでしょ。

と書かれている。辞書攻撃で判明してしまうパスワードは、どんな形式保存しても見つけられてしまう。脆弱なパスワードに対しては、ソルトもストレッチングも気休め言わざるを得ない。

システム側が頑張ったところで、脆弱なパスワードを付けたユーザを守ることはできない。保存形式を適切に保てば、きちんとしたユーザを守ることはできる。

 

ということで、皆さん、

  • 十分に長いパスワードで
  • 使い回しをしない

という原則を守りましょう。

*1:もし、同じストレッチング回数とアルゴリズムのレインボーテーブルが無いなら、それを作らなければいけないし、ストレッチングしていればテーブル作成に時間がかかる、という効果はある。

*2:逆に、漏洩したことをどれだけ迅速に検知できるか、が重要になってくる

*3:MD5なら」というのは、MD5 の衝突耐性の問題ではなく、ビット長が比較的短く、メジャーなハッシュ関数だから、すでにレインボーテーブルが充実している、という理由。任意のハッシュ値に対して、そのハッシュ値を持つビット列を求めるのは、MD5 でも、そこまで容易ではない。MD5 が使えないのは、ハッシュ値は何でも良いから、同じハッシュ値を持つユニークなビット列を見つける、という強衝突耐性の問題。

Path MTU Discovery が原因の通信障害は分りづらい

普通、何らかの通信障害が発生すると、当然、通信の当事者に関する事を調べ出します。

サーバ側に何かログは出ていないか、パケットキャプチャをして相手からどんなパケットが届いているのか、Firewall 等で当該ホストに関するルールが引っかかっていないか....

ところが、Path MTU Discovery に起因する問題が発生すると、通信の当事者では、一見、相手が悪いように見えるし、実際、原因が当事者とは無関係のように見えるところにあります。

通信障害の最終手段として登場*1するパケットキャプチャをしても、単に、相手からのパケットが届かないだけに見えます。

そもそも、Path MTU Discovery とは?

まず、MTU

フレッツ網が出始めた頃、ネットワークスピードの改善方法として MTU のサイズを調整する話で有名になった MTU ですが、Maximum Transmission Unit の略で、1つのフレームで送信できる最大サイズの事を言います。

元々、MTU 自体は回線の種別によって決まるもので、一般的な Ethernet では 1500 バイトで、WAN 回線で使われる PPP の場合は可変ですが、Ethernet と同じ 1500 バイトになる事が多いようです。今はほとんど見かけない FDDI では 4352、IP over ATM で 9180 バイト、といった具合に、本来は様々な MTU を持つ回線があります。

フレッツ網が登場したときに MTU が話題になったのは、PPPoE というカプセル化技術を使ったためで、1500 バイトの Ethernet フレームの中に、ユーザ側の端末と契約先の ISP をつなぐ為の PPP を入れ、さらにその中に IP を入れる、という仕組みです。

そのため、通常の Ehternet に比べて PPP の分だけ、入れられる IP のサイズは小さくなります。

Path MTU とは

「Path」は「経路」を意味します。相手にパケットが届くまで、いくつものルータを渡っていきますが、その間の回線がどんな物が使われているか分りません。

ある相手と通信する時に、その経路全体での MTU が Path MTU です。

つまり、経路全体で、最も小さい MTU が Path MTU です。

Path MTU Discovery は、経路全体で最も小さい MTU をどうやったら分るか? という仕組みです。

ルータによるパケットの分割

ルータが受け取ったパケットを転送する際、転送先となる回線の MTU が受け取ったサイズより小さかった場合、パケットを分割して送ることがあります。

こうすれば、通信当事者は経路上の MTU など意識することなく、パケットを送信できますが、その代わり、ルータに取っては負担になります。

Path MTU Discovery の RFC 1191 が出されたのは 1990 年ですが、1990 年代に入ってから、ISP のルータのような、多くのネットワークとつながって、大量のパケットを裁くルータにかかる負荷が問題になっていました。

時期的に次期 IP(後の IPv6)の策定が行われていて、次期 IP のテーマの一つに、このルータの負荷問題もありました。

経路上の MTU が分ってしまえば、最初っからそれに合わせたサイズでパケットを送り出すことになるので、ルータでの分割は発生しません。

そのため、IPv6 では Path MTU Discovery が使われる事が前提になりました。

Path MTU Discovery の仕組み

もともと、IP パケットには DF(Don't Fragment)フラグというフラグがあり、このフラグがセットされているパケットは、ルータによる分割をしてはならない、ということになっています。

ルータがより小さい MTU を持つ回線を使ってパケットを転送しようとした時に、「分割できない」という事は、これ以上、パケットを転送する事ができない、という事を意味します。

こういった事態が発生していることを、パケットの送信元に伝えるために ICMP が使われます。

Type 3(Destination Unreachable Message)の Code 4(fragmentation needed and DF set)という ICMP がルータから送信元に送られ、この中に「○○○バイトだったら転送できるんだけど...」という情報が入っています。

送信元がこれを受け取ったら、そのサイズに合わせて IP パケットを再送します。

最終的に相手にパケットが届くまで、ICMP の情報に合わせて小さくしていけば、経路上の最小 MTU が見つかる、という仕組みです。

ICMP がフィルタされると Path MTU Discovery が成立しない

Path MTU Discovery は、パケットが分割できないときにルータが返す ICMP を利用して実現しています。ということは、もし ICMP がフィルタされている*2と、

  • ルータはこれ以上転送できないから、受け取ったパケットを捨てて ICMP を返す。
  • 送信元から見ると、送ったパケットに対して、相手から一切応答がない状態。

ということになります。

IPv6 では Path MTU Discovery が使われるので、ICMP をバッサリと Firewall 等で落としてしまうと、通信できない事態が発生する可能性があります。そのため、わざわざ RFC で、ICMPv6 のフィルタリングに関する文章が出されています。

RFC 4890: Recommendations for Filtering ICMPv6 Messages in Firewalls

考えなしにまるごとフィルタリングをしてはいけません。

実は IPv4 でも同じことが...

IPv6 の話だと思って、「そんなの使っていないし」と思っていると落とし穴があります。Path MTU Discovery 自体は IPv4 でも使える仕組みなので、原理的には同じことが発生しうることになります。

実は、割と以前から Linux では IPv4 でも Path MTU Discovery が有効になっていて、そのことに私が気がついたのは5年ほど前の CentOS Ver.6 系でした。CentOS 上で Squid を動かした HTTP Proxy を経由すると、繋がったり繋がらなかったり、みたいなことがあって、そのときにパケットキャプチャをして DF フラグが立っていることに気が付きました。

LinuxIPv4 の Path MTU Discovery が有効になっているかは sysctl コマンドで分ります。

$ sysctl net.ipv4.ip_no_pmtu_disc
net.ipv4.ip_no_pmtu_disc = 0

ちょっと分りづらいですが、ip_no_pmtu_disc なので、値が 0 なら有効です。

LinuxIPv4 の Path MTU Discovery が有効ということは...

今や Linux はサーバ以外にもいろいろなところで使われています。という事は、明確に「Linux」と認識していないものでも、Path MTU Discovery が有効になっている、ということになります。ネットワーク機器や IoT デバイスにも Linux は使われています。

私が実際に経験したのは、AWS のロードバランサである ELB です。

ELB に対する接続が VPN 経由だと NG、という事があったのですが、「ELB の実体って、Amazon Linuxインスタンスとかじゃないかなぁ」と思って、ICMP をブロックしていた Network ACL の設定を変更したら解消した、という事がありました。

IPv6 に限らず IPv4 に関しても、いたずらに「ICMP を丸ごとブロック」をするべきではありません。

Ping of Death*3Smurf 攻撃*4は、正直、過去の物です。ICMP に対して過大な恐怖心を持たず、フィルタするなら ICMP の「中身」も意識したいものです。

*1:個人的には、レイヤー 3 以下が原因になりそうな症状で、当事者が Linux だったら、最初っから tcpdump の登場させますが。

*2:Firewall 製品だと、意図的にブロックしない限り、許可している通信に対して発生した ICMP は適切に転送してくれる事が多いです。L3 スイッチの ACL のように、ステートレスにパケットを判断する場合、フィルタされてしまいがちです。

*3:Windows 95 が登場した頃、UNIX 系 OS がサイズの大きい ICMP Echo Request を送りつけられると、カーネルパニックに陥る欠陥があった。

*4:宛先がブロードキャストアドレスとなっている ICMP Echo Request に対して、該当するホストが応答する動作利用し、送信元アドレスを攻撃対象のアドレスに偽装してパケットを送りつけると、その何倍ものパケットが攻撃対象に向かう、という DDoS 攻撃の一種

せきゅぽろ 22

今回は、立命館大学の上原哲太郎先生による、マイナンバーのお話でした。

マイナンバーの話となると、

  • マイナンバーを管理するはめになった企業向けの話。
  • プライバシーに関するマイナンバー制度の問題。

といった2つのパターンのが多いと思いますが、もちろん上記の点も含め、非常に広範囲の話を聞くことが出来ました。

Slideshare で今日の資料が公開されています。

今さら聞けないマイナンバー

詳細は上記の資料に譲るとして、印象に残った話を少し書きます。

国民総背番号制」はすでに実施済み

厳密に言うと、住基ネットに参加しなかった一つの自治体を除けば、ですが、住民票があれば、住基ネットで使われている住民票コードが付与されているので、「国民総背番号制」という意味では、マイナンバーで導入されるものではないです。

言われてみれば、そうだよなぁ、という話なのですが、逆に、住基ネットがうまく活用出来なかったことを示唆している感じがします。

チェックデジットがダサい

マイナンバーの最後の数字はチェックデジットなのですが、計算式で得られる計算結果は 1 〜 11 の整数。チェックデジットは一桁の数字。で、どうしているかというと、

「計算結果が 10 以上の時は 0」

だそうです。つまり、末尾が 0 の人は、他の数字に比べて2倍の確率で存在することになります。

ちなみに、この計算式は官報にも載っていて、で、官報は縦書き、ということで、計算式がどう書かれているのか、気になる人はチェックしてみましょう*1

システムの仕組みは結構複雑

Slideshare 資料にもありますが、単純に中央のデータベースがあって、そこにアクセスするような仕組みではなく、基本的には、各自治体が組織ごとにデータベースがあって、中央では必要に応じてひも付けをするような仕組みだそうです。

なので、マイナンバーのシステムによって、現状の住基ネットよりセキュリティリスクが特に高くなるようなことはない感じです。

マイナンバーによるプラバシー問題の本質は「名寄せコストの減少」

個人情報の漏洩、ということがニュースで大きく取り扱われるご時世ですが、本当に問題なのは、各種のプライバシー情報が「名寄せ」されることによって、芋づる式に個人関する情報が取得できてしまうことで、マイナンバーは、その「名寄せ」のコストを劇的に下げる効果があります。

故に、マイナンバーの民間利用に関しては慎重になる必要があり、民間が使いたいと言っている「目的」に対しては、フェデレーションのような別の「手段」が適当ではないか、といったお話がありました。

マイナンバーは「アカウント」

マイナンバーはあくまで「識別」するための番号であって、「認証」に利用するものではなく、いわば「アカウント」のようなもの、という説明は分かりやすかったです。アカウントだけでは本人かどうかは判断できないのと同様に、マイナンバーの番号だけで本人性を確認できるわけはない、ということになります。


全体的に、メディアが煽るようなリスク拡大はないように思えます。ただ、上原先生もおっしゃっていましたが、今後、民間活用を含めた利用拡大が検討されているので、その部分で注視していく必要はありそうです。

*1:私も後で探してみます ... ありました! http://www.soumu.go.jp/main_content/000327387.pdf