GCP Internal HTTPS LB は Firewall が効かない話

# はじめに
**GCP の Firewall 制御って、実はほぼ GCE インスタンスにしか効かない** という話、皆さんご存知でしょうか？
`CloudSQL`, `MemoryStore`、そしてなんと `Internal HTTPS LB` もそうなんです。
※External HTTPS LB は `Cloud Armor` という別の機能でIP制御が可能です。

# Firewall が効かないとはどういう事か？
まず先に、公式サイトは[こちら](https://cloud.google.com/load-balancing/docs/l7-internal)です。
読めば雰囲気はわかると思いますが、Firewallが本当に効かないのか？あたりはすごいちゃんと読み込まないとわかりません。私が捉えているイメージを簡単に図にするとこんな感じです。
※正確にはbackend groupとかも挟まりますが、一旦オンプレ目線で行きます。
<img width="600" alt="Internal LB FW Problem.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/9e436a8d-ba66-4e73-96b4-4d8cbae93b8b.png">

普通に構成を組むと、クライアントのIPで制御することが出来ないんですよね。

# 対処法について
これを何とかする方法はたくさんありますが、私が思いつく限りはこんな感じですかね？

|No |方法 |おすすめ度 |概要|
|---|---|---|---|
|1  |諦めてExternalにする|★★★|最も簡単。上策だと個人的には思います。GCP内同士なら、Googleの専用線が使われる。いわゆるInternetには出ない。|
|2  |TCP LBを使う |★|HTTPS終端はアプリで行い、GCPレイヤでは分散のみ。証明書管理がアプリ側になる。SSLが必須でなければコレでOK。|
|3  |リバプロを使う |★|HTTPS LBを作り、リバプロを置いて通信制御し、アプリに分散させる。SSLが必須でどうしても証明書管理をGCPレイヤでやりたい場合はコレを使うしかない。|

GCPにおいては、そもそもIaaS的な設計をそのまま適用しようとするとどうしても不整合が出てきます。
なので、普通にInternet Overで暗号化してCloud Armorで制御する方が、本質的な事の検討（アプリの機能など）に時間を割けると思っています。

# あえてリバプロで制御してみる
さて、今回紹介する方法は、あえて全部の要件を吸収できるリッチな案の上記No.3の『リバプロ案』です。
以下の様な要望が求められる場合に採用することになります。

* 会社のセキュリティポリシー上、社内システム同士はプライベートIP での通信が **Must** である。
    * オンプレの接続元から、インターネットに出れないケースも含まれます。
* 社内からの通信だが、秘匿性の高い通信を行うため、特定の IP のみに明確にフィルタリングを掛けたい。
* 社内からの通信だが、秘匿性の高い通信を行うため、暗号化をしたい（HTTPSを使いたい）。
* だが、アプリケーション側では秘密鍵の管理や復号化に伴う性能などの課題から、どうしてもSSL終端はしたくない。

# コアとなる実装方法
[こちらの公式サイト](https://cloud.google.com/load-balancing/docs/features#ip_addresses)を確認しましょう。
重要な部分を囲いました。
<img width="600" alt="2020-11-29 214942.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/d84bbb78-5224-a328-a9f4-02aa40aa4a0f.png">

**X-Forwarded-For ヘッダーでクライアントの送信元IPアドレスが保持される**、とありますね。

* X-Forwarded-Forヘッダとは、主にプロキシサーバが付与するヘッダです。
* プロキシにアクセスしてきたクライアントIPを保持するために使われます。
* （Webサーバに届くパケットの Source IP が Proxy の IP になってしまう構成の場合で、Client IP で何らかの制御をしたい時に使われる事がある）
* 細かくは、[Appendix](https://qiita.com/ys_nishida/private/dc57862f18865750d8e6#appendixx-forwareded-for-%E3%83%98%E3%83%83%E3%83%80%E3%83%BC%E3%81%A8%E3%81%AF) の方に記述します。

# アーキテクチャ
それじゃあ、みんな大好きアーキテクチャです。
<img width="900" alt="ilb_reverse_proxydrawio.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/4624faa3-a394-ba59-0d9b-d2d79b7f8120.png">

# 実装
検証構成としては以下です。リバプロの後段は作っても良かったんですが、とりあえず見たい主眼は `x-forwarded-for` ヘッダで制御できるかどうかなので、余計な部分は極力カットしています。
<img width="700" alt="Architecture of testing.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/f5620d59-0653-59c7-4c8b-56d8bde7f8d6.png">

使用したコードの全量は[こちら](https://github.com/ys-nishida/qiita-https-ilb-xff)に置いておきました。
コアとなるパラメータだけ、下記に記載します。

```リパプロのnginx.conf
～～～略～～～
        # LB からのヘルスチェック用。ヘルスチェックにはx-forwarded-forが含まれないので、分けておく。
        location /health {
            return 200;
        }

        location / {
            if ( $allowed = "deny" ) {
                return 403;
            }
            proxy_http_version 1.1; # GCP は HTTP 1.0 は非対応なので。
            proxy_pass http://127.0.0.1:32080$request_uri;
        }
～～～略～～～
```

```conf.d/allow_ip_map.conf
map $http_x_forwarded_for $allowed {
    default deny;
    ~192\.168\.0\..*$ allow;
}
```

# 結果
curl を打った結果と、リバプロでのパケットキャプチャの結果を書きます。

#### allowed からのアクセス
```
[user@client-allowed ~]$ curl https://10.0.0.3 --insecure
<h3>Hello World!</h3>
```

#### denied からのアクセス
```
[user@client-denied ~]$ curl https://10.0.0.3 --insecure
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.14.1</center>
</body>
</html>
```

#### リバプロ側
```
[root@rproxy-nginx nginx]# cat proxy_access.log
172.16.0.13 - - [06/Dec/2020:14:10:52 +0000] "GET / HTTP/1.1" 200 22 "-" "curl/7.61.1" "192.168.0.2,10.0.0.3"
172.16.0.10 - - [06/Dec/2020:14:14:38 +0000] "GET / HTTP/1.1" 403 169 "-" "curl/7.61.1" "192.168.128.2,10.0.0.3"
```
<img width="600" alt="ppt_allowed.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/d03c293f-8488-1bb3-deee-eac0da57a1a7.png">
<img width="600" alt="ppt_denied.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/598801/767fb147-949d-cda1-8ae7-3dcc2e6f79d3.png">

# 考察
冒頭に申し上げた通り、ここまでやれば、ほぼどんな要望でも対応する事が出来ます。
`nginx` のフィルタ設定変更なども、`Jenkins` を使うなどすればある程度自動化も可能です。
が。。。この路線って、『Firewall機能やルーティング機能なども全部含めて Appliance on GCE で構成すればよい』という方向性の解決方法なので、クラウドネイティブではないですし、コスト効果も低く、頑張る箇所は別のところだと考えています。
本記事を読み、この実装が手間である事を理解してもらえたら幸いです。

# Appendix：X-Forwareded-For ヘッダーとは？
私は前職で、多段Proxy構成を作った事があり、その際、後段のProxyサーバのログに本当にアクセスしてきたクライアントIPを残すために、このX-Forwarded-For ヘッダを使っていました。その際も色々と苦労したんですが、こんな感じです。

- RFCには一応規定されている
- が、解釈がベンダによってまちまち
    - 例えば最たるはその順番。
    - 何段か Proxy を経由している場合は、自分のところに来た時にすでにこのヘッダがつけられている
    - ので、Append するんですが、それを先頭に Append するのか、最後に Append するのかは、製品によって挙動が違う。
- そもそもヘッダ自体を付与しない or 付与されているヘッダに追加しない Proxy も存在する
- もっと言うと、削除する事も出来るし、任意のアドレスを付けたりも出来る。（例えば `squid` とかで。。）
- なので、インターネットから来たパケットについている X-Forwarded-For ヘッダはそんなに信ぴょう性がなく、実際にはこれで運用は出来ない。

従って、基本的には今回の例の様に、ヘッダを付与する方と、そのヘッダを使う方の双方が自分の管理下にある構成の時しか、使えないものです。