朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

朝日ネットにおけるソフトウエア・ロードバランサーの導入(第5回)

朝日ネットでのインフラ設計・構築を担当しているラピー・ステファンです。 前回の冗長構成・拡張可能な設計の説明に続き、今回は運用負担を軽減する便利な設定について紹介します。

第1回 第2回 第3回 第4回

ログ形式の設定

既定のログ形式

HAproxyの既定のログ形式は、送信元IPアドレスと宛先IPアドレスしか記載しない簡易的なものですが、 HTTPフロントエンドの場合「option httplog」を使うことが出来て、内容がいくらか豊富になります。

下記が含まれます:

  • HTTPリクエスト
  • 接続関連のタイマー
  • セッション状態
  • 接続の件数
  • キャプチャーしたヘッダーとクッキー
  • フロントエンド、バックエンド及びサーバー名
  • 送信元IPアドレスとポート

つまり、「option httplog」は下記のログ形式の指定に相当します:

log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"

( HAproxy資料 を引用)

運用的な観点で、集計しておきたい統計情報

暗号化通信がここ数年で爆発的に普及し、それにまつわるベスト・プラクティスも大きく変化しました。

ブラウザーにおいても、脆弱な組み合わせを検知した場合、ユーザーにその安全性の懸念を明確に表示する様になったため、 サービスプロバイダーもまたそれに追随する必要が出てきます。

この記事で紹介している構成においては、SSL機能はすべてpfSense・HAproxyで一元管理しています。 したがって、pfSenseの更新をするだけで、脆弱性対応や新機能の追加対応が可能です。

さらに、対外的なサービス毎にpfSenseのインスタンスを分けることで、それぞれのサービス毎の要件設計も可能になります。

なお、バックエンドでも対外的なサービスのためのSSL通信を考慮しなければならない場合、 アプリケーションが稼働するOSとSSLライブラリの相性関係や回帰テストを考慮する必要があり、 脆弱性対応並びに新プロトコルバージョンへの対応がより一層複雑になります。

こうした複雑な側面も、SSL通信の処理をpfSenseに一元化することで大幅に緩和され、脆弱性対応もしやすくなります。

そこで、もう一つ考慮しなければならないのは対外的なサービスを利用するユーザーの利用実態です。

把握しておきたい項目:

  • SSLのバージョン(いわゆる、SSLv2, SSLv3, TLSv1.0, TLSv1.1, TLSv1.2)
  • SSLサイファー(暗号方式)
    • とても古い事例: RC4-MD5, RC4-SHA (2019年の現在では、RC4暗号化並びにMD5・SHAハッシュ方式が脆弱指定されている)
    • まあまあ古い事例: DHE-RSA-AES256-SHA (2019年の現在では、SHA1ベースのハッシュ方式が脆弱指定されている)
    • 一般的に安全とされているものの事例: ECDHE-RSA-AES256-GCM-SHA384
    • 最新のものの事例: TLS_CHACHA20_POLY1305_SHA256

朝日ネットでは、下記の様にlog-formatを駆使して、SSLのバージョンとサイファーを取得しています。

そのために、フロントエンドの定義のAdvanced SettingsのAdvanced pass thruに下記を追加しています:

log-format "%ci:%cp [%t] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs SSL:{%sslv,%sslc} %{+Q}r"

具体的な事例

そうなれば、syslog等でログを集めた先で、利用統計はこのように確認出来ます:

  • ログのサンプル:
$ bzgrep haproxy service-lb0[12].bz2 | head
service-lb01.bz2:Sep 18 00:00:01 service-lb01 haproxy[39217]: 1.2.3.4:63421 [18/Sep/2019:00:00:01.762] ServiceHTTP ServiceHTTP/ 0/-1/-1/-1/0 301 93 - - LR-- 4/1/0/0/0 0/0 {|} "GET / HTTP/1.0"
service-lb01.bz2:Sep 18 00:00:02 service-lb01 haproxy[39217]: 2.3.4.5:41930 [18/Sep/2019:00:00:01.895] ServiceHTTPS-merged~ ServiceHTTPS_ipvANY/app-server01 511/0/2/4/519 200 37991 - - --NI 4/4/0/1/0 0/0 {service1.example.jp|} SSL:{TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384} "GET /my/url HTTP/1.1"
  • SSLバージョン別統計:
