ブロックページの種類
CloudflareのZero Trustで保護されたアプリケーションへアクセスする際に認証が失敗すると、ブロックページが表示されます。このブログでは、ブロックページの種類、またどこまでカスタマイズができるのか確認してみたいと思います。
ちなみに、追記になりますがCloudflareのZero Trust管理画面も日本語対応されました(2024年12月)!こちらのブログでも日本語画面で操作してみます!
一番簡単なデフォルトのページ
まずは、公式のドキュメントを参照。
それによると、ブロックページはDefault、Custom Redirect URL、Custom Page Templateと3種類あるのがわかります。
デフォルトの設定は Access > アプリケーション > 該当アプリケーション > 概要より設定できます。
デザインのカスタマイズは特にできませんが、エラーのテキストは変更できます。
Cloudflare エラーテキストに"You cannot access this page."と入力すると・・・
Cloudflareのロゴが入ったブロックページにしっかり反映されているのが確認できました。
次にCustom Page Templateを試す
PayGoもしくはエンタープライズ契約のあるアカウントであれば、カスタムページのデザインをCloudflareの管理画面で作成することができます。
設定 > カスタムページ > Access カスタム ページ より設定できます。
ちなみに、カスタムページには、Gatewayのブロックページ、App Launcherのカスタマイズ、ログインページのカスタマイズがありますがここではAccessカスタムページについて述べています。
カスタム HTMLにHTMLの記述を追加すると、独自デザインのブロックページが追加できました。
ちなみに、ドキュメントにはブロックページはIdentityとnon-identity
で分けることができると記載があります。
試しに、Non-identity failure(国、デバイスポスチャの失敗など)でブロックページを表示させてみます。日本語では、「非 ID 障害ブロック ページ」とありますのでこちらに別のテンプレートを設定。
ポリシーで日本からのアクセスを必須にして、シンガポールのIPからアクセスすると・・・
別のブロックページが表示されることを確認できました。
もっとダイナミックなエラーページを作成したい
セキュリティーの管理者としてはアクセスできない理由をブロックページに細かく表示させたいという要望もあるのではないでしょうか。デバイスポスチャ、アイデンティティーなど複雑なポリシーを適応する場合、ブロックページに足りない権限を記載できると便利ですね。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トークン > トークンを作成する > カスタムトークンを作成する
作成されたトークンをメモ。(トークンが表示されるのはこの時だけです)
2 Self Hostedのアプリケーションを追加。詳細は割愛しますが、ここではアプリケーションの追加が完了しているものとします。
3 アプリケーションのブロックページをRedirect URLに変更して、Workersのドメインを設定。この後Workersの仕込みを行います。
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
念の為、ローカルでテスト。
wrangler dev
ここまでできたら、一度デプロイしてRedirect URLとして使えるか確認してみましょう。
wrangler deploy
ちなみに、事前に作成しといたAPIトークンはSecretとしてwrangler.tomlには記載しなかったので、対応しておきましょう。
BEARER_TOKEN含め、設定で以下のようになっていれば取り急ぎOKです。
実際にRedirect URLが動いているか確認。あえてWARPをオフにしてアプリケーションにアクセス。
WARPがオフになっていることを教えてくれました!
https://your-domain/debug とdebugのページに遷移すると、ロゴのアップロードや色味の変更もできます。
ここからは慎重にカスタマイズが必要ですが、例えばデフォルトのテンプレートでは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のチェックは削除されました。
非常に便利なテンプレートではありますが、実際に利用する際には、このように慎重にカスタマイズする必要があります。