Skip to content

Cloudflare Access

Last updated View as MarkdownAgent setup

A public endpoint is unauthenticated by design. Anyone who knows the URL can query your indexed content.

Put a custom domain in front of the public endpoint and protect it with Cloudflare Access. Users then authenticate with your identity provider before any request reaches AI Search. This turns a public knowledge base into an internal one without writing an authentication layer.

How it works

A custom domain is a hostname in a zone that you own. When the CNAME record for that hostname is Proxied, requests pass through your own zone before they reach AI Search:

flowchart LR
  A[Client] --> B["Your zone<br/>Access, WAF, Bots"]
  B --> C["AI Search<br/>public endpoint"]
  C --> D[Your indexed content]

Access runs in your zone, so it evaluates every request first. Requests that fail a policy never reach AI Search. AI Search needs no configuration to support this and applies its own rate limits afterwards.

This routing is called orange-to-orange. It requires the CNAME record to be proxied. A DNS only record bypasses your zone entirely, and Access never runs.

Prerequisites

  • A custom domain on the instance or namespace you want to protect, with a Proxied CNAME record.
  • Cloudflare Access enabled on your account.
  • An identity provider connected to Cloudflare Access, or Cloudflare's one-time PIN.

1. Turn off the default hostname

Access only protects your custom domain. The default <PUBLIC_ENDPOINT_ID>.search.ai.cloudflare.com hostname does not pass through your zone, so it keeps answering unauthenticated requests and defeats the policy you are about to write.

Set default_domain_enabled to false before you create the Access application.

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-search/namespaces/default/instances/<INSTANCE_ID>" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "public_endpoint_params": {
      "enabled": true,
      "custom_domains": ["access.search.example.com"],
      "default_domain_enabled": false
    }
  }'

The default hostname now returns a 404 with error 60018.

2. Create an Access application

  1. In the Cloudflare dashboard, go to Zero Trust > Access controls > Applications.
  2. Select Add an application > Self-hosted.
  3. Name the application, for example AI Search.
  4. Add the public hostname of your custom domain, for example access.search.example.com.
  5. Save the application.

For the full set of options, refer to Publish a self-hosted application.

3. Add policies

An application with no policy denies every request. Add at least one Allow policy that describes who may query your content.

Action Rule type Selector Value
Allow Include Emails ending in @example.com

Use the email, country, IP range, or identity provider group selectors to match your organization. Refer to Common policies for more examples.

4. Authenticate non-browser clients

Browsers follow the Access login redirect and receive a CF_Authorization cookie. MCP clients, backend services, and scripts cannot complete an interactive login, so they need a service token.

  1. Go to Zero Trust > Access controls > Service credentials > Service Tokens.

  2. Select Create Service Token, name it, and choose a duration.

  3. Copy the Client ID and Client Secret. The secret is shown only once.

  4. Return to your Access application and add a second policy:

    Action Rule type Selector Value
    Service Auth Include Service Token ai-search-client

Send both credentials as headers on every request.

curl https://access.search.example.com/search \
  --header "Content-Type: application/json" \
  --header "CF-Access-Client-Id: <CLIENT_ID>" \
  --header "CF-Access-Client-Secret: <CLIENT_SECRET>" \
  --data '{
    "messages": [
      {
        "content": "How do I configure AI Search?",
        "role": "user"
      }
    ]
  }'
{
	"mcpServers": {
		"ai-search": {
			"url": "https://access.search.example.com/mcp",
			"headers": {
				"CF-Access-Client-Id": "<CLIENT_ID>",
				"CF-Access-Client-Secret": "<CLIENT_SECRET>"
			}
		}
	}
}

Header support varies by MCP client. If your client cannot send custom headers, refer to Secure MCP servers for identity-based alternatives, or use cloudflared access curl for command-line requests.

5. Verify

A request without credentials returns the Access login page instead of search results:

curl --include https://access.search.example.com/search \
  --header "Content-Type: application/json" \
  --data '{"messages":[{"content":"test","role":"user"}]}'

Confirm that the default hostname is closed:

curl https://<PUBLIC_ENDPOINT_ID>.search.ai.cloudflare.com/search \
  --header "Content-Type: application/json" \
  --data '{"messages":[{"content":"test","role":"user"}]}'

The response is a 404 with error code 60018.

Limitations

  • UI snippets on a public website stop working. UI snippets call the public endpoint from the visitor's browser. Behind Access, only visitors who pass your policies can use them. Use Access for internal sites, and leave the endpoint open for public marketing sites.
  • Cross-origin browser requests need an Access CORS configuration. If a page on a different origin calls the protected hostname, configure CORS settings on the Access application in addition to the allowed origins on the public endpoint.
  • Rate limits still apply. AI Search enforces its own rate limit after Access, and it is shared across all authenticated callers.
  • Allowed origins are not authentication. The authorized_hosts setting sets CORS response headers, which only browsers honor. It does not stop a direct request from curl or a script.

Alternatives

Once the CNAME record is proxied, other Cloudflare products also apply to the hostname:

Product Use it to
WAF custom rules Allow specific countries, ASNs, IP ranges, or headers
Rate limiting rules Apply per-client limits beyond the public endpoint limit
Bot Management Score and challenge automated traffic
Turnstile Verify humans before a browser client calls the endpoint

Next steps

Was this helpful?