1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブロックページの種類

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

一番簡単なデフォルトのページ

まずは、公式のドキュメントを参照。
それによると、ブロックページはDefault、Custom Redirect URL、Custom Page Templateと3種類あるのがわかります。

デフォルトの設定は Access > アプリケーション > 該当アプリケーション > 概要より設定できます。
スクリーンショット 2024-12-29 23.03.36.png

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

スクリーンショット 2024-12-29 22.41.29.png

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

次にCustom Page Templateを試す

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

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

スクリーンショット 2024-12-29 23.25.57.png

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

スクリーンショット 2024-12-29 23.28.29.png

ちなみに、ドキュメントにはブロックページはIdentityとnon-identity
で分けることができると記載があります。

試しに、Non-identity failure(国、デバイスポスチャの失敗など)でブロックページを表示させてみます。日本語では、「非 ID 障害ブロック ページ」とありますのでこちらに別のテンプレートを設定。
スクリーンショット 2024-12-29 23.54.36.png

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

スクリーンショット 2024-12-29 23.53.31.png

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

もっとダイナミックなエラーページを作成したい

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

まずは、こちらの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
作成されたトークンをメモ。(トークンが表示されるのはこの時だけです)

2 Self Hostedのアプリケーションを追加。詳細は割愛しますが、ここではアプリケーションの追加が完了しているものとします。

3 アプリケーションのブロックページをRedirect URLに変更して、Workersのドメインを設定。この後Workersの仕込みを行います。
スクリーンショット 2024-12-31 12.11.52.png

Workersのデプロイ

GitHubの手順通り進めます。

とりあえずディレクトリーにあるもの全てをローカルに落としときます。そして、wrangler.tomlを編集。IDENTITY_DYNAMIC_THEME_STOREという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

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

wrangler dev

おお、ページができている。
スクリーンショット 2024-12-31 12.51.08.png

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

wrangler deploy

管理画面でデプロイされたのが確認できます。
スクリーンショット 2024-12-31 12.56.42.png

ちなみに、事前に作成しといたAPIトークンはSecretとしてwrangler.tomlには記載しなかったので、対応しておきましょう。

BEARER_TOKEN含め、設定で以下のようになっていれば取り急ぎOKです。
スクリーンショット 2024-12-31 13.16.09.png

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

スクリーンショット 2024-12-31 13.11.06.png

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

https://your-domain/debug とdebugのページに遷移すると、ロゴのアップロードや色味の変更もできます。
スクリーンショット 2024-12-31 13.14.04.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

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

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?