LoginSignup
6
7

オリジンサーバーへの接続を Cloudflare だけにする

Posted at

Protect your origin server (Secure origin connections)

本家ドキュメントに記載のオリジンサーバーを守る方法(オリジンサーバへの接続を安全にする)を実際に試してみます。

レイヤー 方法 free,pro,business enterprise 本記事 Cloudflare からのみ接続可能 Cloudflare 自分のアカウントからのみ接続可能 備考
アプリケーション層 Cloudflare Tunnel (HTTP) - -
HTTP Basic 認証 - -
JSON Web Tokens(JWT)検証 - Eyeball-Cloudflare間の認証も必要
トランスポート層 Authenticated Origin Pulls ✓(Cloudflare証明書) ✓(持ち込み証明書) 証明書に 2 つの方式
(Cloudflare or 持ち込み)
ネットワーク層 Cloudflare IPアドレス 許可リスト - AWS VPC を例に
CNI - - - -
Cloudflare Aegis - - - -

はじめに

本記事の前提についてメモしておきます。

CLI/API で作業

IP アドレスや TLS 証明書などを接続元判別の材料として使うので、実際の運用となれば、定期的な更新作業が入ると思います。そのため、ダッシュボードではなく CLI/API を基本に操作します。

オリジンサーバーの IP アドレスは知られる前提で

オリジンサーバーへの接続を考える場合、オリジンサーバーの IP アドレスは大事な要素になります。
クラウド型のリバースプロキシーを中間に入れた際に、オリジンサーバーの IP アドレスを隠そうとする方策はありますが、そもそもパブリックにさらされるもなので、知られる前提でいきます。
なるだけ推測されないような手段を講じておくのが推奨ですが、知ろうと思えば、ツールもいろいろあります。下記は、Cloudflare でプロキシー済みなホストの DNS の履歴を SecurityTrials で参照し、 Cloudflare 変更前のオリジンサーバーの IP アドレスを見た例です。(Cloudflare 導入後もそのままなら直接接続できるかも知れません。なお、当時も今も他のホストと共有されていたり、今は違うホストに利用されいている可能性はあります。)

700

Cloudflare の他のアカウントからの接続

Cloudflare だけからオリジンサーバーに到達できるように保護しても、Cloudflare の他のアカウントから自分のオリジンサーバーに接続される余地は残ります。

Cloudflare 側でもいくつか対策はされています。
Origin Rules には、オリジンサーバーに接続する際の TLS SNI や HTTP host、宛先 IP アドレスを変更する機能があります(これらの書き換えは Enterprise プランのみ)。これを使えば、他のアカウントがあなたのオリジンサーバーに接続を試みる際に、あなたのドメインを SNI 指定し、正当な経路を通ったリクエストを偽装できるように思えます。
ただし、実際には他のアカウントのドメインへの SNI 指定はできないという保護機能が働きます。
400

また、Enterprise プラン以外でも Workers で fetchすれば、指定した fetch 先ホストに SNI と host は変更されるので、resolveOverride で自分のドメインを指定し、オリジンサーバーの IP を返せば偽装できそうに思いますが、この機能は fetch 先ホストが自分ゾーンの場合にのみ有効になるよう制限されています。

Directs the request to an alternate origin server by overriding the DNS lookup. The value of resolveOverride specifies an alternate hostname which will be used when determining the origin IP address, instead of using the hostname specified in the URL. ... However, resolveOverride will only take effect if both the URL host and the host specified by resolveOverride are within your zone. If either specifies a host from a different zone / domain, then the option will be ignored for security reasons. ...

これらの対策が効いている場合、オリジンサーバーが自身と異なる SNI や host を受け付けないようにしておけば、他のアカウントからの接続抑止対策の一つとなります。

ここからは、オリジンサーバーが自身以外の SNI や host も受け入れるという前提にして、どんな対策がとれるか、試していきます。

ネットワーク層で防ぐ

Cloudflare IP アドレス許可リスト

Cloudflare の IP アドレスからのみオリジンサーバーに接続できるようにします。
オリジンサーバーやその前段のファイアウォールで制御します。
700

オリジンサーバーに接続する Cloudflare の IP アドレス(v4/v6)は本家ページで公開していますが、JSON 形式 で取れるので、こちらを利用してみます。

Cloudflare IPs
IP の入手
~ $ curl -s "https://api.cloudflare.com/client/v4/ips" | jq '.result'
{
  "ipv4_cidrs": [
    "173.245.48.0/20",
    "103.21.244.0/22",
    "103.22.200.0/22",
    "103.31.4.0/22",
    "141.101.64.0/18",
    "108.162.192.0/18",
    "190.93.240.0/20",
    "188.114.96.0/20",
    "197.234.240.0/22",
    "198.41.128.0/17",
    "162.158.0.0/15",
    "104.16.0.0/13",
    "104.24.0.0/14",
    "172.64.0.0/13",
    "131.0.72.0/22"
  ],
  "ipv6_cidrs": [
    "2400:cb00::/32",
    "2606:4700::/32",
    "2803:f800::/32",
    "2405:b500::/32",
    "2405:8100::/32",
    "2a06:98c0::/29",
    "2c0f:f248::/32"
  ],
  "etag": "38f79d050aa027e3be3865e495dcc9bc"
}

変更されると etag が変わります。
また、過去の新規追加時は 30 日前に Email で通知されています。これからもおそらくそうでしょう。

初期状態(アクセス制限なし)

本家には iptables の例がありますが、今回は AWS EC2 にオリジンサーバーを用意しているので、VPC の Security Group でポリシーを適用します。

他のクラウドプラットフォームやオンプレミスのファイアウォールでも同じような方針でいけると思います。

まず、オリジンサーバーが動くインスタンスの NIC の パブリック IP と Security Group の ID を得ておきます。プロファイルは環境変数のほうに設定済みとします。

【AWS】インスタンスのパブリック IP と Security Group
~/.aws $ INSTNAME="<インスタンス名>”
~/.aws $ EC2IID=`aws ec2 describe-instances | jq -r '.Reservations[].Instances[]|select(.Tags[].Value == "'$INSTNAME'")|.InstanceId'`
~/.aws $ PUBIP=`aws ec2 describe-instances |jq -r '.Reservations[].Instances[]|select (.InstanceId == "'$EC2IID'")|.PublicIpAddress'`
~/.aws $ EC2SGID=`aws ec2 describe-instances |jq -r '.Reservations[].Instances[]|select (.InstanceId == "'$EC2IID'")|.SecurityGroups[].GroupId'`

初期状態としては TCP 443 宛の通信を、すべての IPv4 アドレスから許可しています。
Security Group の Port 443 の設定を確認します。