$ bzgrep SSL: service-lb0[12].bz2 | sed 's/.*SSL:{\([^}]*\)}.*/\1/' | cut -d ',' -f 1 | sort | uniq -c | sort -n
      7 TLSv1.1
   5973 TLSv1
 777262 TLSv1.2
  • SSLバージョンとサイファー別統計:
$ bzgrep SSL: service-lb0[12].bz2 | sed 's/.*SSL:{\([^}]*\)}.*/\1/' | sort | uniq -c | sort -n
      7 TLSv1.1,ECDHE-RSA-AES256-SHA
     53 TLSv1.2,DHE-RSA-AES256-SHA
     54 TLSv1.2,DHE-RSA-AES256-SHA256
    815 TLSv1,DHE-RSA-AES256-SHA
   1194 TLSv1.2,ECDHE-RSA-AES256-SHA384
   1721 TLSv1.2,ECDHE-RSA-AES256-SHA
   5158 TLSv1,ECDHE-RSA-AES256-SHA
  10160 TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256
  81324 TLSv1.2,DHE-RSA-AES256-GCM-SHA384
 682756 TLSv1.2,ECDHE-RSA-AES256-GCM-SHA384
  • TLSv1.0を利用したユーザー(送信元IPアドレス)の数:
$ bzgrep "SSL:{TLSv1," service-lb0[12].bz2 | cut -d ' ' -f 6 | cut -d : -f 1 | sort | uniq -c | sort -n | wc -l
165

たとえば、これによりTLSv1.0を利用したユーザーが165人であることが分かります。 更に詳しく分析をすれば、対外的なサービスの正規利用者なのか、脆弱性スキャナーの類かどうかも区別出来ます。

そして、その分析結果に基づき、二次的な対応を検討することが可能です

  • 「もう、TLSv1.0, TLSv1.1に対応する必要がない、設定に no-tlsv10 no-tlsv11 を追加する」
  • 「TLSv1.0の正当な利用者がいれば、注意喚起をしてから拒否設定を追加する」

ヘッダーやクッキー情報の取得

朝日ネットでは、上記ログ形式の指定に加えて、 下記の設定もAdvanced SettingsのAdvanced pass thruに加えています:

  • ホストやリファラー情報の取得:
capture request header Host len 256
capture request header Referer len 256

これにより、クエリの具体的な向き先や前後のアクセスパターンが分かります。

  • 同様にTomcat系のアプリケーションもクッキーを取得することが出来ます:
capture cookie JSESSIONID len 63
  • アプリケーションによって、認証が成立したことで返答文のヘッダーとして返すこともあります:
capture response header ConnectionId len 32

アプリケーションを考慮したログ回収で、関連付け及び調査がしやすくなりますので、是非お勧めします。

クライアント⇔バックエンドの対応付けの維持について

クッキーに触れてきましたが、アプリケーションによっては同じセッションは引き続き同じバックエンドに継続処理させなければならない要件があります。

第2回 でstickyセッションの話に軽く触れましたが、 具体的な設定方法やその詳細について今から説明します。

クッキーによるsticky設定

一度アプリケーションがクッキーを付与してくれたら、中間にあるHAproxyが下記の挙動を取ります:

  • クッキーの値と、対応したバックエンドサーバーの関連付けをメモリ上で保存する
  • 次に同じクッキー値を提示したクライアントは送信元IPアドレスとは関係なく、同じバックエンドサーバーに振り分けられる

設定方法:

  1. pfSense上のHAproxyの設定画面で、バックエンドの設定を開きます
  2. バックエンドのStick-table persistence
    • Stick tables : Stick on existing Cookie value (既存のクッキー値で追跡する)
    • Stick cookie name : JSESSIONID (Tomcatアプリケーションの事例の場合)
    • Stick cookie length : 4096 (記録されるクッキーの最大長;Tomcatの場合は64で足りることが一般的だが、アプリケーションの要件・仕様次第)
    • Stick-table expire : 30m (更新がなかった場合のセッション失効時刻;これもアプリケーションの要件・仕様次第)
    • Stick-table size : 1m (これはアプリケーション設計の問題で、同時に最大何人受け付けるか、という要件次第)

