English
Zero Trust Access Control Based on Cloudflare Access: A Perfect Solution from Canary Release to Official Launch

Use cases
Pre-release environment/Canary validation environment/Internal test environment
Core requirements Need to implement access control for an unfinished product, allowing only team members to access it, but do not want to implement login logic in the application code, and do not want Nginx basic authentication to conflict with application authentication.
In the development process of SaaS products, we often face a dilemma:How can we protect an unfinished preview product without interfering with normal development and debugging?
Traditional Nginx Basic Auth, although simple, will ... with the application's Authorization: Bearer Token Authentication conflicts arise. Worse still, when the product officially goes live, we often have to modify the Nginx configuration, restart services, or even change code to remove this "temporary wall".
This paper introduces a type of ... based on Cloudflare Access a zero-trust access control solution that can establish an authentication wall that can be toggled on and off at any time after DNS resolution and before requests reach the origin server, enabling seamless switching from “canary release” to “official launch,”No code or configuration changes required..
1. Problem Background: Pain Points of Traditional Solutions
1.1 Architecture Scenario
Suppose we have a standard SaaS project architecture:
| subdomain | Purpose | Tech stack |
|---|---|---|
dash.myapp.com | SaaS Console (Frontend) | Nuxt.js |
api.myapp.com | Backend API service | Golang |
1.2 Problems with Traditional Solutions
Issue 1: Conflict Between Nginx Basic Authentication and Application Authentication
Nginx Basic Authentication usage
Authorization: BasicRequest headerSaaS application certification requires
Authorization: BearerRequest headersThe two conflict, causing the application to be unable to log in normally
Issue 2: Configuration needs to be modified when going live.
Need to manually edit the Nginx configuration file.
Delete or comment out
auth_basicRelated instructionsExecute
nginx -s reloadRestart the serviceIt carries operational risk and is not elegant enough.
Problem 3: Poor management experience
Adding or removing team members requires manual modification.
.htpasswdFileUnable to by email domain (e.g.
@company.com) Batch authorizationWithout audit logs, there is no way to know who accessed it and when.
2. Solution: Cloudflare Access Zero Trust Access Control
2.1 What is Cloudflare Access?
Cloudflare Access is a core component of Cloudflare's Zero Trust architecture. It can Before the request reaches the origin server, perform authentication and access control on Cloudflare edge nodes.
Working principle:
2.2 Solution Selection: Zero Trust Free Is Completely Sufficient
Many people worry that Cloudflare Access requires payment. In fact,Zero Trust Free Plan Provides complete Access functionality:
| Function | Free Plan | Does it meet the requirements? |
|---|---|---|
| Self-hosted application | ✅ Support | ✅ Satisfied |
| Email domain policy | ✅ Support | ✅ Satisfied |
| Service Token | ✅ Support | ✅ Satisfied |
| Seat quantity | Free for 50 people | ✅ The team is sufficient |
| Cost | $0/month | ✅ Completely free |
Activation steps:
Go to the Cloudflare dashboard → Zero Trust
Select Zero Trust Free Plan
Confirm payment information (no charge will be made; used only for risk control)
Activation successful; you can now begin configuration.
2.3 The Essential Difference from Traditional Solutions
| Comparison dimension | Nginx Basic Authentication | Cloudflare Access |
|---|---|---|
| Authentication location | Origin server Nginx layer | Cloudflare edge nodes |
Whether occupied Authorization head | ✅ Yes, it causes conflict. | ❌ No, do not modify the request headers at all |
| Go-live cutover method | Modify the Nginx configuration and restart | Console one-click shutdown policy |
| Authentication method | Username/Password | Supports GitHub, Google, OTP, and email verification codes |
| Team management | Manual editing .htpasswd | Web interface management, supports email domain authorization |
| audit log | ❌ None | ✅ Complete record |
2.4 Why uniformly manage two subdomains?
During the canary release phase, we need to simultaneously protect:
Console: Prevent external users from accessing unfinished pages
API Prevent unauthorized API calls
Cloudflare Access allows us to ... for two subdomains.Create policies separately but use a unified management interface., implement:
Unified identity authentication source
Unified team permission management
Unified go-live cutover operation
3. Architecture Design
3.1 Overall Architecture Diagram
3.2 Core Design Principles
Authentication and business decoupling: Access control is handled entirely at the Cloudflare layer, and the origin Nginx and application do not need to be aware of it.
Unified policy management: Two subdomains are managed in the same console and can be switched with one click when going live.
Zero-configuration deployment: When officially launched, you only need to disable the Access policy; no server configuration changes are required.
4. Nginx configuration (simplified)
4.1 dash.myapp.com Configuration
nginx
# HTTP redirect to HTTPS
server {
listen 80;
server_name dash.myapp.com;
return 301 https://$server_name$request_uri;
}
# HTTPS Configuration
server {
listen 443 ssl http2;
server_name dash.myapp.com;
ssl_certificate /etc/nginx/ssl/myapp.com.pem;
ssl_certificate_key /etc/nginx/ssl/myapp.com.key;
# SSL optimization configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
# Log
access_log /var/log/nginx/dash.myapp.com-access.log;
error_log /var/log/nginx/dash.myapp.com-error.log;
# Cloudflare real IP (used with Access)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
location / {
# Note: There is no auth_basic configuration.
# Authentication is handled entirely by Cloudflare Access.
proxy_pass http://127.0.0.1:3100;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Optional: pass the logged-in user information to the backend
proxy_set_header X-Auth-User-Email $http_cf_access_authenticated_user_email;
}
# Static asset caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
proxy_pass http://127.0.0.1:3100;
expires 1y;
add_header Cache-Control "public, immutable";
}
}4.2 api.myapp.com Configuration
nginx
# HTTP redirect to HTTPS
server {
listen 80;
server_name api.myapp.com;
return 301 https://$server_name$request_uri;
}
# HTTPS Configuration
server {
listen 443 ssl http2;
server_name api.myapp.com;
ssl_certificate /etc/nginx/ssl/myapp.com.pem;
ssl_certificate_key /etc/nginx/ssl/myapp.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
access_log /var/log/nginx/api.myapp.com-access.log;
error_log /var/log/nginx/api.myapp.com-error.log;
# Cloudflare real IP
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
real_ip_header CF-Connecting-IP;
location / {
# Likewise, there is no auth_basic configuration at all.
proxy_pass http://127.0.0.1:2759;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Pass the user information authenticated by the Service Token
proxy_set_header X-Auth-User-Email $http_cf_access_authenticated_user_email;
}
# Static asset caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ {
proxy_pass http://127.0.0.1:2759;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check authentication exemption
location = /health {
proxy_pass http://127.0.0.1:2759;
proxy_set_header Host $host;
}
}5. Cloudflare Access Configuration in Practice
5.1 Activate the Zero Trust Free plan
Before starting configuration, you first need to activate the Cloudflare Zero Trust Free plan:
Log in to the Cloudflare dashboard and click the left menu's Zero Trust
If it is your first time using it, you will see the plan selection page.
Select Zero Trust Free Plan (
$0/seat/month, free for 50 people)Confirm payment information (requires linking a credit card or PayPal,For risk control verification only; no charge will be incurred.)
Click Activate Complete activation
After successful activation, you can begin configuring the Access app.
5.2 For dash.myapp.com Configure interactive logon protection
Step 1: Create a Self-hosted application
Enter Zero Trust→Access → Applications
Click Add an application→ Select Self-hosted
Step 2: Fill in the basic application information
| Field | Fill in the content | Explanation |
|---|---|---|
| Application name | dash-myapp | Application name, for easy identification |
| Subdomain | dash | Subdomain part |
| Domain | myapp.com | Primary domain |
| Path | Leave blank (or fill in *) | Protect all paths |
After completing, click Add public hostname.
Step 3: Configure access policies
向下滚动到 Access policies Region
Click Add a policy
Fill in policy information:
| field | Value | Description |
|---|---|---|
| Policy name | Allow Team Members | Strategy Name |
| Action | Allow | Allow eligible requests |
| Selector | Emails Ending In | Select email suffix match |
| Value | @myapp.com | Your team's email domain |
Click Save policy
Click at the bottom of the page Save Complete app creation
Verify configuration: Access in browser incognito mode https://dash.myapp.com, it should automatically redirect to the Cloudflare Access login page, enter @myapp.com After entering your email, you can log in once you receive the verification code.
5.3 is api.myapp.com Configure Service Token (service-to-service invocation)
For the API subdomain, we need to support two access methods:
Inter-service invocation: Frontend service (Nuxt backend) makes silent calls, with no manual interaction required
Team member access: Developers debug APIs through the browser
This requires configuration.Service Token and Two access policies.
Step 1: Create Service Token
In the Access left menu, click Service Credentials→Service Tokens
Click Create Service Token
Fill in information:
| field | Fill in the content | Description |
|---|---|---|
| Name | dash-apiserver | Token name; it is recommended to associate it with the caller. |
| Duration | 2 years | Validity period: it is recommended to set it longer. |
Click Generate token
⚠️ Key step: Save the generated immediately
Client IDandClient Secret!Client ID format example:
888/104a908c1a61de52c89237/26bee0.accessClient Secret is a long string of random characters:
9eca11cf456625c4ac861f327eee7988...The secret will only be displayed once and cannot be viewed again after closing the page!
Step 2: Create API App
回到 Access → Applications
Click Create new application → Self-hosted
Fill in the application information:
| field | Fill in the content |
|---|---|
| Application name | api-myapp |
| Subdomain | api |
| Domain | myapp.com |
| Path | Leave blank (or fill in *) |
Click Add public hostname.
Step 3: Configure the first policy (allow Service Token)
Click Add a policy
Fill in policy information:
| field | value | Explanation |
|---|---|---|
| Policy name | Allow Service Token | Strategy Name |
| Action | Service Auth | ⚠️ Note: not Allow! |
| Selector | Service Token | 选择服务令牌 |
| Value | dash-apiserver | Select the newly created Token |
- Click Save policy
Why should Action choose Service Auth?Allow 策略会要求同时满足身份认证(如邮箱登录),导致 Service Token 请求被重定向到登录页。 Service Auth Dedicated to service-to-service calls and does not require additional authentication.
Step 4: Configure the second policy (team member access)
Click again Add a policy
Fill in policy information:
| field | value |
|---|---|
| Policy name | Allow Team Members |
| Action | Allow |
| Selector | Emails Ending In |
| Value | @myapp.com |
- Click Save policy
Step 5: Adjust the strategy order
In the policy list, ensure Allow Service Token Strategy comes first.,Allow Team Members In second place.
Policy order affects matching logic: Cloudflare Access evaluates policies from top to bottom and stops after the first match. Placing the Service Token policy first ensures that requests carrying a Token are allowed through first.
Step 6: Save the application
Click at the bottom of the page Save Complete the application creation.
5.4 Test Verification
Test 1: Browser access (team members)
Access in browser incognito mode https://api.myapp.com/health:
Automatically redirect to the Cloudflare Access login page
Input
@myapp.comemailReceive verification code and log in
✅ Successfully saw the API response:
json
{"service":"myapp-apiserver","status":"ok","timestamp":"2026-05-26T14:23:48Z"}Test 2: Service Token call
Use the curl command to test service-to-service calls:
bash
curl -H "CF-Access-Client-Id: 888/104a908c1a61de52c89237/26bee0.access" \
-H "CF-Access-Client-Secret: 9eca11cf456625c4ac861f327eee798824496842d718f1b248b3c36936a2b238" \
https://api.myapp.com/healthExpected result: return 200 and a JSON response directly, with no redirect and no login page.
Test 3: Unauthorized Access
bash
curl https://api.myapp.com/healthExpected result: returns a 302 redirect to the Cloudflare Access login page.
5.5 Common Configuration Errors and Solutions
| Error symptoms | Possible causes | Solution |
|---|---|---|
| curl request returns 302 redirect | Policy Action used Allow | Change to Service Auth |
| curl request returns a 302 redirect | Policy order error | Move the Service Token policy to the first position |
| Unable to log in in the browser | Email domain input error | Confirm it is @myapp.com Format |
| Configuration does not take effect | Forgot to save | Ensure that it has been clicked Save policy and Save |
| Service Token not found | Not created or expired | Recreate in Service Credentials |
5.6 Frontend Service Configuration
In Nuxt's backend code, use a Service Token to call the API:
javascript
// server/api/call-backend.js
export default defineEventHandler(async (event) => {
const response = await $fetch('https://api.myapp.com/user/info', {
headers: {
'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID,
'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET,
}
})
return response
})Environment variable configuration (.env):
ini
CF_ACCESS_CLIENT_ID=888/104a908c1a61de52c89237/26bee0.access
CF_ACCESS_CLIENT_SECRET=9eca11cf456625c4ac861f327eee798824496842d718f1b248b3c36936a2b2386. Go-live cutover operation
When the product is ready for official launch:
| Operation | Steps | Effect |
|---|---|---|
Close dash Protection | Access → Applications → Click the application → Change the status from On Change to Off | Everyone can access the console directly without logging in. |
Close api Protection | Access → Applications → Click the application → Change the status from On Change to Off | The API no longer requires a Service Token or email authentication. |
It's that simple. No need to modify any Nginx configuration or restart any services.
7. Canary mode vs. production mode comparison
| visitor | Grayscale mode (Access enabled) | Online mode (Access disabled) |
|---|---|---|
| Team members (email @myapp.com) | ✅ Normal access | ✅ Normal access |
| External users (other email addresses) | ❌ Blocked by Access | ✅ Normal access |
| API call (without Service Token) | ❌ Returns a 302 redirect | ✅ Normal response |
| Frontend service call (with Service Token) | ✅ Normal call | ✅ Normal call |
| Nginx configuration | No changes needed | No changes needed. |
| Application code | No changes needed | No changes needed. |
8. Summary of Solution Advantages
| advantage | Description |
|---|---|
| ✅ Resolve head conflict | Does not occupy Authorization Header, Token authentication for SaaS applications is completely unaffected. |
| ✅ Zero-config launch | When going live, you only need to disable the policy in the Cloudflare dashboard; no server configuration changes are required. |
| ✅ Unified management | The console and API subdomains are managed in the same interface, with clear rules. |
| ✅ Team-friendly | Supports batch authorization by email domain (e.g.@myapp.com), no need to maintain a password file |
| ✅ Audit log | Fully record who logged in, when, and from where to meet security compliance requirements. |
| ✅ Low cost | Cloudflare Access provides 50 users The free quota is completely sufficient. |
| ✅ Easy to undo | When a team member leaves, you only need to remove their permissions in the Access console; there is no need to modify the server. |
9. Frequently Asked Questions
Q1: After official launch, what should we do if the API still wants to retain Token authentication?
A: Absolutely no problem. After Cloudflare Access is disabled,Authorization The header is no longer occupied by any middleware, and your Golang API can parse its own JWT token normally.
Q2: Does Cloudflare Access affect SEO?
A: SEO is not needed during the gray release phase. After official launch, disabling Access will not affect it. Alternatively, you can configure Bypass rules for search engines.
Q3: How secure is the Service Token?
A: It is recommended to use the Service Token only on the frontend.Server-side Use it in code and do not expose it on the client. The Client Secret is displayed only once at creation, so keep it safe.
Q4: Does this plan require payment?
A: Cloudflare Access provides 50 users The free quota is completely sufficient for canary release scenarios in most teams.
Q5: Why choose Zero Trust Free instead of other plans?
A: The Free plan already includes core features such as Self-hosted apps, Service Tokens, and email domain policies. It is completely free for up to 50 people, with no paid upgrade required.
Q6: Why use the Service Token strategy?Service Auth rather than Allow?
A:Allow The policy requires identity authentication (such as email login) to be satisfied as well, causing Service Token requests to be redirected to the login page.Service Auth Specifically for service-to-service calls, no additional authentication is required.
10. Summary
With Cloudflare Access, we implemented a perfect access control solution for canary releases:
Canary release phase: Access protection at edge nodes
dash.andapi., Team members log in via email verificationOfficially launched: Simply disable the policy in the console, and the product will immediately be available to the entire network.
No need to modify Nginx or application code throughout the entire process. The authentication layer and the application layer are completely decoupled.
Quick memorization
Hand authentication over to Cloudflare; Nginx only acts as a proxy; disable it with one click when going live; no code changes are needed throughout. The Free plan is completely sufficient; Service Tokens must use Service Auth!
Appendix: Placeholder Replacement Checklist
| placeholder | Example value | Description |
|---|---|---|
myapp.com | yourdomain.com | Main domain |
dash.myapp.com | preview.yourdomain.com | Console domain |
api.myapp.com | api.yourdomain.com | API domain name |
@myapp.com | @company.com | Team email domain |
dash-apiserver | frontend-backend-token | Service Token Name |
Related blog posts
Cloudflare Series
CI/CD Series
Nuxt.js Project GitHub Actions Automated CI/CD Practice (Part 1): Build and Release
Nuxt.js Project GitHub Actions Automated CI/CD Practices (Part 2): Remote Deployment
Nuxt.js Project GitHub Actions Automated CI/CD Practice (Part 3): Email Notifications
Practice of Automated CI/CD with GitHub Actions for VitePress Projects