【AWS】TCP 443 全部許可
~/.aws $ aws ec2 describe-security-groups --group-id $EC2SGID | jq '.SecurityGroups[].IpPermissions[]|select (.FromPort == 443)'

## 応答抜粋
{
  "FromPort": 443,
  "IpProtocol": "tcp",
  "IpRanges": [
    {
      "CidrIp": "0.0.0.0/0"
    }
  ],
  "Ipv6Ranges": [],
  "PrefixListIds": [],
  "ToPort": 443,
  "UserIdGroupPairs": []
}

Cloudflare ではすでにプロキシー済みですので、この状態で接続を確認しておきます。
Cloudflare 経由・直接接続、どちらも接続可能です。

【Eyeball】接続テスト
~ $ MYHOST="<テスト対象ホスト>”

# Cloudflare Proxy
~ $ curl https://$MYHOST/
Hello World

# 直接接続
~ $ curl https://$MYHOST/ --resolve $MYHOST:443:$PUBIP
Hello World

/cdn-cgi/trace で情報が見れれば Cloudflare でプロキシー中の証です。

% curl -s https://$MYHOST/cdn-cgi/trace | grep colo
colo=NRT

アクセス制限開始

それでは、制限にかかります。
VPC の Managed Prefix List という機能が使えそうです。

まず、Cloudflare の API で得た IP 一覧を Prefix List に使えるように整形します。

【準備】Cloudflare IPv4 一覧を整形
# Cloudflare の IP リストを Prefix List 対応に整形
~/.aws $ CIDR=`curl -s "https://api.cloudflare.com/client/v4/ips" | jq '.result.ipv4_cidrs | map({Cidr: .})'`

# [{"Cidr":"x.x.x.x/x"},....] という形
~/.aws $ echo $CIDR
[
  {
    "Cidr": "173.245.48.0/20"
  },
  {
    "Cidr": "103.21.244.0/22"
  },
:

IPv6 も同様にやります。

【準備】Cloudflare IPv6 一覧を整形
~/.aws $ CIDR6=`curl -s "https://api.cloudflare.com/client/v4/ips" | jq '.result.ipv6_cidrs | map({Cidr: .})'`

~/.aws $ echo $CIDR6
[
  {
    "Cidr": "2400:cb00::/32"
  },
  {
    "Cidr": "2606:4700::/32"
  },
: 

作成したデータを元に Prefix List を作ります

【AWS】Prefix List 作成 v4 と v6
# IPv4 Prefix List 作成
~/.aws $ aws ec2 create-managed-prefix-list \
    --address-family IPv4 \
    --max-entries 30 \
    --entries $CIDR \
    --prefix-list-name CfCidr

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv4",

# IPv6 Prefix List 作成
~/.aws $ aws ec2 create-managed-prefix-list \
    --address-family IPv6 \
    --max-entries 15 \
    --entries $CIDR6 \
    --prefix-list-name CfCidr6

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv6",
        
# 内容を確認する場合、それぞれの Prefix List ID を得る
~/.aws $ PLISTID=`aws ec2 describe-managed-prefix-lists | jq -r '.PrefixLists[]|select (.PrefixListName=="CfCidr")|.PrefixListId'`
~/.aws $ PLISTID6=`aws ec2 describe-managed-prefix-lists | jq -r '.PrefixLists[]|select (.PrefixListName=="CfCidr6")|.PrefixListId'`

# 作ったものの確認
~/.aws $ aws ec2 get-managed-prefix-list-entries --prefix-list-id $PLISTID
~/.aws $ aws ec2 get-managed-prefix-list-entries --prefix-list-id $PLISTID6

max-entries を適当に 100 とかにしてたら、Prefixの上限に引っかかって進めず。

An error occurred (RulesPerSecurityGroupLimitExceeded) when calling the AuthorizeSecurityGroupIngress operation: The maximum number of rules per security group has been reached.

回避にはこちらを参考にしました。

Prefix List をインスタンスの Security Group Ingress に追加し、既存の全部許可のルールは削除します。

【AWS】Prefixl List を Security Group に追加
# Ingress Permission に Cloudflare IP TCP 443 を追加
~/.aws $ aws ec2 authorize-security-group-ingress --group-id $EC2SGID --ip-permissions PrefixListIds="[{PrefixListId=$PLISTID},{PrefixListId=$PLISTID6}]",IpProtocol=tcp,FromPort=443,ToPort=443

## 応答抜粋
    "SecurityGroupRules": [
        {
            "SecurityGroupRuleId": "sgr-*",
            "GroupId": "sg-*",
            :
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "PrefixListId": "pl-*"
        },
        {
            "SecurityGroupRuleId": "sgr-*",
            "GroupId": "sg-*",
            :
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "PrefixListId": "pl-*"
        }
    ]

# Ingress Permission から Any TCP 443 を削除
~/.aws $ aws ec2 revoke-security-group-ingress --group-id $EC2SGID --ip-permissions IpRanges="[{CidrIp=0.0.0.0/0}]",IpProtocol=tcp,FromPort=443,ToPort=443

接続確認すると、直接接続が繋がらなくなりました。

【Eyeball】接続確認
~ $ curl https://$MYHOST
Hello World

~ $ curl https://$MYHOST --resolve $MYHOST:443:$PUBIP
curl: (28) Failed to connect to $MYHOST port 443 after 75000 ms: Couldn't connect to server

他の Cloudflare アカウントのゾーンから到達の可能性

Cloudflare の他のアカウントのゾーン経由でアクセス可能性は残ります。

補足:運用へのメモ

Cloudflare IP リストの変更に追随する場合に利用できます。前述したように、API で入手するリストは etag が変わるので、それをキーに変更するのがいいでしょう。
ここでは一時的に作業端末の IP を追加・削除してみます。

【AWS】既存の Prefix List の変更
# 例)操作している端末の IP アドレスを一時的に追加する
# 今の自分のIPを得る
~/.aws $ MYIP=`curl -s httpbin.org/ip | jq -r '.origin'`/32

# 現行 Version を得て、サブネット(自分IP/32)を追加
~/.aws $ PLVER=`aws ec2 describe-managed-prefix-lists | jq '.PrefixLists[]|select(.PrefixListId=="'$PLISTID'").Version'`
~/.aws $ aws ec2 modify-managed-prefix-list \
    --add-entries Cidr=$MYIP,Description=DevOffice \
    --prefix-list-id $PLISTID \
    --current-version $PLVER

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv4",
        :
        "Version": 1,
        :
    }

# 現行 Version を得て、サブネットを削除
~/.aws $ PLVER=`aws ec2 describe-managed-prefix-lists | jq '.PrefixLists[]|select(.PrefixListId=="'$PLISTID'").Version'`
~/.aws $ aws ec2 modify-managed-prefix-list \
    --remove-entries Cidr=$MYIP\
    --prefix-list-id $PLISTID \
    --current-version $PLVER

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv4",
        :
        "Version": 2,
        :
    }
  • Managed Prefix List の max-entries を変更
【AWS】Prefix List の最大値調整
# IPv4 は現行のリストが 15 行、Max を 30 から 20 に減らす
~/.aws $ aws ec2 modify-managed-prefix-list --prefix-list-id $PLISTID --max-entries 20

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv4",
        :
        "StateMessage": "Attempting to modify maximum entries from (30) to (20).",
        :
        "PrefixListName": "CfCidr",
        "MaxEntries": 30,
        "Version": 3,
        :
    }

# IPv6 は現行のリストが 7 行、Max を 15 から10 に減らす
~/.aws $ aws ec2 modify-managed-prefix-list --prefix-list-id $PLISTID6 --max-entries 10

## 応答抜粋
    "PrefixList": {
        "PrefixListId": "pl-*",
        "AddressFamily": "IPv6",
        :
        "StateMessage": "Attempting to modify maximum entries from (15) to (10).",
        :
        "PrefixListName": "CfCidr6",
        "MaxEntries": 15,
        "Version": 1,
        :
    }

# 確認
~/.aws $ aws ec2 describe-managed-prefix-lists |jq '.PrefixLists[]|select(.OwnerId!="AWS")|.PrefixListName,.MaxEntries'

## 応答抜粋
"CfCidr"
20
"CfCidr6"
10
  • Managed Prefix List の削除
【AWS】Security Group 作成
~/.aws $  aws ec2 delete-managed-prefix-list --prefix-list-id $PLISTID
【AWS】Security Group 作成
# Security Group の作成
~/.aws $ SGID=`aws ec2 create-security-group --group-name CF_HTTP_Proxy --description "Cloudflare HTTP Proxy" --vpc-id $MYVPC | jq -r '.GroupId'`

# 作成した Security Group の確認
~/.aws $ aws ec2 describe-security-groups|jq '.SecurityGroups[]|select (.GroupId == "'$SGID'")'

## 応答抜粋
  "Description": "Cloudflare HTTP Proxy",
  "GroupName": "CF_HTTP_Proxy",
  "IpPermissions": [],
:

トランスポート層で防ぐ

Authenticated Origin Pulls

TLS のクライアント認証(mTLS)で保護します。
Cloudflare がクライアントとして証明書を提示し、オリジンサーバーが認証します。Cloudflare では Authenticated Origin Pullsと呼んでます。

二つの方法があるので、どちらも試します。

  • Cloudflare 発行のクライアント証明書
  • ユーザー持ち込みのクライアント証明書

Authenticated Origin Pulls を有効にしても、オリジンサーバー側が mTLS を要求しなければ、クライアント認証なく、そのまま TLS 通信が可能です。

Cloudflare 発行のクライアント証明書

まずは、より簡単に実装できる Cloudflare のクライアント証明書を使う方法です。
700

オリジンサーバー側の準備をします。

  • Cloudflare の CA ルート証明書をオリジンサーバーにダウンロード
    • Cloudflare が提示してくるクライアント証明書を検証するため
  • mTLS を有効化
【オリジンサーバー】Nginx
# Cloudflare ルート証明書のダウンロード
$ sudo wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem

# 念のため、証明書の中身をチラ見(2029年まで有効の模様)
$ openssl x509 -in authenticated_origin_pull_ca.pem -noout -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 6310029703491235425 (0x5791ba9556c22e61)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = US, O = "CloudFlare, Inc.", OU = Origin Pull, L = San Francisco, ST = California, CN = origin-pull.cloudflare.net
        Validity
            Not Before: Oct 10 18:45:00 2019 GMT
            Not After : Nov  1 17:00:00 2029 GMT
        Subject: C = US, O = "CloudFlare, Inc.", OU = Origin Pull, L = San Francisco, ST = California, CN = origin-pull.cloudflare.net
        :

# Nginx の設定ファイルでルート証明書を読み込ませ、mTLS を有効化
$ vi <Nginx の server 設定ファイル>

ssl_client_certificate /etc/nginx/certs/authenticated_origin_pull_ca.pem;
ssl_verify_client on;

オリジンサーバー側で正常に設定が反映されればサーバーは mTLS を要求します。
そのため、クライアント証明書を提示しないこれまでのリクエストはエラーとなります。

【Eyeball】接続確認
# Cloudflare 経由での接続
~ $ curl https://$MYHOST
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
~ $

念のためでが、この mTLS は Cloudflare とオリジンサーバー間の話であり、Eyeball と Cloudflare の間のことではありません。

オリジンサーバーの mTLS 要求に対応するよう、Cloudflare の Authenticated Origin Pulls 設定を有効にします。SSL/TLS のモードは Full 以上が必須です。

【Cloudflare】Authenticated Origin Pulls 設定
# TLS 設定が Full 以上のことを確認
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ssl" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq

## 応答抜粋
    "id": "ssl",
    "value": "full",

# Authenticated Origin Pulls 現状確認(off)
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/tls_client_auth" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.result'

## 応答抜粋
  "id": "tls_client_auth",
  "value": "off",

# Authenticated Origin Pulls 有効化(on)
~/.cf $ curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/tls_client_auth" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
  "value": "on"
}' | jq '.result'

## 応答抜粋
  "id": "tls_client_auth",
  "value": "on",

Authenticated Origin Pulls が有効になったので、再度リクエストすると、mTLS が成功しコンテンツが表示されます。

【Eyeball】接続確認
~ $ curl https://$MYHOST
Hello World

オリジンサーバーのログでクライアント証明書の subject DN を取ると、CN=origin-pull.cloudflare.net,OU=Origin Pull,O=Cloudflare\x5C, Inc.,L=San Francisco,ST=California,C=US の表示となりました。

【オリジンサーバー】Nginx アクセスログ
172.68.118.56 - CN=origin-pull.cloudflare.net,OU=Origin Pull,O=Cloudflare\x5C, Inc.,L=San Francisco,ST=California,C=US - - [03/Sep/2023:05:41:45 +0000] "GET / HTTP/1.1" 200 12 "-" "curl/8.2.1" "-"

他の Cloudflare アカウントのゾーンから到達の可能性

Cloudflare の他のアカウントのゾーンでも同じように Authenticated Origin Pulls を有効にすれば接続の可能性が残ります。

ユーザー持ち込みのクライアント証明書

特定のゾーンあるいは特定のホスト名について、ユーザー持ち込みのクライアント証明書を使います。

700

持ち込みの証明書を適用する範囲を、ゾーン全体あるいはホストごとで設定します。
ホストごとの設定が優先されます。