最終的にこういうコンフィグに展開される:

stick-table type string len 4096 size 1m expire 30m
stick store-response res.cook(JSESSIONID)
stick on req.cook(JSESSIONID)

もちろん、これはバランス方法の代わりではないので、ラウンドロビンやIPハッシュなどのバランス方法の設定を忘れてはいけません。

挙動は:

  • sticky情報がなければ、既定の方法(IPハッシュ、ラウンドロビン等々)に基づいて振り分ける
  • sticky情報があれば、優先的に採用される
    • ただし、指定のバックエンドがダウンな場合はその限りではない

クッキーによるバックエンドサーバータグ付け設定

上記のクッキー設定は、同じクライアントから同じpfSenseにすべてのアクセスが集約した場合、有効な方法ですが、 複数台のpfSense体制においては、stickyテーブルが共有されないので、 障害により別のpfSenseに振り分けられた場合、既存の対応付が分からず、 ノード間でセッションの共有がなってないアプリケーションの場合、いきなりセッションがリセットされます。

現代のHAproxyの対応について

pfSenseでまだ設定可能な機能として取り込まれていないが、数台のHAproxy間でstick-tableの情報共有の機能が導入されています:

https://rascaldev.io/2018/08/08/load-balancing-for-high-availability-with-haproxy/

リアルタイム共有しなくても理解出来る情報

そうなれば、既に接続実績のあるクライアントに情報を渡して、 その情報はどのpfSenseノードがもらっても理解出来る様にすれば、 上記の問題は解決出来ます。

それでバックエンドを定義する際、固定値の「Cookie」値を設定することが出来るが、無駄な情報を渡す恐れがあります。

例えば、ノード1からNまで、「s1」から「sN」設定すれば、分かりやすすぎるということですし、 複雑な文字列に設定するにしても管理手間が発生しては元も子もありません。

設定方法:

  1. pfSense上のHAproxyの設定画面で、バックエンドの設定を開きます
  2. バックエンドのCookie persistence

    • Cookie Enabled : x
    • Cookie Name : _app_component_backend
    • Cookie Mode : Insert
    • Cookie Options :
      • Max idle time : 30m
      • Max life time : 24h
    • Cookie dynamic key : app-prod-custom-key-2019 (そこは本番系・開発系、アプリに応じて鍵を変えて下さい)

最終的にこういうコンフィグに展開される:

cookie _app_component_backend insert nocache maxidle 30m maxlife 24h dynamic
dynamic-cookie-key app-prod-custom-key-2019

そうすることで、Statsページをみていただくと、IPアドレス・ポート番号等の固定識別情報が鍵で暗号され、 意味が理解出来ない文字列になります:

  • 1号機の場合 → _app_component_backend: 3de7cb61aaabbb9e
  • 2号機の場合 → _app_component_backend: 0ce5165a074dd9c6
  • 3号機の場合 → _app_component_backend: 95facc53eebd836f

つまり、その設定を共有したpfSense・HAproxyであれば、リアルタイムでsticky情報を共有しなくても、 例えばクライアントの送信元IPアドレスが変わったり、pfSenseが障害で切り替わったりしても、 設定した「_app_component_backend」のクッキーの値を解読して、指定のバックエンドに振り分けてもらうことが可能です。

もちろん、そのロジックはバランス方式より優先的に採用されます。

つまり、クライアントとバックエンドの経路が如何に変わっても、最終的に到達すべきサーバーに到達する設計です。

特別なTCPベースのアプリケーションの対応事例

あるアプリケーションで、今まで紹介してきた事例と異なりますが、 HAproxyの柔軟性をアピールするために紹介します:

  • プロトコルはHTTPではなく、つまりHAproxyにとってTCPアプリケーション
  • TCPであるため、X-Forwarded-For等の付加情報を使って送信元IPアドレスを盛り込む事が出来ない
  • そのアプリケーションサーバーが位置するネットワークにおいて、別のゲートウエイが既に存在していた
  • 仮想基板上でCARPを使うと不便なため、pfSenseをゲートウエイに据えることが出来ない

送信元IPアドレスを保つtransparent設定・接続の管理の工夫

