CloudflareをALBの前に配置してAzureAD認証してみた

CloudflareをALB(AWS)の前に配置して、CloudflareのAccessでAzureAD認証してみました。
どのクラウドでも実装できるはずなので、ちょっと認証かけたい時などなかなか便利です。

# 構成

![cloudflare.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/b7052257-98e6-cf69-a7ad-ffdce3ba33f2.png)

- Cloudflare以外からのアクセスは、ALBのセキュリティーグループでブロックします。
- 自分のCloudflare以外のCloudflareからのアクセスは、Lambdaでjwtを検証してブロックします。

# 必要なもの
- Cloudflareアカウント
- AWSアカウント
- ドメイン2個
    - ドメインA
    - ドメインB
- ドメインBに対しAWS ACMで取得したワイルドカード証明書1個

# 設定
1. Azure AD認証の対象リソースを作成する
2. Cloudflare DNSを設定する
3. Cloudflare Accessを設定する
4. CloudfraleのIPアドレスをALBのセキュリティグループに設定する
5. テストする

## 1. Azure AD認証の対象リソースを作成する

1. AWS側でALBのターゲット用のLambdaを作成する
2. ALBを作成する
    1. HTTPSのリスナーを作成しドメインBの証明書を紐づける
    2. ターゲットグループを作成して、Lambdaをターゲットに設定する
3. Route53に、ALBのエイリアスレコードをドメインBのサブドメインとして設定する

- jwtの検証は以下を参考にしました。
    - https://developers.cloudflare.com/access/setting-up-access/validate-jwt-tokens/
- Lambda作成時のハマりポイント
    - Amazon Linux上でpip installしてLambdaのパッケージをzipで固めます。
    - Amazon Linuxの現在のyumリポジトリのPythonのバージョンは3.6なので、Lambda側も3.6に合わせます。
    - Lambdaのパッケージをzipで固める時、".libs_cffi_backend"配下も忘れずに含めます。
    - Cloudflareからのjwtを検証するためALBからのヘルスチェックは失敗するので、ヘルスチェックを外します。


以下Lambda内で指定している値は、Cloudflareの設定後に確認します。

|Lambda内で指定している定数|値|
|---|---|
|CERTS_URL|CloudflareのAccessのLogin Page Domain＋"/cdn-cgi/access/certs"|
|AUDIENCE_TAG|CloudflareのAccessのAccess PoliciesのAudience Tag|

```requirements.txt
pyjwt
cryptography
cffi
```

```Lambda_function.py
import jwt
import json
import urllib.request

CERTS_URL = 'https://*******************/cdn-cgi/access/certs'
AUDIENCE_TAG = '**************************************'

def get_public_keys():
    headers = {
        'User-Agent': 'curl/7.61.1'
    }
    req = urllib.request.Request(CERTS_URL, headers=headers)
    with urllib.request.urlopen(req) as response:
        res = response.read()
    public_keys = []
    jwk_set = json.loads(res)

    for key_dict in jwk_set['keys']:
        public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_dict))
        public_keys.append(public_key)
    return public_keys


def validate(header):
    is_valid = False
    token = ''
    if 'cf-access-jwt-assertion' in header:
        token = header['cf-access-jwt-assertion']
    else:
        return is_valid

    keys = get_public_keys()

    for key in keys:
        try:
            decoded_token = jwt.decode(token, key=key, audience=AUDIENCE_TAG)
            # print(decoded_token)
            is_valid = True
            break
        except Exception as e:
            print(e)
            pass
    return is_valid


def lambda_handler(event, context):
    is_valid = validate(event['headers'])

    if is_valid:
        res = {
            "isBase64Encoded": False,
            "statusCode": 200,
            "statusDescription": "200 OK",
            "headers": {
                "Set-cookie": "cookies",
                "Content-Type": "text/html"
            }
        }
        
        res['body'] = """<html>
        <head>
        <title>hello world</title>
        </head>
        <body>
        <h1>Hello World</h1>
        </body>
        </html>"""
    else:
        res = {
            "isBase64Encoded": False,
            "statusCode": 403,
            "statusDescription": "403"
        }

    return res
```

## 2. Cloudflare DNSを設定する

Cloudflare側でドメインAを登録します。

<img width="464" alt="2_1_add_site.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/694db5a8-ca61-ee7c-42c7-1a193e3bed5c.png">

プランを選択します。

<img width="919" alt="2_2_plan.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/acfe043f-6c03-d048-b81c-8aa2c83ca66b.png">

ドメインAのレジストラで、Cloudfraleのネームサーバを設定します。
設定後、Cloudflareの画面に戻って、下記をクリックします。

<img width="281" alt="2_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/37ae9ca5-b504-e49c-e635-22c5f1e7fd0e.png">

Cloudflare側がドメインを認識するまで、数時間かかりました。

CNAMEレコードを登録します。

<img width="1042" alt="2_4_cname.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/a131bb41-c15c-9c71-c144-c831899dbd09.png">

|項目|値|
|---|---|
|Type|CNAME|
|Name|ドメインA|
|Content|ドメインBのサブドメイン(Route53に設定したALBのCNAME)|
|Proxy status|Proxied|

SSL/TLSの画面で、Fullを選択します。

<img width="1054" alt="2_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/e773f2dc-b2e5-5544-8e69-f6ce9c860d80.png">

ここまででの設定で、ドメインAにアクセスするとドメインBのサブドメインにproxyされてALBのターゲットのLambdaが呼ばれるようになります。

## 3. Cloudflare Accessを設定する

Cloudflare Accessを有効化します。

<img width="1070" alt="3_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/cf4cf253-423e-b57e-f596-28d0ee4370d3.png">

下記以外はデフォルトのまま設定します。

- プランはAzure ADを利用するため、Access Premiumを選択

<img width="824" alt="3_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/b98c6093-c61a-e739-c8f5-62174497f137.png">

Login methodでAzure ADを選択します

<img width="1043" alt="3_4_loginmethod.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/7eecc968-715b-f9d9-9229-dd799ed6b888.png">

Azure ADでアプリケーションを登録します。

<img width="1115" alt="4_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/7c83c2fb-f919-2063-19b0-86b311c38e4c.png">

クライアントシークレットを作成します。

<img width="1156" alt="4_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/d6dc2710-6dfd-0c40-0eb2-beeeeb7ecc43.png">

パーミッションを設定します。

<img width="1156" alt="4_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/10949f1e-9397-cbc2-ba86-bb251a82fdae.png">

Application IDとDirectory IDを確認します。

<img width="846" alt="4_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/910ebfb2-b712-d386-eb82-f38296622e68.png">


Azure ADで設定した値をCloud flare側で設定します。

<img width="442" alt="4_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/060c7a3d-a4e1-98f9-182b-91a784617807.png">

## 4. CloudfraleのIPアドレスをALBのセキュリティグループに設定する

下記IPアドレスをALBのセキュリティグループに設定します。
https://www.cloudflare.com/ips/

## 5. テストする

ドメインAに対しアクセスするとCloudflareの認証画面が表示されます。

<img width="354" alt="5_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/5e19af70-0d94-b82a-3879-f2e69a964736.png">

Azure ADで認証して…

<img width="453" alt="5_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/fca2078a-c1f7-a96e-59d1-f5c1bb62f451.png">

Lambdaで生成したページが表示されれば成功です!

<img width="327" alt="5_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192584/11613f48-7457-dfdf-0ed3-f6257f0b05f3.png">