リバースプロキシが動かない

問題名

リバースプロキシが動かない

概要

あなたはある会社で先輩に次のように言われました。

ちょっと前、WebアプリがHTTPで通信してて危険だって言われてたから、リバースプロキシを挟もうと思ったんだよね。検証環境では動いていたから、そのコンフィグをそのまま実環境に上げてみたんだけど、動かないんだよね。なんかよく分からないんだけどデバッグお願いできる?

この先輩の悩みを解決してあげてください。なお、先輩の机の上に残されたメモから、この実環境について次のような事が分かっている。

  • 先輩はHAProxyを用いてリバースプロキシを構築している
  • 先輩が残した検証環境のconfigは/etc/haproxy/haproxy.cfgにある
  • 検証環境と実環境では同じパスに証明書が配置されている

前提条件

  • HAProxyを経由せずに問題を解決してはいけない
  • この問題を解くために踏み台サーバに手を加えてはいけない
    • 問題を解析するために何かをインストールするのは良いですが、踏み台には手を加えずに問題を解くことができます

初期状態

  • serverに対してcurlをしても応答が返ってこない
% curl https://192.168.4.1
curl: (7) Failed to connect to 192.168.4.1 port 443: Connection refused

終了状態

  • 踏み台サーバからserverに対し、実環境のドメイン名を使ってcurlすることができる
    • この時、insecureオプションをつけずにリクエストができる
% curl https://...
<!DOCTYPE html>
<html>
...

接続情報

VM名ホスト名ユーザパスワード
server192.168.4.1userictsc2020

解説

まずは、前提条件を確認します。踏み台サーバに入ってcurlを実行してみると、次のようなエラーメッセージが返されます。

% curl https://192.168.4.1
curl: (7) Failed to connect to 192.168.4.1 port 443: Connection refused

192.168.4.1にSSHをしてサービスの状態を確認してみます。すると、なんらかの理由でHAProxyが起動していないことが分かります。

user@server:~$ sudo systemctl status haproxy
● haproxy.service - HAProxy Load Balancer
   Loaded: loaded (/lib/systemd/system/haproxy.service; enabled; vendor preset: enabled)
   Active: failed (Result: exit-code) since Fri 2021-02-26 18:31:03 JST; 21h ago
     Docs: man:haproxy(1)
           file:/usr/share/doc/haproxy/configuration.txt.gz
  Process: 1240 ExecStartPre=/usr/sbin/haproxy -f $CONFIG -c -q $EXTRAOPTS (code=exited, status=1/FAILURE)

Feb 26 18:31:02 server systemd[1]: haproxy.service: Control process exited, code=exited status=1
Feb 26 18:31:02 server systemd[1]: haproxy.service: Failed with result 'exit-code'.
Feb 26 18:31:02 server systemd[1]: Failed to start HAProxy Load Balancer.
Feb 26 18:31:03 server systemd[1]: haproxy.service: Service hold-off time over, scheduling restart.
Feb 26 18:31:03 server systemd[1]: haproxy.service: Scheduled restart job, restart counter is at 5.
Feb 26 18:31:03 server systemd[1]: Stopped HAProxy Load Balancer.
Feb 26 18:31:03 server systemd[1]: haproxy.service: Start request repeated too quickly.
Feb 26 18:31:03 server systemd[1]: haproxy.service: Failed with result 'exit-code'.
Feb 26 18:31:03 server systemd[1]: Failed to start HAProxy Load Balancer.

/var/log/haproxy.logを確認すると、以下のようなログが出力されていることが分かります。

Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:17] : unknown keyword 'HA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA2' in 'global' section
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:18] : unknown keyword '56:DHE-RSA-AES256-GCM-SHA384' in 'global' section
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:19] : unknown keyword 'ssl-default-bind-ciphersuites' in 'global' section
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:39] : 'bind *:443' : unable to load SSL private key from PEM file '/srv/ictsc2020/server.pem'.
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : Error(s) found in configuration file : /etc/haproxy/haproxy.cfg
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : Fatal errors found in configuration.

1つ目の問題は以下の部分です。実際に設定ファイルを見てみると、よく分からない部分で改行文字が入っています。先輩が検証環境からコピーした際に、何も見ずにコピーをしたせいで不要な改行文字が残ってしまったようです。

[/etc/haproxy/haproxy.cfg:17] : unknown keyword 'HA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA2' in 'global' section
Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:18] : unknown keyword '56:DHE-RSA-AES256-GCM-SHA384' in 'global' section

2つ目の問題は以下の部分です。設定ファイルにはssl-default-bind-ciphersuitesという設定項目がありますが、この設定項目が解釈できていないようです。検証環境から本番環境に設定を持ってきた際に消し忘れたであろう設定のようです。

Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:19] : unknown keyword 'ssl-default-bind-ciphersuites' in 'global' section

この設定項目についてのドキュメントを見ると、この設定はHAProxyがOpenSSLのサポートをつけてビルドされている必要があると書かれていますが、問題の環境ではそのようになっていないため発生した問題だと考えられます。

This setting is only available when support for OpenSSL was built in and
OpenSSL 1.1.1 or later was used to build HAProxy.