特定ホストで持ち込みクライアント証明書を使う

まずは特定ホストに対して設定します。
テストで利用する CA については、easy-rsa を使い、下記の証明書を作成しました。

【オリジンサーバー】証明書
# CA ルート(3650日)
        Issuer: CN = Private CA EC2
        Validity
            Not Before: Sep  3 02:33:26 2023 GMT
            Not After : Aug 31 02:33:26 2033 GMT
        Subject: CN = Private CA EC2

# クライアント(825日)
        Issuer: CN = Private CA EC2
        Validity
            Not Before: Sep  3 02:37:03 2023 GMT
            Not After : Dec  6 02:37:03 2025 GMT
        Subject: CN = Cloudflare Edge Servers

まず、作成したクライアント証明書と秘密鍵(PEM)について、改行を置換したデータを用意します。

【オリジンサーバー】API アップロードサンプルに合わせ整形
# ルート証明書とクライアントの秘密鍵(PEM)の改行を \n に置換
$ sed -z -e 's/\n/\\n/g' issued/edge.cloudflare_certonly.crt
-----BEGIN CERTIFICATE-----\nM..==\n-----END CERTIFICATE-----\n

$ sed -z -e 's/\n/\\n/g' private/client.key
-----BEGIN PRIVATE KEY-----\nM..Y=\n-----END PRIVATE KEY-----\n

クライアント証明書と秘密鍵を Cloudflare にアップロードします。

【Cloudflare】クライアント証明書と秘密鍵のアップロード
# 初期は設定が空
~/.cf $ curl -s -X GET  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames/certificates" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.'

# API でアップロード(status が pending_deployment)
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames/certificates" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
           "certificate":"-----BEGIN CERTIFICATE-----\nM..==\n-----END CERTIFICATE-----\n",
           "private_key":"-----BEGIN PRIVATE KEY-----\nM..Y=\n-----END PRIVATE KEY-----\n"
        }' | jq '.result'

## 応答抜粋
  "id": "2*",
  "status": "pending_deployment",

# 再確認(status が active)
~/.cf $ curl -s -X GET  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames/certificates" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.result'

## 応答抜粋
  "id": "2*",
  "status": "active",

アップロードした持ち込みクライアント証明書を特定ホストに対して適用します。

【Cloudflare】特定ホストに持ち込み証明書を適用
# 証明書 ID の取得
~/.cf $ CERTID=`curl -s -X GET  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames/certificates" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq -r '.result[].id'`

# ID とホスト名で有効化(status が pending_deployment)
~/.cf $ curl -s -X PUT  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
     "config":[
       {
         "cert_id": "'$CERTID'",
         "enabled": true,
         "hostname":"'$MYHOST'"
       }
      ]
     }' | jq '.result'

## 応答抜粋
    "hostname": "$MYHOST",
    "cert_id": "2*",
    "enabled": true,
    "status": "pending_deployment",

# 再確認(status が active)
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/hostnames/$TLSHOST" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.result.status'

## 応答抜粋
    "active"

オリジンサーバーでは、当該の Virtual Host が参照するルート証明書を切り替えます。

【オリジンサーバー】Nginx
# mTLS に使う CA ルート証明書を Cloudflare からオレオレに切り替え
ssl_client_certificate /etc/nginx/certs/easyrsa_ca.pem;

この状態で、再度接続してみます。
オリジンサーバーのアクセスログで subject DN が持ち込み証明書の CN=Cloudflare Edge Servers となっています。

【Eyeball】接続確認
# 切り替えられ、当該ホストへの接続が成功
~ $ curl https://$MYHOST/
Hello World

## オリジンサーバーのアクセスログ(subject DN が持ち込んだクライアント証明書)
172.68.118.3 - CN=Cloudflare Edge Servers - - [03/Sep/2023:07:28:57 +0000] "GET / HTTP/1.1" 200 12 "-" "curl/8.2.1" "-"
ゾーン全体で持ち込みクライアント証明書を使う

この状態ではゾーンに属する他のホストでは相変わらず Cloudflare のクライアント証明書が使われます。
ゾーン全体で持ち込み証明書を利用する場合は、下記の設定を有効にします。

【Cloudflare】ゾーン全体への持ち込み証明書適用
# ゾーン全体への適用状況の確認(false=未適用)
~/.cf $ curl -s -X GET  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/settings" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.result'

## 応答抜粋
  "enabled": false

# オリジンサーバーのアクセスログ(subject DN は Cloudflare のクライアント証明書)
172.68.119.56 - CN=origin-pull.cloudflare.net,OU=Origin Pull,O=Cloudflare\x5C, Inc.,L=San Francisco,ST=California,C=US - - [03/Sep/2023:07:55:55 +0000] "GET / HTTP/1.1" 200 12 "-" "curl/8.2.1" "-"

# 証明書と秘密鍵のアップロード(特定ホストの場合と違う URL なので注意)
~/.cf $  curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{"certificate":"'$EDGCRT'",
     "private_key":"'$EDGKEY'"
     }' | jq '.result'

## 応答抜粋
  "id": "b*",
  "status": "pending_deployment",
  "issuer": "CN=Private CA EC2",

# ゾーン全体のクライアント証明書が持ち込みに設定変更がされるが、
# "enabled": false なので その他のホストで mTLS が無効になる
~ $ curl https://<ゾーンの他のホスト>/
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
~ $

# オリジンサーバーのアクセスログ(どちらのクライアント証明書も来ていない)
172.68.119.47 - - - - [03/Sep/2023:08:39:49 +0000] "GET / HTTP/1.1" 400 246 "-" "curl/8.2.1" "-"

# ゾーンでの持ち込み証明書利用を有効にする
~/.cf $ curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/origin_tls_client_auth/settings" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{"enabled":true}' | jq '.result'

## 応答抜粋
  "enabled": true

# その他ホストも mTLS に対応し、持ち込み証明書を利用開始
# アクセスログ(subject DN が持ち込んだクライアント証明書)
172.68.118.108 - CN=cf1 - - [03/Sep/2023:08:44:12 +0000] "GET / HTTP/1.1" 400 224 "-" "curl/8.2.1" "-"

機能理解の参考に、実験として簡易的な CA で作業をしました。
実際の本番環境では参照せず、証明書の更新、廃止など、管理運用は厳密に行ってください。

他の Cloudflare アカウントのゾーンから到達の可能性

持ち込み証明書を利用したことで、他のアカウントのゾーンからのオリジン到達が失敗しています。

【Eyeball】他のアカウントのゾーンからオリジンへつなぐ
# 他のアカウントからオリジンを向けた接続は失敗しはじめる
~ $ curl https://<他のゾーン>/
<html>
<head><title>400 The SSL certificate error</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The SSL certificate error</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

