Additional Security Layer for Cloudflare Access Applications
CFTL provides an extra validation layer on top of Cloudflare Zero Trust, adding application-level security with AUD validation and email-based access control.
| Layer | Component | Purpose |
|---|---|---|
| 1 | Cloudflare Access | Identity validation (SSO, MFA, policies) |
| 2 | Cloudflare Tunnel | Secure network connection + JWT validation |
| 3 | CFTL (This System) | Additional AUD validation + email restrictions |
services:
cftl:
image: hezzit/cftl:latest
container_name: cftl
restart: always
environment:
- TUNNEL_TOKEN=your_tunnel_token_here # v aud v comma-separated list of emails
- CONFIGS=your-app.example.com:my-app:3000:your_aud:[email protected]
# ^ hostname ^ service:port
networks:
- app-network
depends_on:
- app
# Your application
my-app:
image: your-app:latest
container_name: my-app
networks:
- app-network
# No external ports - access only through CFTL
networks:
app-network:
external: falseCFTL uses a flexible alias-based configuration system that allows you to define reusable components and organize your services in multiple ways.
# Define reusable components with aliases
AUDS=aud_alias:aud_value
EMAILS=email_alias:[email protected],[email protected]
HOSTNAMES=hostname_alias:hostname.example.com
SERVICES=service_alias:service_name
# Configure services using aliases
CONFIGS=hostname_alias:service_alias:port:aud_alias:email_alias# Define components
AUDS=prod:abc123def456789
EMAILS=team:[email protected],[email protected]
HOSTNAMES=app:myapp.example.com
SERVICES=backend:my-backend-service
# Configure the service
CONFIGS=app:backend:3000:prod:team# Define shared components
AUDS=prod:abc123def456789
EMAILS=admin:[email protected]|dev:[email protected]
HOSTNAMES=api:api.example.com|web:web.example.com
SERVICES=backend:backend-api|frontend:nginx
# Configure each service
CONFIGS_API=api:backend:3000:prod:admin
CONFIGS_WEB=web:frontend:80:prod:dev# Development environment
AUDS_DEV=dev:dev_aud_123
EMAILS_DEV=devteam:[email protected],[email protected]
HOSTNAMES_DEV=ide:ide.dev.example.com|debug:debug.dev.example.com
SERVICES_DEV=vscode:code-server
CONFIGS_DEV_IDE=ide:vscode:8080:dev:devteam
CONFIGS_DEV_DEBUG=debug:vscode:8081:dev:devteam
# Production environment
AUDS_PROD=prod:prod_aud_456
EMAILS_PROD=ops:[email protected]
HOSTNAMES_PROD=app:app.example.com|api:api.example.com
SERVICES_PROD=web:nginx|api:backend
CONFIGS_PROD_APP=app:web:80:prod:ops
CONFIGS_PROD_API=api:api:3000:prod:ops# Frontend team resources
AUDS_FRONTEND=fe:frontend_aud
EMAILS_FRONTEND=fe_team:[email protected]
HOSTNAMES_FRONTEND=app:app.example.com|preview:preview.example.com
# Backend team resources
AUDS_BACKEND=be:backend_aud
EMAILS_BACKEND=be_team:[email protected]
HOSTNAMES_BACKEND=api:api.example.com|admin:admin.example.com
# Shared services
SERVICES=nginx:nginx|node:nodejs|python:python-app
# Service configurations
CONFIGS_FE_APP=app:nginx:80:fe:fe_team
CONFIGS_FE_PREVIEW=preview:node:3000:fe:fe_team
CONFIGS_BE_API=api:python:8000:be:be_team
CONFIGS_BE_ADMIN=admin:node:4000:be:be_team# When you don't need aliases, values without ':' use '0' as default alias
AUDS=abc123def456789
[email protected],[email protected]
HOSTNAMES=app.example.com
SERVICES=backend
# Reference using '0' alias
CONFIGS=0:0:3000:0:0# Empty AUD and emails fields = no third layer protection
CONFIGS_PUBLIC=public:nginx:80::CONFIGS_API=api:backend:3000:prod:
# ^ no email restriction# You can mix all patterns in one deployment
AUDS=prod:prod_aud|staging:staging_aud
[email protected] # Uses '0' as default alias
HOSTNAMES_PROD=app:app.example.com
HOSTNAMES_DEV=dev:dev.example.com
SERVICES=backend:my-backend|frontend:nginx
# Multiple CONFIGS variations
CONFIGS=app:backend:3000:prod:0|dev:frontend:80:staging:0 # Using default email alias
CONFIGS_PUBLIC=public.example.com:nginx:80:: # Direct values without aliasesFirst, set up your identity provider integration:
π Guide: Identity Provider Integration
Popular options:
- GitHub OAuth
- Azure AD
- Generic OIDC
Define who can access your applications:
π Guide: Access Policies
Example policy:
- Policy Name: "Admin Access"
- Rule: Email contains
[email protected] - Action: Allow
Set up your application in Cloudflare Access:
π Guide: Self-Hosted Applications
Configuration:
- Application Name: Your App Name
- Application Domain:
your-app.example.com - Authentication Method: Select your configured method (GitHub, Google, etc.)
- Policies: Select your access policy
- Save and copy the Application AUD (you'll need this later)
Set up secure connection to your application:
π Guide: Create Remote Tunnel
Configuration:
- Tunnel Name: Your tunnel name
- Public Hostname:
your-app.example.com(same as Access app) - Service:
http://localhost:8080(CFTL proxy port)
- Navigate to Access tab in tunnel configuration
- Enable "Validate JWT"
- Select your Access Application (created in Step 3)
- Save and copy the Tunnel Token
Choose your configuration style:
CONFIGS=your-app.example.com:your-service:3000:your_aud_from_step_3:[email protected]TUNNEL_TOKEN=your_tunnel_token
# Define all your AUDs
AUDS=prod:aud_123|staging:aud_456
# Define email groups
EMAILS=admin:[email protected]|dev:[email protected],[email protected]
# Define all hostnames
HOSTNAMES=app:app.example.com|api:api.example.com|admin:admin.example.com
# Define service types
SERVICES=web:nginx|backend:nodejs-api
# Configure each service
CONFIGS_APP=app:web:80:prod:admin
CONFIGS_API=api:backend:3000:prod:dev
CONFIGS_ADMIN=admin:backend:4000:staging:admindocker-compose up -d| Variable | Description | Required | Example |
|---|---|---|---|
TUNNEL_TOKEN |
Cloudflare Tunnel token | β | eyJhbGci... |
PORT |
CFTL listening port | β | 8080 (default) |
VERBOSE |
Enable verbose logging | β | false (default) |
| Variable Pattern | Description | Example |
|---|---|---|
AUDS* |
AUD definitions | AUDS=prod:abc123 or AUDS_DEV=dev:xyz789 |
EMAILS* |
Email group definitions | EMAILS=admin:[email protected] |
HOSTNAMES* |
Hostname definitions | HOSTNAMES=app:app.example.com |
SERVICES* |
Service type definitions | SERVICES=backend:my-backend |
CONFIGS* |
Service configurations | CONFIGS_APP=app:backend:3000:prod:admin |
Note: All patterns support multiple definitions (AUDS, AUDS_1, AUDS_PROD, etc.)
- User visits
your-app.example.com - Cloudflare Access validates identity (Layer 1)
- Cloudflare Tunnel validates JWT and forwards to CFTL (Layer 2)
- CFTL validates AUD and email authorization (Layer 3)
- If all layers pass, request is forwarded to your application
When authentication is successful, your application receives these HTTP headers:
| Header | Description | Example |
|---|---|---|
X-Auth-User-Email |
User's email address | [email protected] |
X-Auth-User-ID |
Unique user ID (CF Access sub) | 7335d417-61da-459d-899c-0a01c76a2f94 |
X-Auth-User-Country |
User's authentication country | US |
X-Auth-Method |
Authentication method used | cf-access-third-layer |
X-Auth-Service |
Service name (internal) | app_example_com |
X-Auth-AUD |
Validated CF Access AUD | abc123def456... |
X-Auth-Issuer |
CF Access issuer URL | https://yourteam.cloudflareaccess.com |
X-Auth-Token-Type |
Token type | app |
X-Auth-Identity-Nonce |
CF identity cache key | 6ei69kawdKzMIAPF |
Flask (Python):
@app.route('/')
def index():
user_email = request.headers.get('X-Auth-User-Email')
user_country = request.headers.get('X-Auth-User-Country')
return f"Welcome {user_email} from {user_country}"Express.js (Node.js):
app.get('/', (req, res) => {
const userEmail = req.headers['x-auth-user-email'];
const userCountry = req.headers['x-auth-user-country'];
res.send(`Welcome ${userEmail} from ${userCountry}`);
});Note: Only services with third layer protection receive user headers. Services without third layer (bypass mode) are direct proxied without authentication headers.
docker-compose logs cftl| Issue | Possible Cause | Solution |
|---|---|---|
DENIED: No CF Access token |
Domain mismatch between Access App and CFTL | Ensure Access Application domain matches CFTL hostname exactly |
DENIED: AUD mismatch |
Wrong AUD in configuration | Copy correct AUD from Access Application |
DENIED: Unauthorized email |
Email not in authorized list | Add email to EMAILS configuration |
| Nginx error page (white/plain) | Issue between CFTL and your app | Check app connectivity and port configuration |
| Browser error (empty response/connection reset) | Issue between Tunnel and Access App | Ensure tunnel hostname matches Access Application domain exactly |
| Tunnel not connecting | Invalid tunnel token | Generate new token from tunnel settings |
# Test if tunnel is working
curl -I https://your-app.example.com
# Check CFTL logs for third layer validation
docker-compose logs -f cftlThis project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ by Hezzit. Contributions are welcome!
β If this project helped you, please give it a star!