mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
V2
This commit is contained in:
@@ -9,31 +9,294 @@ metadata:
|
||||
|
||||
# Cloudflare Pages
|
||||
|
||||
Deploy frontend projects with preview builds and edge functions.
|
||||
Deploy frontend projects with preview builds, edge functions, and global CDN delivery on Cloudflare's network.
|
||||
|
||||
## Connect Project
|
||||
## When to Use
|
||||
|
||||
1. Create a Pages project in Cloudflare dashboard.
|
||||
2. Link your GitHub repository.
|
||||
3. Set build command and output directory.
|
||||
4. Configure environment variables per environment.
|
||||
- Deploying static sites (React, Vue, Astro, Hugo, Next.js static export).
|
||||
- Full-stack applications using Pages Functions for server-side logic.
|
||||
- Projects that need automatic preview deployments per pull request.
|
||||
- Teams that want zero-config CDN with custom domain and TLS.
|
||||
- Migrating from Vercel, Netlify, or GitHub Pages to Cloudflare's ecosystem.
|
||||
|
||||
## Wrangler-Based Deploy
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ and npm installed locally.
|
||||
- A Cloudflare account (free tier works for most projects).
|
||||
- Wrangler CLI installed: `npm install -g wrangler`.
|
||||
- Authenticated via `wrangler login` or `CLOUDFLARE_API_TOKEN` environment variable.
|
||||
- Source code in a Git repository (GitHub or GitLab for dashboard integration).
|
||||
|
||||
## Project Setup via Wrangler
|
||||
|
||||
### Create a New Project
|
||||
|
||||
```bash
|
||||
npm install -D wrangler
|
||||
# Create a new Pages project
|
||||
npx wrangler pages project create my-site
|
||||
npx wrangler pages deploy dist --project-name=my-site
|
||||
|
||||
# List existing projects
|
||||
npx wrangler pages project list
|
||||
|
||||
# Delete a project (removes all deployments)
|
||||
npx wrangler pages project delete my-site
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
### Deploy from Local Build Output
|
||||
|
||||
- Require previews for pull requests.
|
||||
- Separate production and preview secrets.
|
||||
- Enable Web Analytics for performance visibility.
|
||||
- Add Cloudflare WAF rules for abuse protection.
|
||||
```bash
|
||||
# Build your framework first
|
||||
npm run build
|
||||
|
||||
# Deploy the output directory
|
||||
npx wrangler pages deploy dist --project-name=my-site
|
||||
|
||||
# Deploy with a custom branch name (triggers preview URL)
|
||||
npx wrangler pages deploy dist --project-name=my-site --branch=feature-auth
|
||||
|
||||
# Deploy and get the deployment URL in JSON
|
||||
npx wrangler pages deploy dist --project-name=my-site --branch=main 2>&1 | tail -1
|
||||
```
|
||||
|
||||
### List and Manage Deployments
|
||||
|
||||
```bash
|
||||
# List recent deployments
|
||||
npx wrangler pages deployment list --project-name=my-site
|
||||
|
||||
# Tail live logs from a deployment
|
||||
npx wrangler pages deployment tail --project-name=my-site --environment=production
|
||||
```
|
||||
|
||||
## Dashboard Git Integration
|
||||
|
||||
1. Navigate to **Workers & Pages > Create application > Pages**.
|
||||
2. Connect your GitHub or GitLab account.
|
||||
3. Select the repository and configure:
|
||||
- **Production branch**: `main`
|
||||
- **Build command**: `npm run build`
|
||||
- **Build output directory**: `dist` (or `build`, `.next`, `public` depending on framework)
|
||||
4. Set environment variables per environment (Production vs Preview).
|
||||
|
||||
### Framework Presets
|
||||
|
||||
Cloudflare auto-detects frameworks. Override if needed:
|
||||
|
||||
| Framework | Build Command | Output Directory |
|
||||
|------------|----------------------|------------------|
|
||||
| React CRA | `npm run build` | `build` |
|
||||
| Vite | `npm run build` | `dist` |
|
||||
| Next.js | `npx @cloudflare/next-on-pages` | `.vercel/output/static` |
|
||||
| Astro | `npm run build` | `dist` |
|
||||
| Hugo | `hugo` | `public` |
|
||||
| SvelteKit | `npm run build` | `.svelte-kit/cloudflare` |
|
||||
|
||||
## Preview Deployments
|
||||
|
||||
Every non-production branch gets a unique preview URL automatically.
|
||||
|
||||
```
|
||||
# URL format for preview deployments
|
||||
https://<commit-hash>.<project-name>.pages.dev
|
||||
https://<branch-name>.<project-name>.pages.dev
|
||||
```
|
||||
|
||||
### Branch-Based Access Control
|
||||
|
||||
```bash
|
||||
# Set preview branch patterns in wrangler.toml (Pages-specific)
|
||||
# Or configure via dashboard: Settings > Builds & deployments
|
||||
# Include branches: feature/*, staging
|
||||
# Exclude branches: dependabot/*
|
||||
```
|
||||
|
||||
### Preview Comment on Pull Requests
|
||||
|
||||
Enable the Cloudflare Pages GitHub App to post deployment URLs as PR comments. Configure under **Settings > Builds & deployments > Preview comment**.
|
||||
|
||||
## Pages Functions
|
||||
|
||||
Pages Functions provide server-side logic deployed alongside your static site. Place files in a `functions/` directory at the project root.
|
||||
|
||||
### Basic API Route
|
||||
|
||||
```typescript
|
||||
// functions/api/hello.ts
|
||||
export const onRequestGet: PagesFunction = async (context) => {
|
||||
return new Response(JSON.stringify({ message: "Hello from the edge" }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
// functions/api/users/[id].ts — dynamic route parameter
|
||||
export const onRequestGet: PagesFunction = async (context) => {
|
||||
const userId = context.params.id;
|
||||
return new Response(JSON.stringify({ userId }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```typescript
|
||||
// functions/_middleware.ts — runs before all routes
|
||||
export const onRequest: PagesFunction = async (context) => {
|
||||
const authHeader = context.request.headers.get("Authorization");
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
return context.next();
|
||||
};
|
||||
```
|
||||
|
||||
### Functions with Bindings
|
||||
|
||||
```typescript
|
||||
// functions/api/data.ts — using KV and D1 bindings
|
||||
interface Env {
|
||||
MY_KV: KVNamespace;
|
||||
MY_DB: D1Database;
|
||||
MY_BUCKET: R2Bucket;
|
||||
}
|
||||
|
||||
export const onRequestGet: PagesFunction<Env> = async (context) => {
|
||||
// Read from KV
|
||||
const cached = await context.env.MY_KV.get("key");
|
||||
if (cached) return new Response(cached);
|
||||
|
||||
// Query D1
|
||||
const result = await context.env.MY_DB.prepare(
|
||||
"SELECT * FROM items LIMIT 10"
|
||||
).all();
|
||||
|
||||
// Cache in KV
|
||||
await context.env.MY_KV.put("key", JSON.stringify(result.results), {
|
||||
expirationTtl: 300,
|
||||
});
|
||||
|
||||
return Response.json(result.results);
|
||||
};
|
||||
```
|
||||
|
||||
## Wrangler Configuration
|
||||
|
||||
```toml
|
||||
# wrangler.toml — Pages project configuration
|
||||
name = "my-site"
|
||||
compatibility_date = "2024-09-01"
|
||||
pages_build_output_dir = "dist"
|
||||
|
||||
# KV namespace binding
|
||||
[[kv_namespaces]]
|
||||
binding = "MY_KV"
|
||||
id = "abc123def456"
|
||||
|
||||
# D1 database binding
|
||||
[[d1_databases]]
|
||||
binding = "MY_DB"
|
||||
database_name = "my-app-db"
|
||||
database_id = "xxxx-yyyy-zzzz"
|
||||
|
||||
# R2 bucket binding
|
||||
[[r2_buckets]]
|
||||
binding = "MY_BUCKET"
|
||||
bucket_name = "app-assets"
|
||||
|
||||
# Environment variables
|
||||
[vars]
|
||||
API_BASE_URL = "https://api.example.com"
|
||||
```
|
||||
|
||||
## Headers and Redirects
|
||||
|
||||
### Custom Headers
|
||||
|
||||
```
|
||||
# public/_headers
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'
|
||||
|
||||
/api/*
|
||||
Access-Control-Allow-Origin: https://example.com
|
||||
Access-Control-Allow-Methods: GET, POST, OPTIONS
|
||||
```
|
||||
|
||||
### Redirects
|
||||
|
||||
```
|
||||
# public/_redirects
|
||||
/old-page /new-page 301
|
||||
/blog/:slug /posts/:slug 301
|
||||
/docs/* https://docs.example.com/:splat 302
|
||||
/home / 302
|
||||
```
|
||||
|
||||
## Custom Domains
|
||||
|
||||
```bash
|
||||
# Add a custom domain via Cloudflare dashboard:
|
||||
# Pages project > Custom domains > Set up a custom domain
|
||||
|
||||
# Or via API
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/pages/projects/my-site/domains" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"www.example.com"}'
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy to Cloudflare Pages
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy dist --project-name=my-site
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Build fails with out-of-memory | Build exceeds 1 GB RAM limit | Reduce dependencies; use `NODE_OPTIONS=--max_old_space_size=768` |
|
||||
| Functions return 404 | `functions/` directory not at project root | Move `functions/` to repo root, not inside `src/` |
|
||||
| Preview URL shows old content | Browser cache or stale deployment | Hard refresh; check deployment list for latest commit hash |
|
||||
| Custom domain shows SSL error | DNS not proxied through Cloudflare | Enable orange cloud (proxy) on the CNAME record |
|
||||
| `_headers` file ignored | File not in build output directory | Place in `public/` so it copies to `dist/` during build |
|
||||
| Bindings undefined in Functions | Missing `wrangler.toml` or dashboard config | Add bindings in `wrangler.toml` and redeploy |
|
||||
| 1 MB function size limit exceeded | Too many dependencies bundled | Tree-shake; move large deps to KV or R2 |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [cloudflare-workers](../cloudflare-workers/) - Edge backend logic
|
||||
- [vercel-deployments](../../platforms/vercel-deployments/) - Alternative frontend hosting
|
||||
- [cloudflare-workers](../cloudflare-workers/) - Edge backend logic and API routes
|
||||
- [cloudflare-r2](../cloudflare-r2/) - Object storage for assets and uploads
|
||||
- [cloudflare-zero-trust](../cloudflare-zero-trust/) - Protect preview deployments with Access policies
|
||||
- [cdn-setup](../../networking/cdn-setup/) - General CDN configuration patterns
|
||||
|
||||
@@ -9,34 +9,329 @@ metadata:
|
||||
|
||||
# Cloudflare R2
|
||||
|
||||
Use S3-compatible object storage without egress fees.
|
||||
S3-compatible object storage with zero egress fees, built on Cloudflare's global network.
|
||||
|
||||
## Setup
|
||||
## When to Use
|
||||
|
||||
- Storing user uploads, media files, backups, or static assets.
|
||||
- Replacing AWS S3 to eliminate egress costs for read-heavy workloads.
|
||||
- Serving files at the edge via Workers or public bucket access.
|
||||
- Building multi-cloud storage that avoids vendor lock-in (S3 API compatible).
|
||||
- Storing ML model artifacts, training data, or inference results.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Cloudflare account with R2 enabled (dashboard > R2 > subscribe).
|
||||
- Wrangler CLI v3+ installed: `npm install -g wrangler`.
|
||||
- Authenticated via `wrangler login` or `CLOUDFLARE_API_TOKEN`.
|
||||
- For S3 API access: R2 API token created under **R2 > Manage R2 API Tokens**.
|
||||
|
||||
## Bucket Management with Wrangler
|
||||
|
||||
### Create and List Buckets
|
||||
|
||||
```bash
|
||||
# Create bucket
|
||||
# Create a new bucket
|
||||
npx wrangler r2 bucket create app-assets
|
||||
|
||||
# List buckets
|
||||
# Create a bucket in a specific region (hint for data locality)
|
||||
npx wrangler r2 bucket create eu-uploads --location=eu
|
||||
|
||||
# List all buckets
|
||||
npx wrangler r2 bucket list
|
||||
|
||||
# Upload object
|
||||
npx wrangler r2 object put app-assets/logo.png --file ./logo.png
|
||||
# Delete an empty bucket
|
||||
npx wrangler r2 bucket delete old-bucket
|
||||
```
|
||||
|
||||
## S3-Compatible Access
|
||||
### Object Operations
|
||||
|
||||
- Generate R2 API tokens with least privilege.
|
||||
- Use endpoint format: `https://<accountid>.r2.cloudflarestorage.com`.
|
||||
- Configure lifecycle rules for archive/delete.
|
||||
```bash
|
||||
# Upload a single file
|
||||
npx wrangler r2 object put app-assets/images/logo.png --file=./logo.png
|
||||
|
||||
## Best Practices
|
||||
# Upload with content type
|
||||
npx wrangler r2 object put app-assets/data/report.json \
|
||||
--file=./report.json \
|
||||
--content-type="application/json"
|
||||
|
||||
- Use short-lived signed URLs for private content.
|
||||
- Store user uploads in tenant-specific prefixes.
|
||||
- Enable object versioning for recovery-critical buckets.
|
||||
# Download an object
|
||||
npx wrangler r2 object get app-assets/images/logo.png --file=./downloaded-logo.png
|
||||
|
||||
# Delete an object
|
||||
npx wrangler r2 object delete app-assets/images/old-logo.png
|
||||
|
||||
# Get object metadata
|
||||
npx wrangler r2 object head app-assets/images/logo.png
|
||||
```
|
||||
|
||||
## S3-Compatible API Access
|
||||
|
||||
R2 supports the S3 API, so existing tools (AWS CLI, boto3, s3cmd) work out of the box.
|
||||
|
||||
### Generate R2 API Tokens
|
||||
|
||||
1. Go to **R2 > Manage R2 API Tokens > Create API token**.
|
||||
2. Select permissions: Object Read & Write, or Object Read only.
|
||||
3. Scope to specific buckets if possible.
|
||||
4. Save the Access Key ID and Secret Access Key.
|
||||
|
||||
### AWS CLI Configuration
|
||||
|
||||
```bash
|
||||
# Configure a named profile for R2
|
||||
aws configure --profile r2
|
||||
# Access Key ID: <your-r2-access-key>
|
||||
# Secret Access Key: <your-r2-secret-key>
|
||||
# Region: auto
|
||||
# Output: json
|
||||
|
||||
# Use the R2 endpoint
|
||||
export R2_ENDPOINT="https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
|
||||
|
||||
# List buckets
|
||||
aws s3 ls --endpoint-url=$R2_ENDPOINT --profile=r2
|
||||
|
||||
# Sync a directory
|
||||
aws s3 sync ./dist s3://app-assets/static/ \
|
||||
--endpoint-url=$R2_ENDPOINT \
|
||||
--profile=r2
|
||||
|
||||
# Copy a file
|
||||
aws s3 cp ./backup.tar.gz s3://app-assets/backups/backup-$(date +%Y%m%d).tar.gz \
|
||||
--endpoint-url=$R2_ENDPOINT \
|
||||
--profile=r2
|
||||
|
||||
# List objects with prefix
|
||||
aws s3 ls s3://app-assets/images/ \
|
||||
--endpoint-url=$R2_ENDPOINT \
|
||||
--profile=r2
|
||||
|
||||
# Remove objects by prefix
|
||||
aws s3 rm s3://app-assets/tmp/ --recursive \
|
||||
--endpoint-url=$R2_ENDPOINT \
|
||||
--profile=r2
|
||||
```
|
||||
|
||||
### Python boto3 Client
|
||||
|
||||
```python
|
||||
import boto3
|
||||
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
endpoint_url="https://<ACCOUNT_ID>.r2.cloudflarestorage.com",
|
||||
aws_access_key_id="<R2_ACCESS_KEY>",
|
||||
aws_secret_access_key="<R2_SECRET_KEY>",
|
||||
region_name="auto",
|
||||
)
|
||||
|
||||
# Upload file
|
||||
s3.upload_file("./report.pdf", "app-assets", "reports/report.pdf")
|
||||
|
||||
# Generate presigned URL (valid for 1 hour)
|
||||
url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": "app-assets", "Key": "reports/report.pdf"},
|
||||
ExpiresIn=3600,
|
||||
)
|
||||
print(url)
|
||||
|
||||
# List objects
|
||||
response = s3.list_objects_v2(Bucket="app-assets", Prefix="images/", MaxKeys=100)
|
||||
for obj in response.get("Contents", []):
|
||||
print(f"{obj['Key']} - {obj['Size']} bytes")
|
||||
```
|
||||
|
||||
## Worker Bindings
|
||||
|
||||
Bind R2 buckets to Workers or Pages Functions for server-side access without API tokens.
|
||||
|
||||
### Wrangler Configuration
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
name = "asset-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-09-01"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "ASSETS"
|
||||
bucket_name = "app-assets"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "UPLOADS"
|
||||
bucket_name = "user-uploads"
|
||||
```
|
||||
|
||||
### Worker with R2 Operations
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
interface Env {
|
||||
ASSETS: R2Bucket;
|
||||
UPLOADS: R2Bucket;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// GET — serve file from R2
|
||||
if (request.method === "GET") {
|
||||
const key = url.pathname.slice(1); // strip leading /
|
||||
const object = await env.ASSETS.get(key);
|
||||
|
||||
if (!object) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set("etag", object.httpEtag);
|
||||
headers.set("cache-control", "public, max-age=86400");
|
||||
|
||||
return new Response(object.body, { headers });
|
||||
}
|
||||
|
||||
// PUT — upload file to R2
|
||||
if (request.method === "PUT") {
|
||||
const key = url.pathname.slice(1);
|
||||
const contentType = request.headers.get("content-type") || "application/octet-stream";
|
||||
|
||||
await env.UPLOADS.put(key, request.body, {
|
||||
httpMetadata: { contentType },
|
||||
customMetadata: { uploadedAt: new Date().toISOString() },
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ key, status: "uploaded" }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE — remove file
|
||||
if (request.method === "DELETE") {
|
||||
const key = url.pathname.slice(1);
|
||||
await env.UPLOADS.delete(key);
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Presigned URL Generation in a Worker
|
||||
|
||||
```typescript
|
||||
// Generate time-limited signed URLs using Workers
|
||||
import { AwsClient } from "aws4fetch";
|
||||
|
||||
interface Env {
|
||||
R2_ACCESS_KEY: string;
|
||||
R2_SECRET_KEY: string;
|
||||
R2_ACCOUNT_ID: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const aws = new AwsClient({
|
||||
accessKeyId: env.R2_ACCESS_KEY,
|
||||
secretAccessKey: env.R2_SECRET_KEY,
|
||||
});
|
||||
|
||||
const url = new URL(request.url);
|
||||
const key = url.searchParams.get("key");
|
||||
if (!key) return new Response("Missing key", { status: 400 });
|
||||
|
||||
const r2Url = `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/app-assets/${key}`;
|
||||
|
||||
const signed = await aws.sign(new Request(r2Url), {
|
||||
aws: { signQuery: true },
|
||||
});
|
||||
|
||||
return Response.json({ url: signed.url });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Public Bucket Access
|
||||
|
||||
Enable public access to serve files directly without a Worker.
|
||||
|
||||
1. Go to **R2 > bucket > Settings > Public access**.
|
||||
2. Enable and set a custom domain (e.g., `assets.example.com`).
|
||||
3. Objects are accessible at `https://assets.example.com/<key>`.
|
||||
|
||||
```bash
|
||||
# Or enable via the r2.dev subdomain (for testing)
|
||||
# Bucket Settings > R2.dev subdomain > Allow Access
|
||||
# URL: https://pub-<hash>.r2.dev/<key>
|
||||
```
|
||||
|
||||
## Lifecycle Rules
|
||||
|
||||
Configure automatic object expiration or transition.
|
||||
|
||||
```bash
|
||||
# Set lifecycle rules via the Cloudflare dashboard:
|
||||
# R2 > bucket > Settings > Object lifecycle rules
|
||||
|
||||
# Or via API
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/app-assets/lifecycle" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"rules": [
|
||||
{
|
||||
"id": "expire-tmp-files",
|
||||
"enabled": true,
|
||||
"conditions": { "prefix": "tmp/" },
|
||||
"actions": { "deleteObject": { "daysAfterCreationDate": 7 } }
|
||||
},
|
||||
{
|
||||
"id": "expire-old-logs",
|
||||
"enabled": true,
|
||||
"conditions": { "prefix": "logs/" },
|
||||
"actions": { "deleteObject": { "daysAfterCreationDate": 90 } }
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## CORS Configuration
|
||||
|
||||
```bash
|
||||
# Set CORS policy for browser-based uploads
|
||||
curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/app-assets/cors" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"corsRules": [
|
||||
{
|
||||
"allowedOrigins": ["https://example.com"],
|
||||
"allowedMethods": ["GET", "PUT", "HEAD"],
|
||||
"allowedHeaders": ["Content-Type", "Authorization"],
|
||||
"maxAgeSeconds": 3600
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `NoSuchBucket` error via S3 API | Wrong endpoint or bucket name | Verify endpoint is `https://<ACCOUNT_ID>.r2.cloudflarestorage.com` |
|
||||
| `SignatureDoesNotMatch` | Incorrect secret key or endpoint mismatch | Regenerate R2 API token; ensure region is `auto` |
|
||||
| Uploads succeed but GET returns 404 | Key path mismatch (leading slash) | R2 keys should not start with `/` |
|
||||
| Slow uploads for large files | Single-stream upload | Use multipart upload; set `--expected-size` with wrangler |
|
||||
| CORS errors in browser | Missing CORS config on bucket | Add CORS rules for your origin domain |
|
||||
| Worker binding returns `undefined` | `wrangler.toml` binding name mismatch | Verify `binding` name matches `Env` interface property |
|
||||
| Public access returns 403 | Public access not enabled | Enable in bucket Settings > Public access |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [cloudflare-workers](../cloudflare-workers/) - Signed URL generation
|
||||
- [object-storage](../../storage/object-storage/) - Storage patterns
|
||||
- [cloudflare-workers](../cloudflare-workers/) - Signed URL generation and edge file serving
|
||||
- [cloudflare-pages](../cloudflare-pages/) - Pages Functions with R2 bindings
|
||||
- [cdn-setup](../../networking/cdn-setup/) - CDN configuration for asset delivery
|
||||
|
||||
@@ -9,38 +9,391 @@ metadata:
|
||||
|
||||
# Cloudflare Workers
|
||||
|
||||
Deploy JavaScript/TypeScript functions globally at the edge.
|
||||
Deploy JavaScript and TypeScript functions to Cloudflare's global edge network with sub-millisecond cold starts.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Building lightweight APIs and microservices at the edge.
|
||||
- Adding middleware (auth, rate limiting, header injection) in front of origin servers.
|
||||
- Running cron jobs on a schedule without maintaining infrastructure.
|
||||
- Processing webhooks, image transformations, or A/B testing logic.
|
||||
- Serving dynamic content from KV, D1, or R2 storage bindings.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+ installed locally.
|
||||
- Wrangler CLI: `npm install -g wrangler`.
|
||||
- Cloudflare account (free plan supports 100,000 requests/day).
|
||||
- Authenticated: `wrangler login` or set `CLOUDFLARE_API_TOKEN`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Scaffold a new Worker project
|
||||
npm create cloudflare@latest my-worker
|
||||
cd my-worker
|
||||
|
||||
# Login to Cloudflare
|
||||
npx wrangler login
|
||||
|
||||
# Start local development server (port 8787)
|
||||
npx wrangler dev
|
||||
|
||||
# Deploy to production
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
## Essential Wrangler Commands
|
||||
|
||||
```bash
|
||||
# Local dev
|
||||
npx wrangler dev
|
||||
# Local development with remote bindings (KV, D1, R2)
|
||||
npx wrangler dev --remote
|
||||
|
||||
# Set secret
|
||||
# Deploy to a specific environment
|
||||
npx wrangler deploy --env staging
|
||||
|
||||
# Set a secret (prompts for value)
|
||||
npx wrangler secret put API_TOKEN
|
||||
npx wrangler secret put API_TOKEN --env staging
|
||||
|
||||
# Tail logs
|
||||
# List secrets
|
||||
npx wrangler secret list
|
||||
|
||||
# Tail production logs in real time
|
||||
npx wrangler tail
|
||||
|
||||
# Tail with filters
|
||||
npx wrangler tail --status=error --search="timeout"
|
||||
|
||||
# View deployment versions
|
||||
npx wrangler deployments list
|
||||
|
||||
# Rollback to a previous deployment
|
||||
npx wrangler rollback
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## Wrangler Configuration
|
||||
|
||||
- Keep workers stateless and fast.
|
||||
- Use KV, D1, or R2 for persistence.
|
||||
- Add rate limits for public APIs.
|
||||
- Version Wrangler config in git.
|
||||
```toml
|
||||
# wrangler.toml
|
||||
name = "my-api"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-09-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
# Custom routes
|
||||
routes = [
|
||||
{ pattern = "api.example.com/*", zone_name = "example.com" }
|
||||
]
|
||||
|
||||
# Or use a workers.dev subdomain
|
||||
# workers_dev = true
|
||||
|
||||
# Environment variables (non-secret)
|
||||
[vars]
|
||||
ENVIRONMENT = "production"
|
||||
API_VERSION = "v2"
|
||||
|
||||
# Staging environment override
|
||||
[env.staging]
|
||||
name = "my-api-staging"
|
||||
routes = [
|
||||
{ pattern = "api-staging.example.com/*", zone_name = "example.com" }
|
||||
]
|
||||
[env.staging.vars]
|
||||
ENVIRONMENT = "staging"
|
||||
```
|
||||
|
||||
## Worker Examples
|
||||
|
||||
### Basic API Router
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
export interface Env {
|
||||
ENVIRONMENT: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
switch (url.pathname) {
|
||||
case "/":
|
||||
return new Response("OK", { status: 200 });
|
||||
|
||||
case "/api/health":
|
||||
return Response.json({
|
||||
status: "healthy",
|
||||
env: env.ENVIRONMENT,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
case "/api/data":
|
||||
if (request.method !== "POST") {
|
||||
return new Response("Method Not Allowed", { status: 405 });
|
||||
}
|
||||
const body = await request.json();
|
||||
// Process in the background after returning response
|
||||
ctx.waitUntil(logToAnalytics(body));
|
||||
return Response.json({ received: true });
|
||||
|
||||
default:
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function logToAnalytics(data: unknown): Promise<void> {
|
||||
await fetch("https://analytics.example.com/ingest", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware: Rate Limiting with KV
|
||||
|
||||
```typescript
|
||||
// src/rate-limiter.ts
|
||||
interface Env {
|
||||
RATE_LIMIT_KV: KVNamespace;
|
||||
ORIGIN_URL: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const ip = request.headers.get("CF-Connecting-IP") || "unknown";
|
||||
const key = `ratelimit:${ip}`;
|
||||
const window = 60; // seconds
|
||||
const maxRequests = 100;
|
||||
|
||||
const current = parseInt((await env.RATE_LIMIT_KV.get(key)) || "0");
|
||||
|
||||
if (current >= maxRequests) {
|
||||
return new Response("Too Many Requests", {
|
||||
status: 429,
|
||||
headers: { "Retry-After": String(window) },
|
||||
});
|
||||
}
|
||||
|
||||
await env.RATE_LIMIT_KV.put(key, String(current + 1), {
|
||||
expirationTtl: window,
|
||||
});
|
||||
|
||||
// Forward to origin
|
||||
const originRequest = new Request(env.ORIGIN_URL + new URL(request.url).pathname, request);
|
||||
return fetch(originRequest);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## KV Storage Binding
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[[kv_namespaces]]
|
||||
binding = "MY_KV"
|
||||
id = "abc123def456"
|
||||
|
||||
# Preview namespace for local dev
|
||||
[[kv_namespaces]]
|
||||
binding = "MY_KV"
|
||||
id = "abc123def456"
|
||||
preview_id = "preview789"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// KV operations in a Worker
|
||||
interface Env {
|
||||
MY_KV: KVNamespace;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Write with TTL
|
||||
await env.MY_KV.put("session:abc", JSON.stringify({ user: "alice" }), {
|
||||
expirationTtl: 3600,
|
||||
});
|
||||
|
||||
// Read
|
||||
const session = await env.MY_KV.get("session:abc", "json");
|
||||
|
||||
// List keys by prefix
|
||||
const list = await env.MY_KV.list({ prefix: "session:", limit: 100 });
|
||||
|
||||
// Delete
|
||||
await env.MY_KV.delete("session:abc");
|
||||
|
||||
return Response.json({ session, keys: list.keys.length });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```bash
|
||||
# KV CLI operations
|
||||
npx wrangler kv namespace create MY_KV
|
||||
npx wrangler kv namespace list
|
||||
npx wrangler kv key put --namespace-id=abc123 "config:feature-flags" '{"darkMode":true}'
|
||||
npx wrangler kv key get --namespace-id=abc123 "config:feature-flags"
|
||||
npx wrangler kv key list --namespace-id=abc123 --prefix="config:"
|
||||
```
|
||||
|
||||
## D1 Database Binding
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "my-app"
|
||||
database_id = "xxxx-yyyy-zzzz"
|
||||
```
|
||||
|
||||
```typescript
|
||||
// D1 SQL queries in a Worker
|
||||
interface Env {
|
||||
DB: D1Database;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Parameterized query
|
||||
const { results } = await env.DB.prepare(
|
||||
"SELECT id, name, email FROM users WHERE active = ? LIMIT ?"
|
||||
)
|
||||
.bind(1, 50)
|
||||
.all();
|
||||
|
||||
// Insert
|
||||
await env.DB.prepare("INSERT INTO users (name, email) VALUES (?, ?)")
|
||||
.bind("Alice", "alice@example.com")
|
||||
.run();
|
||||
|
||||
// Batch multiple statements
|
||||
await env.DB.batch([
|
||||
env.DB.prepare("UPDATE users SET active = 0 WHERE last_login < ?").bind("2024-01-01"),
|
||||
env.DB.prepare("DELETE FROM sessions WHERE expires_at < ?").bind(Date.now()),
|
||||
]);
|
||||
|
||||
return Response.json(results);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```bash
|
||||
# D1 CLI operations
|
||||
npx wrangler d1 create my-app
|
||||
npx wrangler d1 list
|
||||
npx wrangler d1 execute my-app --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, active INTEGER DEFAULT 1)"
|
||||
npx wrangler d1 execute my-app --file=./migrations/001_init.sql
|
||||
npx wrangler d1 execute my-app --command="SELECT * FROM users" --json
|
||||
```
|
||||
|
||||
## Cron Triggers
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[triggers]
|
||||
crons = [
|
||||
"0 */6 * * *", # Every 6 hours
|
||||
"0 0 * * MON", # Every Monday at midnight
|
||||
"*/15 * * * *", # Every 15 minutes
|
||||
]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/index.ts — scheduled handler
|
||||
export default {
|
||||
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||
switch (event.cron) {
|
||||
case "0 */6 * * *":
|
||||
ctx.waitUntil(cleanupExpiredSessions(env));
|
||||
break;
|
||||
case "0 0 * * MON":
|
||||
ctx.waitUntil(generateWeeklyReport(env));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
return new Response("OK");
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Durable Objects
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[durable_objects]
|
||||
bindings = [
|
||||
{ name = "COUNTER", class_name = "Counter" }
|
||||
]
|
||||
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_classes = ["Counter"]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// src/counter.ts — Durable Object class
|
||||
export class Counter {
|
||||
state: DurableObjectState;
|
||||
|
||||
constructor(state: DurableObjectState) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
let count = (await this.state.storage.get<number>("count")) || 0;
|
||||
count++;
|
||||
await this.state.storage.put("count", count);
|
||||
return Response.json({ count });
|
||||
}
|
||||
}
|
||||
|
||||
// src/index.ts — route to Durable Object
|
||||
interface Env {
|
||||
COUNTER: DurableObjectNamespace;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const id = env.COUNTER.idFromName("global-counter");
|
||||
const stub = env.COUNTER.get(id);
|
||||
return stub.fetch(request);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Custom Routing
|
||||
|
||||
```toml
|
||||
# Route to specific zones
|
||||
routes = [
|
||||
{ pattern = "api.example.com/v1/*", zone_name = "example.com" },
|
||||
{ pattern = "api.example.com/v2/*", zone_name = "example.com" },
|
||||
]
|
||||
|
||||
# Or use custom domains (automatic SSL)
|
||||
# Dashboard: Workers > your-worker > Triggers > Custom Domains
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `Error 1101: Worker threw exception` | Unhandled error in fetch handler | Wrap handler in try/catch; check `wrangler tail` for stack trace |
|
||||
| `exceeded CPU time limit` | Worker exceeds 10ms CPU (free) or 30s (paid) | Optimize code; offload work with `ctx.waitUntil()` |
|
||||
| KV reads return stale data | KV is eventually consistent (~60s) | Use `cacheTtl` option or switch to Durable Objects for strong consistency |
|
||||
| `wrangler dev` binding errors | Local bindings not configured | Use `--remote` flag or configure `preview_id` in `wrangler.toml` |
|
||||
| Secret not found in Worker | Secret set for wrong environment | Verify with `wrangler secret list --env <env>` |
|
||||
| CORS errors from browser | Missing CORS headers in response | Add `Access-Control-Allow-Origin` headers; handle OPTIONS preflight |
|
||||
| Route not matching | Pattern does not include `/*` suffix | Add `/*` to catch all paths: `api.example.com/*` |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [cloudflare-pages](../cloudflare-pages/) - Frontend deployments
|
||||
- [cloudflare-pages](../cloudflare-pages/) - Frontend deployments with Pages Functions
|
||||
- [cloudflare-r2](../cloudflare-r2/) - Object storage at the edge
|
||||
- [cloudflare-zero-trust](../cloudflare-zero-trust/) - Protect Worker endpoints with Access
|
||||
|
||||
@@ -9,31 +9,337 @@ metadata:
|
||||
|
||||
# Cloudflare Zero Trust
|
||||
|
||||
Secure access to internal services without exposing public VPN endpoints.
|
||||
Secure access to internal services without VPNs using Cloudflare's Zero Trust platform (Access, Tunnel, Gateway, and WARP).
|
||||
|
||||
## Core Workflow
|
||||
## When to Use
|
||||
|
||||
1. Register application in Cloudflare Access.
|
||||
2. Integrate identity provider (Google Workspace, Okta, Entra ID).
|
||||
3. Define access policies by group, email domain, and device posture.
|
||||
4. Add logging and alerts for blocked requests.
|
||||
- Replacing VPN access to internal web applications, SSH, or RDP.
|
||||
- Enforcing identity-aware access policies on internal tools (dashboards, admin panels).
|
||||
- Exposing on-premises or private-network services securely to remote teams.
|
||||
- Filtering DNS traffic to block malware, phishing, and shadow IT.
|
||||
- Enforcing device posture checks (managed devices, OS version, disk encryption).
|
||||
|
||||
## Tunnel Setup
|
||||
## Prerequisites
|
||||
|
||||
- Cloudflare account with Zero Trust plan (free tier supports up to 50 users).
|
||||
- A domain on Cloudflare (for Access application hostnames).
|
||||
- Identity provider configured (Google Workspace, Okta, Azure AD/Entra ID, GitHub).
|
||||
- `cloudflared` CLI installed on the server hosting internal services.
|
||||
|
||||
```bash
|
||||
cloudflared tunnel login
|
||||
cloudflared tunnel create internal-app
|
||||
cloudflared tunnel route dns internal-app app.example.com
|
||||
cloudflared tunnel run internal-app
|
||||
# Install cloudflared
|
||||
# macOS
|
||||
brew install cloudflared
|
||||
|
||||
# Debian/Ubuntu
|
||||
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
|
||||
sudo apt update && sudo apt install -y cloudflared
|
||||
|
||||
# Docker
|
||||
docker pull cloudflare/cloudflared:latest
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
## Cloudflare Tunnel Setup
|
||||
|
||||
- Enforce MFA and managed-device posture checks.
|
||||
- Use service tokens for CI/CD automation.
|
||||
- Review app policies quarterly.
|
||||
Tunnels create encrypted outbound connections from your infrastructure to Cloudflare's edge, eliminating the need to open inbound ports.
|
||||
|
||||
### Create and Configure a Tunnel
|
||||
|
||||
```bash
|
||||
# Authenticate with Cloudflare
|
||||
cloudflared tunnel login
|
||||
|
||||
# Create a named tunnel
|
||||
cloudflared tunnel create internal-apps
|
||||
|
||||
# This creates credentials at ~/.cloudflared/<TUNNEL_ID>.json
|
||||
|
||||
# List tunnels
|
||||
cloudflared tunnel list
|
||||
|
||||
# Route DNS to the tunnel (creates a CNAME record)
|
||||
cloudflared tunnel route dns internal-apps grafana.example.com
|
||||
cloudflared tunnel route dns internal-apps wiki.example.com
|
||||
cloudflared tunnel route dns internal-apps ssh.example.com
|
||||
```
|
||||
|
||||
### Tunnel Configuration File
|
||||
|
||||
```yaml
|
||||
# ~/.cloudflared/config.yml
|
||||
tunnel: <TUNNEL_ID>
|
||||
credentials-file: /home/deploy/.cloudflared/<TUNNEL_ID>.json
|
||||
|
||||
ingress:
|
||||
# Grafana dashboard
|
||||
- hostname: grafana.example.com
|
||||
service: http://localhost:3000
|
||||
|
||||
# Internal wiki
|
||||
- hostname: wiki.example.com
|
||||
service: http://localhost:8080
|
||||
originRequest:
|
||||
noTLSVerify: true
|
||||
|
||||
# SSH access via browser
|
||||
- hostname: ssh.example.com
|
||||
service: ssh://localhost:22
|
||||
|
||||
# Private network access (CIDR routing)
|
||||
- hostname: internal.example.com
|
||||
service: http://10.0.0.0/24
|
||||
|
||||
# Catch-all — required as the last rule
|
||||
- service: http_status:404
|
||||
```
|
||||
|
||||
### Run the Tunnel
|
||||
|
||||
```bash
|
||||
# Run in foreground (for testing)
|
||||
cloudflared tunnel run internal-apps
|
||||
|
||||
# Install as a systemd service
|
||||
sudo cloudflared service install
|
||||
sudo systemctl enable cloudflared
|
||||
sudo systemctl start cloudflared
|
||||
|
||||
# Or run via Docker
|
||||
docker run -d --name cloudflared \
|
||||
--restart unless-stopped \
|
||||
-v /home/deploy/.cloudflared:/etc/cloudflared \
|
||||
cloudflare/cloudflared:latest \
|
||||
tunnel run internal-apps
|
||||
```
|
||||
|
||||
### Docker Compose with Tunnel
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: "3.8"
|
||||
services:
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared:latest
|
||||
restart: unless-stopped
|
||||
command: tunnel run
|
||||
environment:
|
||||
- TUNNEL_TOKEN=${TUNNEL_TOKEN}
|
||||
networks:
|
||||
- internal
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
networks:
|
||||
- internal
|
||||
|
||||
wiki:
|
||||
image: requarks/wiki:2
|
||||
networks:
|
||||
- internal
|
||||
|
||||
networks:
|
||||
internal:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
## Access Policies
|
||||
|
||||
Access policies control who can reach applications behind Cloudflare.
|
||||
|
||||
### Create an Access Application
|
||||
|
||||
```bash
|
||||
# Via API — create a self-hosted application
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/access/apps" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Grafana",
|
||||
"domain": "grafana.example.com",
|
||||
"type": "self_hosted",
|
||||
"session_duration": "12h",
|
||||
"auto_redirect_to_identity": true,
|
||||
"allowed_idps": ["<IDP_UUID>"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Policy Types and Examples
|
||||
|
||||
```bash
|
||||
# Allow policy — members of the engineering group
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/access/apps/<APP_ID>/policies" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Engineering Team",
|
||||
"decision": "allow",
|
||||
"include": [
|
||||
{ "group": { "id": "<GROUP_UUID>" } }
|
||||
],
|
||||
"require": [
|
||||
{ "login_method": { "id": "<MFA_METHOD_UUID>" } }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Common Policy Patterns
|
||||
|
||||
| Pattern | Include Rule | Require Rule |
|
||||
|---------|-------------|--------------|
|
||||
| All employees | Email domain `@company.com` | - |
|
||||
| Engineering only | Access Group "Engineering" | MFA |
|
||||
| Contractors (time-limited) | Email list | Device posture |
|
||||
| CI/CD automation | Service token | - |
|
||||
| External partners | Specific emails | Country check |
|
||||
|
||||
### Service Tokens for Automation
|
||||
|
||||
```bash
|
||||
# Create a service token for CI/CD
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/access/service_tokens" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "github-actions-deploy"}'
|
||||
|
||||
# Response includes Client ID and Client Secret
|
||||
# Use in CI with headers:
|
||||
# CF-Access-Client-Id: <CLIENT_ID>
|
||||
# CF-Access-Client-Secret: <CLIENT_SECRET>
|
||||
```
|
||||
|
||||
```bash
|
||||
# Use service token in CI/CD
|
||||
curl -H "CF-Access-Client-Id: $CF_CLIENT_ID" \
|
||||
-H "CF-Access-Client-Secret: $CF_CLIENT_SECRET" \
|
||||
https://grafana.example.com/api/health
|
||||
```
|
||||
|
||||
## Device Posture Checks
|
||||
|
||||
Enforce endpoint requirements before granting access.
|
||||
|
||||
### Configure Posture Checks (Dashboard)
|
||||
|
||||
1. Go to **Settings > WARP Client > Device posture**.
|
||||
2. Add checks:
|
||||
- **Disk encryption**: Require FileVault (macOS) or BitLocker (Windows).
|
||||
- **OS version**: Minimum macOS 14.0 or Windows 11.
|
||||
- **Firewall**: Ensure host firewall is enabled.
|
||||
- **Crowdstrike/SentinelOne**: Verify EDR agent is running.
|
||||
3. Reference posture checks in Access policies under **Require** rules.
|
||||
|
||||
## Gateway DNS Filtering
|
||||
|
||||
Block malicious domains and enforce acceptable use policies at the DNS level.
|
||||
|
||||
### DNS Locations
|
||||
|
||||
```bash
|
||||
# Configure DNS endpoints for offices or networks
|
||||
# Dashboard: Gateway > DNS Locations > Add a location
|
||||
# Assign the Gateway DNS IPs to your network's DNS resolver:
|
||||
# IPv4: 172.64.36.1, 172.64.36.2
|
||||
# IPv6: 2606:4700:4700::1111
|
||||
# DoH: https://<UNIQUE_ID>.cloudflare-gateway.com/dns-query
|
||||
```
|
||||
|
||||
### DNS Policies
|
||||
|
||||
```bash
|
||||
# Create a DNS policy to block malware and phishing
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/gateway/rules" \
|
||||
-H "Authorization: Bearer $CF_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Block Security Threats",
|
||||
"enabled": true,
|
||||
"action": "block",
|
||||
"traffic": "any(dns.security_category[*] in {80 83 131 134 151 153})",
|
||||
"filters": ["dns"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Common DNS Policy Rules
|
||||
|
||||
| Rule Name | Traffic Expression | Action |
|
||||
|-----------|-------------------|--------|
|
||||
| Block malware | `any(dns.security_category[*] in {80 83})` | Block |
|
||||
| Block phishing | `any(dns.security_category[*] in {131 134})` | Block |
|
||||
| Block social media | `any(dns.content_category[*] in {75})` | Block |
|
||||
| Allow exceptions | `dns.fqdn == "allowed.example.com"` | Allow |
|
||||
|
||||
## WARP Client Deployment
|
||||
|
||||
Deploy the Cloudflare WARP client to route traffic through Gateway.
|
||||
|
||||
```bash
|
||||
# MDM deployment — macOS configuration profile
|
||||
# Use Cloudflare's managed deployment:
|
||||
# Dashboard: Settings > WARP Client > Device enrollment
|
||||
|
||||
# Manual enrollment
|
||||
# 1. Install WARP client from https://1.1.1.1
|
||||
# 2. Click gear icon > Account > Login with Cloudflare Zero Trust
|
||||
# 3. Enter your team name (from Settings > General)
|
||||
|
||||
# Verify WARP is connected
|
||||
curl https://connectivity.cloudflare.com/cdn-cgi/trace
|
||||
# Look for: warp=on
|
||||
```
|
||||
|
||||
### WARP Split Tunnels
|
||||
|
||||
```bash
|
||||
# Configure split tunnels to exclude certain traffic from WARP
|
||||
# Dashboard: Settings > WARP Client > Device settings > Split Tunnels
|
||||
|
||||
# Exclude mode (default): WARP handles everything except listed IPs
|
||||
# Include mode: WARP only handles listed IPs/domains
|
||||
|
||||
# Common exclusions:
|
||||
# - Local network: 192.168.0.0/16, 10.0.0.0/8
|
||||
# - Video conferencing: zoom.us, *.teams.microsoft.com
|
||||
# - Printer subnets
|
||||
```
|
||||
|
||||
## SSH and Browser-Based Terminal
|
||||
|
||||
```yaml
|
||||
# In cloudflared config.yml — expose SSH via browser rendering
|
||||
ingress:
|
||||
- hostname: ssh.example.com
|
||||
service: ssh://localhost:22
|
||||
```
|
||||
|
||||
```bash
|
||||
# Users access ssh.example.com in their browser
|
||||
# Cloudflare renders an in-browser terminal after Access authentication
|
||||
|
||||
# Or use cloudflared on the client side for native SSH
|
||||
cloudflared access ssh --hostname ssh.example.com
|
||||
|
||||
# Add to SSH config for seamless access
|
||||
# ~/.ssh/config
|
||||
# Host ssh.example.com
|
||||
# ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Tunnel shows `ERR` in dashboard | `cloudflared` not running or config error | Check `systemctl status cloudflared`; validate config YAML |
|
||||
| Access returns 403 despite correct identity | Policy order or missing require rule | Policies are evaluated top-to-bottom; ensure Allow is above Block |
|
||||
| WARP shows "Unable to connect" | Team name wrong or enrollment disabled | Verify team name in Settings > General; check enrollment permissions |
|
||||
| Service token auth fails | Token expired or wrong headers | Regenerate token; use both `CF-Access-Client-Id` and `CF-Access-Client-Secret` |
|
||||
| DNS filtering not blocking | Client not using Gateway DNS resolvers | Verify DNS is set to 172.64.36.1; check WARP is connected |
|
||||
| Tunnel latency spikes | Tunnel running on overloaded host | Monitor `cloudflared` resource usage; run on dedicated infra |
|
||||
| "No healthy origins" error | Backend service is down | Check the service at the configured ingress port; review `cloudflared` logs |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [zero-trust](../../../security/network/zero-trust/) - Zero trust architecture fundamentals
|
||||
- [dns-management](../../networking/dns-management/) - DNS routing concepts
|
||||
- [cloudflare-workers](../cloudflare-workers/) - Edge compute behind Access policies
|
||||
- [dns-management](../../networking/dns-management/) - DNS routing and record management
|
||||
- [reverse-proxy](../../networking/reverse-proxy/) - Alternative gateway patterns
|
||||
- [service-mesh](../../networking/service-mesh/) - Internal service-to-service security
|
||||
|
||||
Reference in New Issue
Block a user