アプリケーション層で防ぐ

Cloudflare Tunnel

オリジンサーバーに cloudflared を導入し、そのトンネル経由で接続します。
うまく動けば、オリジンサーバー側のファイアウォールでは Port 443 を閉じることができます。
700

cloudflared はダッシュボードあるいは CLI で作る方法があります。
今回は API を使ってダッシュボードを CLI 風に操作してみます。

まず cloudflared トンネルを作成します。

cloudflared は通信宛先に Port 7844 TCP/UDP を使います。特定の内部サーバーからのみ Port 7844 への通信を許可するなど、外向け通信のファイアウォールポリシーを定義するなど、勝手トンネルの乱立を防ぐのが推奨です。

Cloudflare API へのアップロードするデータの config_src は 2 パターンあります(デフォルトは local)。

  • local: cloudflared の稼働するサーバーで cloudflared の設定を管理(config.yaml ファイル利用)
  • cloudflare: ダッシュボードで管理

今回はダッシュボード(というか API)で管理するので、cloudflare を指定します。

【Cloudflare】cloudflared トンネルの作成
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
     "config_src": "cloudflare",
     "name": "EC2 Origin"
    }' | jq '.result'

## 応答抜粋
  "id": "8*",
  "name": "EC2 Origin",
  "tun_type": "cfd_tunnel",
  "status": "inactive",
  "remote_config": true,

作成したトンネルに設定を当てていきます。
具体的には、オリジンサーバーに HTTPS でプロキシーするための Ingress rules を定義します。

  • 今回はサーバー証明書を Let's Encrypt で取ったので noTLSVerify は false でもいいですが、オレオレのときなどは true(あるいは false + caPool でルートのパス指定)で乗り切ります。
  • service を localhost で指定しているので、Nginx へリクエストする SNI が localhost になります。originServerName で Nginx につなぐときの SNI を元の名前に再度上書きしています。
【Cloudflare】cloudflared トンネルにルールを設定
# 作成したトンネルの ID を得る
~/.cf $ TUNID=`curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq -r '.result[]|select (.name =="EC2 Origin").id'`

#トンネル ID を指定し設定を注入
~/.cf $ curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNID/configurations" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
    "config": {
      "ingress": [
        {
          "hostname": "'$MYHOST'",
          "originRequest": {
            "noTLSVerify": true,
            "originServerName": "'$MYHOST'"
          },
          "service": "https://localhost:443"
        },
        {
            "service": "http_status:404",
            "originRequest": {}
         }
      ]
    }
  }' | jq '.result'

以上で Cloudflare 側のトンネル設定は完了です。
次はオリジンサーバーに cloudflared を設定します。

まず cloudflared をインストールします。

【オリジンサーバー】cloudflared インストール
# リリース確認
$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 22.04.3 LTS
Release:	22.04
Codename:	jammy

# Ubuntu 22.04 なので、こちらの手順に従ってインストール
https://pkg.cloudflare.com/index.html#ubuntu-jammy

インストールできたら、cloudflared の起動設定を行います。

【オリジンサーバー】cloudflared 起動
# 作成済みのトンネルから Token を取り出し
~$ CFDTOKEN=`curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNID/token" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq -r '.result'`

# TOKEN を指定して起動設定
~$ sudo cloudflared service install $CFDTOKEN
2023-09-03T11:06:07Z INF Using Systemd
2023-09-03T11:06:09Z INF Linux service for cloudflared installed successfully

# サービスの確認
~$ sudo systemctl status cloudflared
● cloudflared.service - cloudflared
     Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled; vendor preset: enabled)
     Active: active (running) since Sun 2023-09-03 11:06:09 UTC; 23s ago
   Main PID: 12732 (cloudflared)
      Tasks: 7 (limit: 1121)
     Memory: 17.0M
        CPU: 131ms
     CGroup: /system.slice/cloudflared.service
             └─12732 /usr/bin/cloudflared --no-autoupdate tunnel run --token eyJhIj
     :

設定を反映したトンネルが立ち上がりました。

あとはルーティング(DNS)を変更するだけです。
DNS の設定はオリジンサーバーの IP アドレスを A レコードで指定したままなので、それを CNAME レコードに変更し、ターゲットを cloudflared トンネルの名前にします。

cloudflared の名前は <UUID>.cfargotunnel.com という形式になります(UUID = トンネル ID)。

ダッシュボードでのDNS のレコード変更と同じことを API で行います。

DNS レコードの変更
# 既存 DNS レコードの確認
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq '.result[]|select (.name=="'$MYHOST'")'

## 応答抜粋
  "id": "92*",
  "name": "$MYHOST",
  "type": "A",
  "content": "<オリジンサーバー PUBLIC IP アドレス>",
  "proxied": true,

# DNS レコードの ID を得る
~/.cf $ DNSRRID=`curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq -r '.result[]|select (.name=="'$MYHOST'")|.id'`

# ID に対して変更を加える(CNAME でトンネル名を指す)
~/.cf $  curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNSRRID" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
  -d '{
  "content": "'$TUNID'.cfargotunnel.com",
  "name": "'$MYHOST'",
  "proxied": true,
  "type": "CNAME"
}' | jq '.'

## 応答抜粋
    "name": "$MYHOST",
    "type": "CNAME",
    "proxied": true,

書き換わったようなので、接続をしてみますが、失敗します。クライアント証明書が送られてこなかった、と言われます。

【Eyeball】接続確認
# 接続
~ $ curl https://$MYHOST/
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

# オリジンサーバーのアクセスログ(クライアント証明書が来ていない)
127.0.0.1 - - - - [03/Sep/2023:11:56:54 +0000] "GET / HTTP/1.1" 400 246 "-" "curl/8.2.1" "-"

どうやら、cloudflared をオリジンサーバーの mTLS に対応させるオプションはなさげです。
オリジンサーバーで mTLS を無効にして、再度チャレンジします。

【オリジンサーバー】Nginx で mTLS 無効化
# オリジンサーバー Nginx で ssl_verify_client をコメントアウト
#ssl_verify_client on;

# 再接続
~ $ curl https://$MYHOST/
Hello World

無事に接続することができました。

オリジンサーバー側ファイアウォールで Port 443 を閉塞

AWS の Ingress Policy で Port 443 宛の通信を止めてみます。

【AWS】Security Group の Port 443 を閉塞
# Ingress Permission から Cloudflare IP TCP 443 を削除
~/.aws $ aws ec2 revoke-security-group-ingress --group-id $EC2SGID --ip-permissions PrefixListIds="[{PrefixListId=$PLISTID},{PrefixListId=$P6LISTID}]",IpProtocol=tcp,FromPort=443,ToPort=443