https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#ssl-default-bind-ciphersuites

3つ目の問題点は以下の部分です。HAProxyではSSLの証明書や秘密鍵を1つのファイルにまとめて保存をする必要があります。この問題の環境では証明書と中間証明書、秘密鍵が独立して保存されています。そのため、これらのファイルをくっつけて保存する必要があります。想定解法では、 chain.pem, server.keyの中身をserver.pemの末尾にくっつけることでこの問題を解決することができます。

Feb 27 16:07:19 server haproxy[901]: [ALERT] 057/160719 (901) : parsing [/etc/haproxy/haproxy.cfg:39] : 'bind *:443' : unable to load SSL private key from PEM file '/srv/ictsc2020/server.pem'.
user@server:/srv/ictsc2020$ ls -al
total 28
drwxr-xr-x 3 root root 4096 Mar  1 15:09 .
drwxr-xr-x 3 root root 4096 Dec 30 15:54 ..
-rw-r--r-- 1 root root 1586 Mar  1 15:09 chain.pem
-rw------- 1 root root  306 Mar  1 15:07 server.key
-rw-r--r-- 1 root root 1631 Mar  1 15:08 server.pem
-rw-r--r-- 1 root root  244 Dec 30 15:55 webapp.service
drwxr-xr-x 2 root root 4096 Dec 30 15:55 www

これらの問題を解決することでHAProxyを起動することができます。

最後に、エラーメッセージには出てきませんが、次のような問題が存在します。検証環境では、gwn.localというドメインを用いて証明書の検証を行っていたようなのですが、/srv/ictsc2020/server.pemに保存されている証明書はCN=gwn.2020-final.ictsc.netとして発行されています。そのため、下の設定のままでは正しくリクエストをハンドリングすることはできません。

        acl is_gwn_local hdr_end(host) -i gwn.local
        use_backend backend if is_gwn_local
user@server:/srv/ictsc2020$ openssl x509 -in server.pem -noout -text | grep CN
        Issuer: C = US, O = Let's Encrypt, CN = R3
        Subject: CN = gwn.2020-final.ictsc.net

これらの問題を修正した設定は以下のようになります。

global                                                                                                             
        log /dev/log    local0                                                                                     
        log /dev/log    local1 notice                                                                              
        chroot /var/lib/haproxy                                                                                    
        stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners                              
        stats timeout 30s                                                                                          
        user haproxy                                                                                               
        group haproxy                                                                                              
        daemon                                                                                                     

        # Default SSL material locations                                                                           
        ca-base /etc/ssl/certs                                                                                     
        crt-base /etc/ssl/private                                                                                  

        # See: https://ssl-config.mozilla.org/#server=haproxy&server-version=2.0.3&config=intermediate             
        ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
        # ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256   
        ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets                                                

defaults
        log     global
        mode    http
        option  httplog
        option  dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000
        errorfile 400 /etc/haproxy/errors/400.http
        errorfile 403 /etc/haproxy/errors/403.http
        errorfile 408 /etc/haproxy/errors/408.http
        errorfile 500 /etc/haproxy/errors/500.http
        errorfile 502 /etc/haproxy/errors/502.http
        errorfile 503 /etc/haproxy/errors/503.http
        errorfile 504 /etc/haproxy/errors/504.http

frontend websecure
        bind *:443 ssl crt /srv/ictsc2020/server.pem

        acl is_gwn_ictsc_net hdr_end(host) -i gwn.2020-final.ictsc.net
        use_backend backend if is_gwn_ictsc_net

backend backend
        server server 127.0.0.1:8080

実際にこの設定をロードし、踏み台から以下のようにリクエストを送信することで正しいレスポンスが返ってきます。

% curl https://gwn.2020-final.ictsc.net
<!DOCTYPE html>
<html>
<head>
        <title>Congraturations!</title>
</head>
<body>
        <h1>Congraturations!</h1>
</body>
</html>

講評

この問題は比較的易しい問題だったと思います。そのため、完答チーム数も多かったです。また、想定解から外れにくい問題でもあったので突飛な答えはあまり無かったと思います。多少回答が分かれたポイントとして、use_backendの部分があります。想定解では、aclの設定を書き換えてgwn.2020-final.ictsc.netだけを通すように設定するものでしたが、default_backendの設定を使っているチームもありました。今回の問題の制約ではどちらでも回答できるためどちらも正解としています。

また、最初は部分点を考えていなかったのですが、最後にいくつか不完全な回答が来たため部分点を与えています。

今回の問題とほとんど同環境の問題環境をVagrantfileで用意しているため、どのように構築されているかなどが気になる方はこちらのリポジトリを参考にしてみてください。

https://github.com/proelbtn/ictsc2020-problems-gwn

採点基準

  • 設定ファイル(/etc/haproxy/haproxy.cfg)の記述ミスに気づく(30%)
  • 設定ファイルの誤りを修正し、HAProxyを起動させられる(30%)
  • 踏み台サーバから curl https://gwn.2020-final.ictsc.net を実行し、証明書エラーを起こさずにHTMLをfetchすることができる(40%)