こういったケースに対応するために、バックエンドの設定画面にこちらの設定が存在します:

Use Client-IP to connect to backend servers

設定をすると、コンフィグに下記が現れます:

source ipv4@ usesrc clientip

pfSenseは一旦接続を受けて、SSL等の高度な処理を実施した後、バックエンドに接続するが、送信元IPアドレスを書き換えます:

   CLIENT         L4 LB         pfSense         BACKEND
  10.2.3.4 → 192.168.0.10 → 172.16.0.11 (SSL)
                              172.16.0.11 → 172.16.0.101 (TCP ; 通信元を改ざんして、 10.2.3.4と見せかける)

pfSense (172.16.0.11) 上のnetstat -anの出力はつまり、このように見えます:

$ netstat -an | grep EST
tcp4       0      0 10.2.3.4.63794         172.16.0.101.8997      ESTABLISHED (改ざんされた接続)
tcp4       0      0 172.16.0.11.18997      10.2.3.4.48688         ESTABLISHED

BACKEND (172.16.0.101) 側では、netstat -anはこうなります:

$ netstat -an | grep 10110
tcp6       0      0 :::8997                 :::*                    LISTEN     
tcp6       0      0 172.16.0.101:8997       10.2.3.4:63794          ESTABLISHED (クライアントのIPアドレスが直接見える)

ですがそうなると、バックエンドの方で応答するために自分のルーティングテーブルに基づいて、デフォルトゲートウエイから出します。 そうなると、pfSenseがデフォルトゲートウエイでなければ、接続が成り立たないわけです。

ルーティングの工夫

つまり、IPアドレスであるL3情報に基づいてルーティングするのではなく、パケットが来た同じL2ネットワークにあるデバイスへ戻す必要があります。 スイッチやルーターの機種によっては、そういった特別設定も可能ですが、 今回はフリーソフトウエアのみに依存した、朝日ネットが取った手段の紹介をします。

IPアドレス:

  • バックエンド : 172.16.0.101
  • pfSense LB01 : 172.16.0.11 (MAC : 00:50:56:aa:bb:cc)
  • pfSense LB02 : 172.16.0.12 (MAC : 00:50:56:dd:ee:ff)

Linuxのiptablesとrouteツールで下記が可能です。 ip route, ip ruleコマンドでポリシーベースルーティングを設定出来ます:

  • /etc/iproute2/rt_tablesに追加のテーブルを定義する
101 lb01
102 lb02
  • ポリシーの定義は、下記コマンドを実行する:
# ip route add 172.16.0.0/16 src 172.16.0.101 table lb01
# ip route add default via 172.16.0.11 table lb01
# ip rule add fwmark 100 table lb01
# ip route add 172.16.0.0/16 src 172.16.0.101 table lb02
# ip route add default via 172.16.0.12 table lb02
# ip rule add fwmark 101 table lb02

そして、特定のMACアドレスから来たパケットにmark(印)を付けることが出来ます:

# iptables -t mangle -A INPUT -p tcp -m state --state NEW --match mac --mac-source 00:50:56:aa:bb:cc --jump CONNMARK --set-mark 100
# iptables -t mangle -A INPUT -p tcp -m state --state NEW --match mac --mac-source 00:50:56:dd:ee:ff --jump CONNMARK --set-mark 101
# iptables -t mangle -A OUTPUT --jump CONNMARK --restore-mark

こうすることで: 1. LB01 から接続が来る → iptablesはその接続に印「100」を付ける 2. 印「100」が付いたパケットは、lb01というルーティングテーブルに振り分けられる 3. lb01のデフォルトルートに基づいて、LB01にパケットが返される

上記コマンドをネットワーク起動時の設定に仕込めば、この要件でもSSL接続の終端と負荷分散が可能です。

もちろん、注意点としては:

  • ロードバランサーの台数の変動に気をつけなければならない
  • IPアドレス及びMACアドレスの変化に追随出来なければならない

最後に

さて、ここで仮想基盤上のpfSense / HAproxyを使ったソフトウエア・ロードバランサーの導入における利点や欠点等の考慮事項と便利設定を紹介してきましたが、いかがでしたでしょうか?

読者の皆様が今後便利に活用していただければ嬉しく思います。