# 再接続
~ $ curl https://$MYHOST/
Hello World

オリジンサーバー側でパブリックに Port 443 を開けなくても、接続ができました。

他の Cloudflare アカウントのゾーンから到達の可能性

他のアカウントのトンネルの利用はできないので、オリジンサーバーを使おうとする設定自体ができません。

補足:cloudflared トンネルのデバッグ(ダッシュボード・API からの作成時)

cloudflared のデバッグログは設定ファイルのオプションでフラグを立てます
cloudflared の systemd 設定ファイルに一時的に設定追加してみます。

【オリジンサーバー】cloudflared デバッグログ追加
# 起動コマンドに --loglevel, --logfile 追加
~$ sudo vi /etc/systemd/system/cloudflared.service
#ExecStart=/usr/bin/cloudflared --no-autoupdate tunnel run --token e*
ExecStart=/usr/bin/cloudflared --loglevel debug --logfile /var/log/cloudflared.log --no-autoupdate tunnel run --token e*

# 再読み込みと再起動
~$ sudo systemctl daemon-reload
~$ sudo systemctl restart cloudflared

# ログファイルを見ると、debug レベル以上のログが採取されています
~$ sudo tail -f /var/log/cloudflared.log

{"level":"info","version":20,"config":"{\"ingress\"...}
{"level":"debug","current_version":20,"...}

中締め

オリジンサーバーへの接続を

・Cloudflare からのみ
・Cloudflare の自分のアカウントからのみ

に制限する方法を試しました。

ここから HTTP プロトコルでの制限に移ります。
一旦初期状態に戻し、Cloudflare Proxy とその他の直接接続もできる状態に戻します。

【テスト環境全体】初期化
#【Cloudflare】 Cloudflare DNS をオリジンサーバーの A レコードに戻す
~/.cf $  curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$DNSRRID" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
  -d '{
  "content": "'$PUBIP'",
  "name": "'$MYHOST'",
  "proxied": true,
  "type": "A"
}' | jq '.'

## 応答抜粋
    "name": "$MYHOST",
    "type": "A",
    "content": "<オリジンサーバー PUBLIC IP アドレス>",
    "proxied": true,

#【オリジンサーバー】 cloudflared サービスの停止
~$ sudo systemctl stop cloudflared.service

#【オリジンサーバー】 Nginx mTLS 無効化
#ssl_verify_client on;

#【AWS】 Security Group TCP 443 全許可
~/.aws $ aws ec2 authorize-security-group-ingress --group-id $EC2SGID --ip-permissions IpRanges="[{CidrIp=0.0.0.0/0}]",IpProtocol=tcp,FromPort=443,ToPort=443

## 応答抜粋
            "IpProtocol": "tcp",
            "FromPort": 443,
            "ToPort": 443,
            "CidrIpv4": "0.0.0.0/0"

#【Eyeball】 接続(Cloudflare Proxy 経由と直接)
~/.cf $ curl https://$MYHOST/
Hello World
~/.cf $ curl https://$MYHOST/ --resolve $MYHOST:443:$PUBIP
Hello World

HTTP Basic 認証

オリジンサーバー側で HTTP Basic 認証を必須とします。
認証キーを持たないリクエストは拒否されます。

700

まずはオリジンサーバーで Basic 認証を有効にしておきます。
認証情報を伝えないので、接続が失敗します。

【オリジンサーバー】Basic 認証
# オリジンサーバー Nginx の設定
        auth_basic	"auth area";
		auth_basic_user_file /etc/nginx/passwd/.htpasswd;

# Cloudflare 経由(認証エラー)
~/.cf $ curl https://$MYHOST/p
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

# 直接接続(認証エラー)
~/.cf $ curl https://$MYHOST/p --resolve $MYHOST:443:$PUBIP
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

次に、Basic 認証に対応するよう Cloudflare の設定を追加します。

具体的には Transform RulesRequest header modification rule を使い、
オリジンサーバーにリクエストを送信する際に、Basic 認証のヘッダを追加します。

【Cloudflare】Transform Rules(header modification)設定
# header modification ルールセットの ID を得る
~/.cf $ RSNAME="header modification"
~/.cf $ RSID=`curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json"  | jq -r '.result[]|select(.name=="'$RSNAME'")|.id'`

# ルールセット ID に、Basic 認証用のヘッダを追加するルールを追加
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RSID/rules" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json"  \
     -d '{
         "action": "rewrite",
         "expression": "(http.request.full_uri eq \"https://'$MYHOST'/p\")",
         "description": "basic auth",
         "enabled": true,
         "action_parameters": {
           "headers": {
             "Authorization": {
               "operation": "set",
               "value": "Basic dXNlcjE6dXNlcjE="
              }
            }
          }
         }'

再度接続してみます。

【Eyeball】接続確認
# Cloudflare 経由(認証 OK)
~ $ curl https://$MYHOST/p
Hello Authenticated Users

# Nginx ログ(Basic 認証のユーザー名 user1 が取れて、200 で応答)
172.70.223.94 - - - user1 [04/Sep/2023:08:05:59 +0000] "GET /p HTTP/1.1" 200 57 "-" "curl/8.2.1" "-"

# 直接接続(認証エラー)
~ $ curl https://$MYHOST/p --resolve $MYHOST:443:$PUBIP
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

# Nginx ログ(認証ユーザー名が取れておらず、401 で応答)
104.28.157.52 - - - - [04/Sep/2023:08:07:52 +0000] "GET /p HTTP/1.1" 401 188 "-" "curl/8.2.1" "-"

Cloudflare Proxy で HTTP リクエストやレスポンスの改変をする場合、以前は Cloudflare Workers を利用するケースが多くありましたが、現在はいろいろな Rules でカバーできるよう、実装が増えています。

補足:Eyeball と Cloudflare 間の認証

この状態では Eyeball と Cloudflare の間には認証を入れていません。
何かしら認証したい場合、たとえばこちらの Workers を使ったヘッダー認証と組み合わせると、Eyeball 側でも認証をつけることができます。

【参考】Workers でのヘッダー認証(Eyeball側)と Basic 認証(オリジンサーバー側)の組み合わせ
# 【Cloudflare】下記の Workers スクリプトを当該の URL にルーティングする
https://developers.cloudflare.com/workers/examples/auth-with-headers

# 【Eyeball】Workers 指定のヘッダーなしでの接続
~ $ curl https://$MYHOST/p
Sorry, you have supplied an invalid key.%

# 【Eyeball】Workers 指定のヘッダーを追加して接続
~ $ curl https://$MYHOST/p  -H "X-Custom-PSK:mypresharedkey"
Hello Authenticated Users

