How to customize Cloudflare Access pages
By Flavio
Customize Cloudflare Access login, denial, and CLI pages with HTML and Liquid, and tell a blocked visitor what to do next.
Cloudflare Access can show your own HTML on the login page, and on the pages it shows when a policy blocks someone. A separate template covers the screen where they approve a command-line login. Liquid prints the app name and the email they used. You write the sentence that tells them what to do next.
The allow or block decision stays with the policy. The HTML is the page they read while that happens.
Kenny Johnson announced this on October 27, 2026, with a denial page that lists every check that failed. He said a real page does not have to look like that demo. The Access custom pages docs have the page types and the setup. The changelog entry covers the same change in a few lines.
Four pages, four templates
Each page type is its own template. Access picks one from what happened to the request.
| Page | When it shows | What you leave in the HTML |
|---|---|---|
| Login | The visitor has no session | One <cf-access-authbox> |
| Identity denied | They signed in, and an identity policy blocked them | No control of your own |
| Forbidden | A non-identity policy blocked them, such as service authentication | No control of your own |
| Interstitial | A command-line login is waiting for approval | One <cf-access-cli-approval> |
Access inserts its own controls into those two elements. Each one has to render once, outside any if or loop. Having the tag in the source is not enough if a condition skips it. Do not replace them with your own form, a link to an identity provider, or your own approve button.
Tell a blocked person what to do
I would write the identity denied page first. The visitor has already signed in, and the policy said no. Your copy can name the app and the email Access saw, then say who to contact.
On this page the app name is app.name. On the login page it is app_name. A name that does not exist for that page type prints as empty text, so the two are not interchangeable. The Variables list in the editor is the list for the type you selected.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>No access to {{ app.name }}</title>
</head>
<body>
<main>
<h1>No access to {{ app.name }}</h1>
<p>You reached an app that only a few people can open.</p>
{% if user.email %}
<p>You are signed in as {{ user.email }}.</p>
{% endif %}
<p>If you need access, email the person who runs this app and include the Ray ID below.</p>
{% if ray_id %}
<p>Ray ID: {{ ray_id }}</p>
{% endif %}
</main>
</body>
</html>
user.email and ray_id are optional. The if blocks skip them when Access does not send them. Liquid escapes what it prints, so the email stays text.
This page type can also receive policy_evaluation, the structured result of the checks. You choose which fields to show. The demo on X walks through each failed check. I would leave that off a page a visitor sees, because a group name or a claim is more than they need, and the docs say to skip details you do not need.
The login page
The login template shows the app name, any sign-in error, and the Access control. This is the starter from the docs, with the one element you cannot remove:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign in to {{ app_name }}</title>
</head>
<body>
<main>
<h1>Sign in to {{ app_name }}</h1>
{% if message.text.size > 0 %}
<p role="status">{{ message.text }}</p>
{% endif %}
<cf-access-authbox></cf-access-authbox>
</main>
</body>
</html>
Keep message.text on the page. That is how Access reports a problem with the sign-in. idps is display information for the identity providers. It is not a list of sign-in URLs, so do not paste one into the template.
The command-line approval page
The interstitial template is the screen someone sees when a CLI login needs a yes or a no. It needs this element once, outside any condition:
<cf-access-cli-approval></cf-access-cli-approval>
Access puts the approve and deny controls inside it. The template data for this type includes requesting_tool and an optional user. The approval credentials stay in the Access control, not in your HTML. Start from the starter the editor loads for this type, and add a line of context around that element.
Create it, then assign it to one application
In Cloudflare One, go to Reusable components, then Custom pages. On the Access custom pages card, select Manage, then Create custom page. Name it, pick a type, and the editor loads a starter. Edit the HTML, read Problems, fix the blocking errors, and select Publish. If validation is unavailable, select Retry validation before you publish.
Publishing only saves the template. Access shows it after you assign it to an application, or after you set it as the account default.
For a self-hosted application, the docs walk through the assignment like this:
- Go to Access controls, then Applications, and open the application.
- Open Additional settings. The section is still named Custom block pages. The login and interstitial settings are in that section too.
- Pick the row that matches the template. Identity failure block page takes an identity denied template, under Custom page template. Non-identity failure block page takes a forbidden template. Login page takes a login template, under Custom page. Interstitial page takes an interstitial template. Each dropdown only lists templates of that type.
- Save, then try the flow. Give the change a moment to propagate before you test again.
To remove an assignment, choose Cloudflare default for that page and save. If an account default exists, Access uses it when the application has no template of its own. With neither, Access uses the built-in page.
Set as default and Remove as default are actions on the Access custom pages list. An application assignment wins over the account default for the same type. Try one application before you change the default. A default reaches every application that inherits that page type.
The Problems panel checks the template for errors. It never renders the page, and the editor has no download. A clean panel can still hide an if that skips a line you expected to see. Open the application while signed in as someone the policy rejects, check a long email, and check a narrow window.
Keep a copy of the HTML outside the dashboard. Publishing an edit changes every application that uses that template. A second template is how you try a change without touching the ones already assigned. The type stays fixed after you create it. Before you delete a template, point its applications somewhere else and remove it as a default.
What the template cannot do
Templates run in a restricted Liquid environment.
- No custom JavaScript, no inline event handlers, and no authentication form of your own. Access owns the sign-in and the approval controls.
- The Liquid tags
include,render,layout,echo, andraware unavailable, and so is therawfilter. - HTML and CSS are sanitized. Images and fonts have to be HTTPS URLs the page’s Content Security Policy allows. Do not count on a third-party script to draw the page.
- Parsing, rendering, and memory have limits. Keep the template small, and skip a loop over a long list.
If a saved template is missing or cannot render safely, Access falls back to a built-in page. The allow or deny decision stays as it was.
How I would use it
I already put Access in front of Worker previews and in front of tunnels to machines at home. The built-in block page names the denial and stops, so the visitor still has to guess which app they opened and who can add them.
On one preview hostname I would use the identity denied template, and I would keep that HTML in the project’s repo. Then I would open the preview while signed in with an email the policy rejects, and read the page on a phone.
The account default can wait until that hostname looks right.
The login page can stay close to the starter, with the app name and message.text. The sign-in buttons have to stay inside <cf-access-authbox>.
I would leave the failed policies off the page, because the email and the Ray ID are enough for them to ask someone.
If the new editor is not there
The day after the announcement, a couple of people said the Access custom pages card still showed the older fields, with no Create custom page action. Kenny replied to one of them that he would check the account.
If that is what you see, the dashboard is still offering the legacy block pages. Those are the previous HTML templates. The HTML and Liquid pages are the ones in the docs linked above.
Want me to talk about your product? You can sponsor this site.
Related posts about cloudflare: