Cloudflare Accessのブロックページを色々カスタマイズしてみる

# ブロックページの種類
CloudflareのZero Trustで保護されたアプリケーションへアクセスする際に認証が失敗すると、ブロックページが表示されます。このブログでは、ブロックページの種類、またどこまでカスタマイズができるのか確認してみたいと思います。
ちなみに、追記になりますがCloudflareのZero Trust管理画面も日本語対応されました（2024年12月）！こちらのブログでも日本語画面で操作してみます！

### 一番簡単なデフォルトのページ
まずは、[公式のドキュメント](https://developers.cloudflare.com/cloudflare-one/applications/block-page/ "公式のドキュメント")を参照。
それによると、ブロックページはDefault、Custom Redirect URL、Custom Page Templateと3種類あるのがわかります。

デフォルトの設定は Access > アプリケーション > 該当アプリケーション > 概要より設定できます。
![スクリーンショット 2024-12-29 23.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/9a46e8d2-ccb3-cafc-5bca-4837bc8aeb33.png)

デザインのカスタマイズは特にできませんが、エラーのテキストは変更できます。
Cloudflare エラーテキストに"You cannot access this page."と入力すると・・・

![スクリーンショット 2024-12-29 22.41.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/e4c3fe21-6911-27d6-f8b4-9bdad9ef714c.png)

Cloudflareのロゴが入ったブロックページにしっかり反映されているのが確認できました。

### 次にCustom Page Templateを試す
PayGoもしくはエンタープライズ契約のあるアカウントであれば、カスタムページのデザインをCloudflareの管理画面で作成することができます。

設定 > カスタムページ > Access カスタム ページ より設定できます。
ちなみに、カスタムページには、Gatewayのブロックページ、App Launcherのカスタマイズ、ログインページのカスタマイズがありますがここではAccessカスタムページについて述べています。

![スクリーンショット 2024-12-29 23.25.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/b399d641-1a03-091a-a1bb-eb0196375b38.png)

カスタム HTMLにHTMLの記述を追加すると、独自デザインのブロックページが追加できました。

![スクリーンショット 2024-12-29 23.28.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/b3046d3f-981e-4dc8-ed56-8bfd4dc2414a.png)

ちなみに、ドキュメントにはブロックページは[Identityとnon-identity
](https://developers.cloudflare.com/cloudflare-one/applications/block-page/#types-of-block-pages "Identityとnon-identity")で分けることができると記載があります。

試しに、Non-identity failure（国、デバイスポスチャの失敗など）でブロックページを表示させてみます。日本語では、「非 ID 障害ブロック ページ」とありますのでこちらに別のテンプレートを設定。
![スクリーンショット 2024-12-29 23.54.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/8a1360d1-5825-f9a5-af6c-1bf854fd96bf.png)

ポリシーで日本からのアクセスを必須にして、シンガポールのIPからアクセスすると・・・

![スクリーンショット 2024-12-29 23.53.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/9dffa320-084d-b51a-fa72-e265e3b3afa8.png)

別のブロックページが表示されることを確認できました。

### もっとダイナミックなエラーページを作成したい
セキュリティーの管理者としてはアクセスできない理由をブロックページに細かく表示させたいという要望もあるのではないでしょうか。デバイスポスチャ、アイデンティティーなど複雑なポリシーを適応する場合、ブロックページに足りない権限を記載できると便利ですね。CloudflareではWorkersを利用することでよりダイナミックなブロックページが作成できます。

まずは、[こちらのGitHubページ](https://github.com/cloudflare/cf-identity-dynamic "こちらのGitHubページ")を参照。ユーザー情報を/cdn-cgi/access/get-identityから取ってきて、ダイナミックなブロックページを表示させる仕組みのようです。/cdn-cgi/access/get-identityとは認証されたユーザーの詳細な身元情報を取得するエンドポイントです。通常はhttps://{team name}.cloudflareaccess.com/cdn-cgi/access/get-identityという形でユーザー情報が取得できます。

**3つのRequirement**があるので、こちらを対応。
1 Access: Audit Logs Read、Access: Device Posture Readの権限がついているAPI Tokenを作成。APIトークンは、Cloudflarenのコアの画面（Applicationサービスをいじる方）から作成します。アカウント管理 > アカウントAPIトークン > トークンを作成する > カスタムトークンを作成する

![スクリーンショット 2024-12-30 0.38.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/19a857bd-1a5a-c47f-750d-a55b5a86a963.png)
作成されたトークンをメモ。（トークンが表示されるのはこの時だけです）

2 [Self Hosted](https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/self-hosted-apps/ "Self Hosted")のアプリケーションを追加。詳細は割愛しますが、ここではアプリケーションの追加が完了しているものとします。

3 アプリケーションのブロックページをRedirect URLに変更して、Workersのドメインを設定。この後Workersの仕込みを行います。
![スクリーンショット 2024-12-31 12.11.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/eeeeac11-92d1-2e6c-7a1e-0e5954a26bff.png)

### Workersのデプロイ
[GitHub](https://github.com/cloudflare/cf-identity-dynamic "GitHub")の手順通り進めます。

とりあえずディレクトリーにあるもの全てをローカルに落としときます。そして、wrangler.tomlを編集。IDENTITY_DYNAMIC_THEME_STOREという[KV](https://developers.cloudflare.com/kv/get-started/ "KV")も必要になるので、事前に作っておきましょう。

以下一例ですが、項目を一通り記載。

```
name = "identity-dynamic"
account_id = "cb125xxxxxxxxxxxxx"
workers_dev = false
compatibility_date = "2024-11-25"
main = "src/main.js"


routes = [
	{ pattern = "dynamic-block-page.soonchang.me", custom_domain = true }	
]

# wrangler kv:namespace create IDENTITY_DYNAMIC_THEME_STORE

[[kv_namespaces]]
binding = "IDENTITY_DYNAMIC_THEME_STORE"
id = "05fbxxxxxxxx"

[vars]
# - BEARER_TOKEN (Defined with wrangler secret put)
CORS_ORIGIN = "dynamic-block-page.soonchang.me/debug"
ACCOUNT_ID = "cb125xxxxxxxxxxxxx"
ORGANIZATION_ID = "cb125xxxxxxxxxxxxx"
ORGANIZATION_NAME = "soonbig"
DEBUG = "true"
TARGET_GROUP = "" # Define the "special group" that you want to use for notification

[site]
bucket = "./build" 

[build]
command = "npm run build"
```

wrangler.tomlファイルができたら、早速ビルドします。ちなみに、以下のDEPENDANCESが必要になるので、事前にインストールしておきましょう。

react-loader-spinner
react-router-dom
react-dom
tailwindcss

```
npm run build
```
こんな感じで表示されればOK。
![スクリーンショット 2024-12-31 12.43.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/53911e92-0adc-e427-9448-6166bf32bfbd.png)

念の為、ローカルでテスト。

```
wrangler dev
```
おお、ページができている。
![スクリーンショット 2024-12-31 12.51.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/06f68317-f5a1-94a3-f8a2-5fb579c212b5.png)

ここまでできたら、一度デプロイしてRedirect URLとして使えるか確認してみましょう。

```
wrangler deploy
```
管理画面でデプロイされたのが確認できます。
![スクリーンショット 2024-12-31 12.56.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/dd447af0-51d0-2040-8cb2-a33753be5a36.png)

ちなみに、事前に作成しといたAPIトークンは[Secret](https://developers.cloudflare.com/workers/configuration/secrets/ "Secret")としてwrangler.tomlには記載しなかったので、対応しておきましょう。

BEARER_TOKEN含め、設定で以下のようになっていれば取り急ぎOKです。
![スクリーンショット 2024-12-31 13.16.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/d1acc84f-1106-fbd6-2329-9354fb15e6e0.png)


実際にRedirect URLが動いているか確認。あえてWARPをオフにしてアプリケーションにアクセス。

![スクリーンショット 2024-12-31 13.11.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/66f28743-8ff0-d540-79f5-d5e21590aed1.png)

WARPがオフになっていることを教えてくれました！

https://your-domain/debug とdebugのページに遷移すると、ロゴのアップロードや色味の変更もできます。
![スクリーンショット 2024-12-31 13.14.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/a78469ed-a38c-2e84-b3fe-b777486cd34c.png)

ここからは慎重にカスタマイズが必要ですが、例えばデフォルトのテンプレートではCrowdStrike posture checkが入っています。利用しない場合は、src > components配下にあるposture.jsを編集する必要があります。

以下、CrowdStrikeの記述を試しに削除したposture.js　（あくまで参考に！）

```
import React, { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import "./status.css";

const Posture = ({ onLoaded }) => {
  const [osPostureChecks, setOsPostureChecks] = useState([]);
  const [securityKey, setSecurityKey] = useState(null);
  const [osStatus, setOsStatus] = useState({ message: "", passed: false });
  const [warpEnabled, setWarpEnabled] = useState(null);
  const [tooltipStyles, setTooltipStyles] = useState({});
  const [errorMessage] = useState("");

  const tooltipTriggerRef = useRef(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        console.log("Posture: Fetching data...");

        // Fetch WARP status
        const traceResponse = await fetch(
          "https://www.cloudflare.com/cdn-cgi/trace"
        );
        const traceText = await traceResponse.text();
        const warpStatus = traceText.includes("warp=on");
        setWarpEnabled(warpStatus);

        if (!warpStatus) {
          setSecurityKey(null);
          setOsStatus({
            message: "Posture information unavailable, please enable WARP",
            passed: false,
          });
          if (onLoaded) onLoaded(); // Notify parent that loading is complete
          return;
        }

        // Fetch user details
        const response = await fetch("/api/userdetails");
        const data = await response.json();

        /*
        Note that this looks strictly for the presence/use of "swk" (Proof of possession of a software-secured key)
        */

        const securityKeyInUse = data.identity?.amr?.includes("swk") || false;
        setSecurityKey(
          securityKeyInUse
            ? "Security Key in Use"
            : "Security Key is not in Use"
        );

        /*
        This also requires that rules exist for min-max values for Operating system versions
        https://developers.cloudflare.com/cloudflare-one/identity/devices/warp-client-checks/os-version/#enable-the-os-version-check
        */

        // OS Posture Check
        const postureRules = data.posture?.result || {};
        let relevantRules = [];
        let allConstraintsPassed = true;

        for (const rule of Object.values(postureRules)) {
          if (rule.type === "os_version") {
            relevantRules.push({
              name: rule.rule_name,
              success: rule.success,
              description: rule.description || "No description available",
              checked: rule.hasOwnProperty("check"),
              isMinConstraint: rule.rule_name
                .toLowerCase()
                .includes("min constraint"),
              isPatch: rule.rule_name.toLowerCase().includes("patch"),
            });

            if ((rule.isMinConstraint || rule.isPatch) && !rule.success) {
              allConstraintsPassed = false;
            }
          }
        }

        // Empty OS checks - For niece devices like chromeOS
        if (relevantRules.length === 0) {
          relevantRules.push({
            name: "OS Version Check",
            success: false,
            description:
              "No relevant OS version rules found for this device type",
            checked: false,
          });
          allConstraintsPassed = false;
        }

        setOsPostureChecks(relevantRules);

        // Set OS Status Message
        setOsStatus({
          message: allConstraintsPassed
            ? "Operating system up to date"
            : "Operating system update required",
          passed: allConstraintsPassed,
        });

        console.log("Posture: Data fetch complete.");
      } catch (error) {
        console.error("Posture: Error fetching data:", error);
        // setErrorMessage("Error fetching posture data. Please try again later.");
      } finally {
        if (onLoaded) onLoaded(); // Notify parent that loading is complete
      }
    };

    fetchData();
  }, [onLoaded]);

  // Allow hover-over of the OS results to see detailed information, useful for debugging the rules
  const handleMouseEnter = () => {
    if (tooltipTriggerRef.current) {
      const rect = tooltipTriggerRef.current.getBoundingClientRect();
      setTooltipStyles({
        top: rect.bottom + window.scrollY + 5,
        left: rect.left + window.scrollX,
      });
    }
  };

  const handleMouseLeave = () => {
    setTooltipStyles({});
  };

  const allPassed =
    warpEnabled &&
    securityKey === "Security Key in Use" &&
    osStatus.passed;

  return (
    <div className={allPassed ? "card-normal" : "card-error"}>
      {warpEnabled ? (
        <>
          <h2 className="text-xl font-semibold mb-4">
            Device Posture Requirements
          </h2>
          <ul className="mb-4 space-y-4">
            {/* Security Key Status */}
            <li className="info-item">
              <span
                className={`icon ${
                  securityKey === "Security Key in Use"
                    ? "check-icon"
                    : "cross-icon"
                }`}
              ></span>
              <span>{securityKey}</span>
            </li>

            {/* OS status */}
            <li
              className="info-item relative"
              ref={tooltipTriggerRef}
              onMouseEnter={handleMouseEnter}
              onMouseLeave={handleMouseLeave}
            >
              <span
                className={`icon ${
                  osStatus.passed ? "check-icon" : "cross-icon"
                }`}
              ></span>
              <span>{osStatus.message}</span>
              {tooltipStyles.top &&
                createPortal(
                  <div
                    className="tooltip"
                    style={{
                      position: "absolute",
                      ...tooltipStyles,
                      zIndex: 9999,
                    }}
                  >
                    <ul>
                      {osPostureChecks.map((check, index) => (
                        <li key={index} className="info-item">
                          <span
                            className={`icon ${
                              check.success ? "check-icon" : "cross-icon"
                            }`}
                          ></span>
                          <span>{`${check.name}: ${
                            check.checked
                              ? check.success
                                ? "Compliant"
                                : "Non-compliant"
                              : "Rule was not checked"
                          }`}</span>
                        </li>
                      ))}
                    </ul>
                  </div>,
                  document.body
                )}
            </li>
          </ul>
        </>
      ) : (
        <div className="text-black p-5">
          <span className="icon cross-icon mr-2"></span>
          Please enable WARP to view device posture information.
        </div>
      )}
      {errorMessage && <p className="text-red mt-4">{errorMessage}</p>}
    </div>
  );
};

export default Posture;

```

これでデプロイしなおすと、CrowdStrikeのチェックは削除されました。

![スクリーンショット 2024-12-31 14.15.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3954337/e3e282a4-e16d-70b1-185c-189236f20aa9.png)

非常に便利なテンプレートではありますが、実際に利用する際には、このように慎重にカスタマイズする必要があります。