# 【Cloudflare】Workers ログ抜粋

      "url": "https://$MYHOST/p",
      "method": "GET",
      "headers": {
        "authorization": "REDACTED",
        "x-custom-psk": "mypresharedkey",
        :

# Nginx アクセスログ(オリジンサーバーが受け取るのは Basic 認証 user1)
172.70.222.145 - - - user1 [04/Sep/2023:09:28:43 +0000] "GET /p HTTP/1.1" 200 57 "-" "curl/8.2.1" "-"

Workers と Transform Rules(request header modification)の処理順を確認します。

  • Workers がリクエストを受けたときには、既に Authorization ヘッダーは付与されているので、request header modification rule でヘッダーを挿入したあとに Workers にルーティングされたことがわかります。
  • これは、シーケンシャル UI で確認できる通りです。
    (トラフィックシーケンスが表示されない場合はズームアウトしたり、ブラウザの横幅を広げて見てください。右側にヒョッコリ出てくると思います。)
700

この順番があるので、Eyeball と Workers で Basic 認証をする場合は、オリジンサーバーとの Basic 認証追加も Transform Rules でなく Workers で一緒にやったほうがシンプルかもしれません。

また、こういうちょっとした処理については、今後 Cloudflare Snippets も使えそうです。

リソースの点において、Cloudflare SnippetsはWorkersよりも軽量です。最大実行時間は5msで最大メモリは2MB、パッケージの合計サイズは32KBとなっています。こうした制限はありますが、HTTPヘッダーの変更、URLの書き換え、Cloudflare Workersが提供する追加機能やリソースを必要としないトラフィックタスクのルーティングなど、一般的なユースケースでは困ることがないでしょう。

JSON Web Tokens

これは Eyeball と Cloudflare 間でも認証認可をおここない、その情報をオリジンサーバー側に知らせることで、オリジンサーバーへの接続を Cloudflare で認可されたものだけに限定します。

認証認可を Cloudflare Access で実施、その結果 HTTP リクエストに付与される Access Application Token(JWT)をオリジンサーバーで署名検証、正当なリクエストのみ受け付ける、という連携が可能になります。

700

なので、ここでは Eyeball と Cloudflare の認証の話も入ってきます。

今回は本家にある Python の例を流用し、サーバーに適用します。
単純にリクエストすると、下記のようなメッセージを表示します。

【Eyeball】接続確認
~ $ curl https://$MYHOST/aa
missing required cf authorization token%

Eyeball をユーザー ID (IdP 連携や Email ワンタイム PIN)で認証

Access でアプリケーションのユーザー認証をする場合、Email ワンタイム PIN はデフォルトで利用ができます。IdP との認証連携が必要であれば、事前に構築しておきます。(ここでは Onelogin との接続を構築済)

次に Access アプリケーションを作ります。

【Cloudflare】Access アプリケーションの作成
# Access を利用するドメイン(パス)を定義
~/.cf $ ADOMAIN="$MYHOST/aa"

# Access アプリケーションの作成
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/apps" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  \
-d '{
    "type": "self_hosted",
    "domain": "'$ADOMAIN'"
   }'

作成したアプリケーションに Access のポリシーを当てます。

【Cloudflare】Access アプリケーションに接続ポリシーを適用
# Access アプリケーションの ID を得る
~/.cf $ PUID=`curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/apps" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json" | jq -r '.result[]|select(.domain=="'$ADOMAIN'")|.id'`

# Access アプリケーションに対して、ポリシーを適用
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/apps/$PUID/policies" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  \
-d '{
    "decision": "allow",
    "include": [
      {
        "email_domain": {
          "domain": "cloudflare.com"
        }
      }
    ],
    "name": "cf"
   }'

準備は完了です。接続してみます。
(今回 OpenID Connect での認証が走るので、ブラウザーで試します)

  • Access のログイン画面が表示されますので、認証のアウトソース先を選択します
500
  • 選択した認証方式(今回 Onelogin)にリダイレクトされるので、認証を無事に済ませます。
  • 認証が終わったら Access で認可が行われ、問題がなければオリジンサーバーにリクエストが転送されます。
  • リクエストヘッダーには Applicatin Token(JWT)が含まれます。
  • オリジンサーバーの検証アプリケーションは正当な JWT に含まれるユーザー名(email)を表示します。
500

Service Token による認証

次に、モノからのアクセスを想定して Service Token も試してみます。

Access で作成した Token を Eyeball に事前に渡しておくことで、HTTP ヘッダーによる Access アプリケーションの認証認可を実現します。

まず、 Service Token を作ります。
client_idclient_secret はこれっきりの表示になるので、メモしておきます。

【Cloudflare】Access Service Token の作成
# Service Token の作成(有効期間 duration を 3 年に指定)
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/service_tokens" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  \
-d '{
"duration": "26280h",
"name": "AWS EC2 WEB"
}'
 
## 応答抜粋(client_id と client_secret はこれ以降表示できない)
    "client_id": "3*.access",
    "client_secret": "9*",
    "created_at": "2023-09-04T16:12:39Z",
    "expires_at": "2026-09-03T16:12:39Z",
    "id": "b*",
    "name": "AWS EC2 WEB",
    "duration": "26280h"

作成した Service Token で Policy を作り Access アプリケーションに追加します。

【Cloudflare】Service Token を使った Policy を Access アプリケーションに追加
# Sevice Token の TOKEN ID を得る
~/.cf $ TOKENID=`curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/service_tokens" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  | jq -r '.result[]|select(.name=="AWS EC2 WEB")|.id'`

# Policy の追加
~/.cf $ curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/apps/$PUID/policies" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  \
-d '{
    "decision": "non_identity",
    "include": [
        {
          "service_token": {
            "token_id": "'$TOKENID'"
          }
        }
      ],
    "name": "st"
   }' | jq '.'

## 応答抜粋
    "decision": "non_identity",
    "id": "0*",
    "include": [
      {
        "service_token": {
          "token_id": "b*"
        }
      }
    ],
    "name": "st",
    "precedence": 2

接続を確認します。

client_id と client_secret をそれぞれ指定のヘッダ(CF-Access-Client-IdCF-Access-Client-Secret)に入れることで、検証アプリケーションに接続することができます。

【Eyeball】接続確認
# Service Token の場合、JWT にユーザー情報(email)は 含まれないので、サーバーは代わりに common_name を表示している

~/.cf $ curl https://$MYHOST/aa -H "CF-Access-Client-Id: $STID" -H "CF-Access-Client-Secret: $STSC"
Hello, 3*.access%

cloudflared との併用時

cloudflared と併用する場合は、cloudflared で検証させることもできます。Cloudflare からオリジンサーバーへは CF_Authorization Cookie と同様の JWT が cf-access-jwt-assertion ヘッダーにも格納されます。cloudflared はそれを検証します。
アプリケーションでの検証をオフロードすることができます。

【オリジンサーバー】cloudflared で JWT検証
# Tame name を得る
~/.cf $ MYTEAM=`curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/access/organizations" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" | jq -r '.result.auth_domain' | sed -e 's/.cloudflareaccess.com//'`

# Access アプリケーションの AUD を得る
$ AUD=`curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/access/apps" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json" | jq -r '.result[]|select(.domain=="'$ADOMAIN'").aud'`

# Accessアプリケーションのパス(aa)を追加、検証を有効("access")にし、
# cloudflared トンネルに当てる設定を上書きする
~/.cf $ curl -s -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/cfd_tunnel/$TUNID/configurations" \
     -H "Authorization: Bearer $ATOKEN" \
     -H "Content-Type:application/json" \
     -d '{
          "config": {
           "ingress": [
            {
             "service": "https://localhost:443",
             "hostname": "'$MYHOST'",
             "path": "aa",
             "originRequest": {
              "access": {
               "audTag": [
                "'$AUD'"
               ],
              "required": true,
              "teamName": "'$MYTEAM'"
              },
              "noTLSVerify": true,
              "originServerName": "'$MYHOST'"
             }
            },
            {
             "service": "https://localhost:443",
             "hostname": "'$MYHOST'",
             "originRequest": {
              "noTLSVerify": true,
              "originServerName": "'$MYHOST'"
              }
            },
            {
             "service": "http_status:404",
             "originRequest": {}
            }
           ]
          }
         }' | jq '.result'

リクエストの検証に失敗した場合は、下記のようなエラーログが cloudflared の --logfile に出力されます。

【オリジンサーバー】cloudflared のエラーログ
# 不正な JWT を利用した場合のエラーログ
{"level":"error","error":"request filtered by middleware handler (AccessJWTValidator) due to: Invalid token in jwt: [d*]","cfRay":"8*-NRT","event":1,"ingressRule":"0","originService":"https://localhost:443","time":"2023-09-12T06:46:46Z"}

補足:Access Token の違い(ユーザー ID と Service Token)

ユーザー ID ベースの認証と Service Token では Access Token に違いがあったので、メモしておきます。

【Eyeball】set-cookie に入っている CF_Authorization のデコード
# ユーザ ID による認証
{
  "alg": "RS256",
  "kid": "a*"
}
{
  "aud": [
    "d*"
  ],
  "email": "*",
  "exp": 1694055752,
  "iat": 1693969352,
  "nbf": 1693969352,
  "iss": "https://*",
  "type": "app",
  "identity_nonce": "4*",
  "sub": "a*",
  "country": "JP"
}

# Service Token による認証
{
  "kid": "a0*",
  "alg": "RS256",
  "typ": "JWT"
}
{
  "type": "app",
  "aud": "d5*",
  "exp": 1693977228,
  "iss": "https://*",
  "common_name": "3*.access",
  "iat": 1693890828,
  "sub": ""
}

なお、ユーザ ID の場合、リクエストヘッダーの Cookie CF_Authorization 使って、そのユーザーの認証関連の全部の情報を得ることができます。

500

JWT にはヘッダー容量削減のためユーザー情報の一部しか含まれていませんので、トラブルシュートや他のメタ情報の利用など、必要に応じて別の URL に JWT を渡して確認します。

【Eyeball】認証済みユーザーのすべての情報を確認
~/bin $ curl -s -H "cookie: CF_Authorization=$CFTKN" https://$MYTEAM.cloudflareaccess.com/cdn-cgi/access/get-identity | jq '.'

## 応答抜粋
{
  "id": "1*",
  "name": "Foo Bar",
  "email": "foobar@cloudflare.com",
  "oidc_fields": {
    "preferred_username": "foobar",
    "given_name": "Foo",
    "family_name": "Bar"
   :

補足:運用面

JWT 署名検証用の公開鍵はローテーションがあるので、検証スクリプトでは更新に追随できるよう注意します。

通知

認証のあたりは、利用する情報がフレッシュか、有効か、が重要になってきまます。

今回の対象範囲では Access Sevice Token 期限や持ち込みクライアント証明書について Notification 機能を用いて Cloudflare から有効期限の近いことを通知することができます。(通知方法や通知対象の機能はプランにより異なりますので、確認が必要です。)
500

  • Access Service Token の追加
700
【Cloudflare】アラート通知設定
# Access Service Token 期限切れの通知
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/alerting/v3/policies" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  | jq '.result[]|select(.alert_type=="expiring_service_token_alert")'

## 応答抜粋
{
  "id": "4*",
  "name": "Expiring Access Service Token Alert",
  "description": "",
  "enabled": true,
  "alert_type": "expiring_service_token_alert",
  • Authentivated Origin Pulls
    Authenticated Origin Pulls は設定するとデフォルトで通知が有効になっています(宛先は権限のあるメンバーのメールアドレスが含まれますが、Webhook の追加など調整が必要な場合は別途実施してください)
700 700
【Cloudflare】アラート通知設定
# Authenticated Origin Pulls  期限切れの通知
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/alerting/v3/policies" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  | jq '.result[]|select(.name=="Default notification")'

## 応答抜粋
  "id": "0*",
  "name": "Default notification",
  "description": "This notification is automatically set by Cloudflare",
  "enabled": true,
  "alert_type": "hostname_aop_custom_certificate_expiration_type",

  "id": "f*",
  "name": "Default notification",
  "description": "This notification is automatically set by Cloudflare",
  "enabled": true,
  "alert_type": "zone_aop_custom_certificate_expiration_type",
  • cloudflaredトンネル の通知
700
tunnel
【Cloudflare】アラート通知設定
# トンネル健全性の通知
~/.cf $ curl -s -X GET "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/alerting/v3/policies" \
-H "Authorization: Bearer $ATOKEN" \
-H "Content-Type:application/json"  | jq '.result[]|select(.alert_type=="tunnel_health_event")'
{
  "id": "e*",
  "name": "トンネル状況",
  "description": "",
  "enabled": true,
  "alert_type": "tunnel_health_event",

その先へ

今回触れなかったものの概要です。
Enterprise プランのみですが、オリジンサーバーをより強固に守る手段が用意されています。

  • Cloudflare Aegis
    Cloudflare からオリジンサーバーへ接続する IP をユーザ専有のものにします。
700
  • Cloudflare CNI
    Cloudflare とオリジンサーバー(のネットワーク)を直接接続します。
700 700

その他、

あたりは別の機会にします。

まとめ

以上、オリジンサーバーを Cloudflare Proxy 以外からの接続から保護する方法について、本家ドキュメントに記載の複数の異なるレイヤー、方法で試しました。ご参考になればと思います。

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7