plugins/cloudflare/skills/zero-trust/SKILL.md
Use this skill when the user asks about Cloudflare Zero Trust, Access policies, identity-based access control, protecting self-hosted apps, WARP client, Gateway DNS filtering, device posture, or managing Zero Trust resources with Pulumi. For tunnel setup (the transport layer), also recall the cloudflare-tunnels skill. For deploying Access-protected apps via docker-compose, recall the arcane plugin's arcane-gitops skill.
npx skillsauth add nsheaps/ai-mktpl cloudflare-zero-trustInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Cloudflare Zero Trust (formerly Cloudflare for Teams) replaces traditional VPNs with identity-aware access policies. Every request is authenticated and authorized at Cloudflare's edge before reaching your application.
User → Cloudflare Edge → Access Policy Check → Tunnel → Your App
↓
Identity Provider (IdP)
(Google, GitHub, Okta, SAML, etc.)
The typical Zero Trust stack:
cloudflare-tunnels skill)| Concept | Description |
| --------------------- | ------------------------------------------------------------------- |
| Application | A hostname or path to protect (e.g., app.example.com) |
| Policy | Rules that determine who can access an application |
| Identity Provider | Where users authenticate (Google, GitHub, Okta, one-time PIN, etc.) |
| Service Token | Machine-to-machine auth (no human login) |
| mTLS | Mutual TLS certificate-based auth |
| Session Duration | How long an authenticated session lasts before re-auth |
| Provider | Type | Setup Complexity | | ------------------------ | --------- | ------------------------- | | One-time PIN (email) | Built-in | None — works immediately | | Google | OAuth | Add client ID/secret | | GitHub | OAuth | Add client ID/secret | | Okta | SAML/OIDC | Configure in Okta admin | | Azure AD | SAML/OIDC | Configure in Azure portal | | SAML (generic) | SAML | Any SAML 2.0 IdP | | OpenID Connect (generic) | OIDC | Any OIDC provider |
Policies use Include (match any), Exclude (deny if matched), and Require (must match all):
| Rule Type | Examples |
| --------------------- | ------------------------------------------ |
| Email | [email protected] |
| Email domain | @example.com |
| IP range | 192.168.1.0/24 |
| Country | US, GB, etc. |
| Authentication method | Specific IdP |
| Service token | Machine-to-machine |
| Device posture | WARP enrolled, OS version, disk encryption |
| External evaluation | Custom API check |
Typical flow for protecting a self-hosted app behind a tunnel:
1. Deploy app via docker-compose (on same network as cloudflared)
2. Create tunnel ingress rule: app.example.com → http://my-app:8080
3. Create Access Application: app.example.com
4. Create Access Policy: Allow @example.com
5. Add DNS CNAME: app.example.com → <tunnel-id>.cfargotunnel.com
If using Arcane for GitOps, the docker-compose stack goes in hosts/<hostname>/<app>/docker-compose.yaml and the Access configuration is managed via Pulumi (see below).
Gateway provides network-level security for devices running the WARP client:
Block malicious domains, content categories, or specific domains:
Block security threats: dns.security_category in {68 178 80}
Block social media: dns.content_category in {171}
Block specific domain: dns.fqdn == "example.com"
Allow exception: dns.fqdn == "allowed.example.com"
Inspect and filter HTTP traffic (requires WARP + TLS inspection):
Block file uploads: http.request.method == "POST" and http.upload
Block malware downloads: http.response.content_type matches "application/.*executable"
Control traffic at the network layer:
Block SSH to external: net.dst.port == 22 and not net.dst.ip in {10.0.0.0/8}
WARP is the client-side agent that routes device traffic through Cloudflare:
Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/download-warp/
# macOS (MDM)
# Deploy via Jamf, Kandji, etc. with a JSON config:
{
"organization": "your-team-name",
"auth_client_id": "<service-token-id>.access",
"auth_client_secret": "<service-token-secret>"
}
import * as cloudflare from "@pulumi/cloudflare";
const cfConfig = new pulumi.Config("cloudflare");
const accountId = cfConfig.require("accountId");
// Look up the zone
const zone = cloudflare.getZoneOutput({ name: "example.com", accountId });
// Protect an application
const app = new cloudflare.ZeroTrustAccessApplication("internal-app", {
zoneId: zone.id,
name: "Internal Dashboard",
domain: "dashboard.example.com",
type: "self_hosted",
sessionDuration: "24h",
autoRedirectToIdentity: true,
// Optional: custom logo, cors settings, etc.
appLauncherVisible: true,
logoUrl: "https://example.com/logo.png",
});
// Allow team members by email domain
const teamPolicy = new cloudflare.ZeroTrustAccessPolicy("allow-team", {
applicationId: app.id,
zoneId: zone.id,
name: "Allow Team",
precedence: 1,
decision: "allow",
includes: [
{
emailDomains: ["example.com"],
},
],
});
// Allow specific external users
const externalPolicy = new cloudflare.ZeroTrustAccessPolicy("allow-external", {
applicationId: app.id,
zoneId: zone.id,
name: "Allow External Partners",
precedence: 2,
decision: "allow",
includes: [
{
emails: ["[email protected]"],
},
],
});
// Service token for machine-to-machine access (CI/CD, APIs)
const serviceToken = new cloudflare.ZeroTrustAccessServiceToken("api-token", {
accountId,
name: "CI/CD Pipeline",
duration: "8760h", // 1 year
});
const apiPolicy = new cloudflare.ZeroTrustAccessPolicy("allow-service-token", {
applicationId: app.id,
zoneId: zone.id,
name: "Allow Service Token",
precedence: 3,
decision: "non_identity",
includes: [
{
serviceTokens: [serviceToken.id],
},
],
});
export const serviceTokenClientId = serviceToken.clientId;
export const serviceTokenClientSecret = serviceToken.clientSecret;
// Block malware and phishing domains
const blockMalware = new cloudflare.ZeroTrustGatewayPolicy("block-malware", {
accountId,
name: "Block Security Threats",
action: "block",
enabled: true,
filters: ["dns"],
traffic: "any(dns.security_category[*] in {68 178 80 83})",
description: "Block malware, phishing, cryptomining, and command-and-control domains",
});
// Block adult content
const blockAdult = new cloudflare.ZeroTrustGatewayPolicy("block-adult", {
accountId,
name: "Block Adult Content",
action: "block",
enabled: true,
filters: ["dns"],
traffic: "any(dns.content_category[*] in {4})",
precedence: 2,
});
// Allow-list specific domains
const allowDocs = new cloudflare.ZeroTrustGatewayPolicy("allow-docs", {
accountId,
name: "Allow Documentation Sites",
action: "allow",
enabled: true,
filters: ["dns"],
traffic: 'dns.fqdn in {"docs.example.com" "wiki.example.com"}',
precedence: 1, // Higher precedence = evaluated first
});
// 1. Create tunnel (see cloudflare-tunnels skill)
const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("app-tunnel", {
accountId,
name: "app-tunnel",
secret: tunnelSecret,
});
// 2. Configure tunnel ingress
const tunnelConfig = new cloudflare.ZeroTrustTunnelCloudflaredConfig("app-tunnel-config", {
accountId,
tunnelId: tunnel.id,
config: {
ingressRules: [
{ hostname: "dashboard.example.com", service: "http://dashboard:3000" },
{ service: "http_status:404" },
],
},
});
// 3. DNS record
const dns = new cloudflare.Record("dashboard-dns", {
zoneId: zone.id,
name: "dashboard",
type: "CNAME",
content: pulumi.interpolate`${tunnel.id}.cfargotunnel.com`,
proxied: true,
});
// 4. Access protection
const accessApp = new cloudflare.ZeroTrustAccessApplication("dashboard", {
zoneId: zone.id,
name: "Dashboard",
domain: "dashboard.example.com",
type: "self_hosted",
sessionDuration: "24h",
});
const accessPolicy = new cloudflare.ZeroTrustAccessPolicy("dashboard-policy", {
applicationId: accessApp.id,
zoneId: zone.id,
name: "Allow Team",
precedence: 1,
decision: "allow",
includes: [{ emailDomains: ["example.com"] }],
});
Many self-hosted apps have admin panels that should be locked down:
| App | Path/Hostname | Policy |
| -------------- | ----------------------- | --------------------------------- |
| Nextcloud | nextcloud.example.com | Allow @example.com |
| n8n | n8n.example.com | Allow specific emails |
| Grafana | grafana.example.com | Allow @example.com |
| Portainer | portainer.example.com | Allow admin email only |
| Home Assistant | ha.example.com | Allow @example.com + require WARP |
If your app exposes a public API alongside a protected dashboard:
// Protect the whole app
const app = new cloudflare.ZeroTrustAccessApplication("my-app", {
domain: "app.example.com",
// ...
});
// But allow unauthenticated access to /api/*
const bypassPolicy = new cloudflare.ZeroTrustAccessPolicy("bypass-api", {
applicationId: app.id,
zoneId: zone.id,
name: "Bypass API",
precedence: 0,
decision: "bypass",
includes: [{ anyValidServiceToken: {} }],
});
| Feature | Free (50 users) | Pay-as-you-go | | ----------------- | --------------- | -------------- | | Access | Included | $7/user/month | | Gateway | Included | $7/user/month | | WARP (Zero Trust) | Included | $7/user/month | | Browser Isolation | — | $10/user/month |
The free plan (50 users) is generous for personal and small-team use.
tools
Manually reproduce what the github-app plugin's SessionStart hook does to make a GitHub App installation token usable in the current session — materialize the PEM, generate the token, isolate GH_CONFIG_DIR, write the runtime env file, and wire CLAUDE_ENV_FILE so every Bash call sees GH_TOKEN/GITHUB_TOKEN. Use when the hook did not run, the token is missing from the environment, or a shell/teammate needs the token wired up by hand. <example>GH_TOKEN isn't set even though github-app is configured</example> <example>the github-app SessionStart hook didn't run, set up the token manually</example> <example>wire the github app token into CLAUDE_ENV_FILE</example> <example>gh keeps falling back to the wrong account, isolate GH_CONFIG_DIR</example>
tools
Manually configure the GitHub App bot git identity the way the github-app plugin's SessionStart hook does — resolve the app slug and bot user ID, build the <slug>[bot] name and noreply email, set GIT_AUTHOR_*/GIT_COMMITTER_* env vars, and write an isolated GIT_CONFIG_GLOBAL with the gh auth git-credential helper. Use when commits are attributed to the wrong account, "Author identity unknown" appears, or git identity must be set up by hand. <example>my commits are showing up as the handler, not the bot</example> <example>git says Author identity unknown after the github-app hook ran</example> <example>configure the github app bot git identity manually</example> <example>set up the gh credential helper for git push</example>
tools
Manages spec files for requirements capture and validation
tools
# Bash Chaining Alternatives This skill teaches you how to work around the bash command chaining restriction enforced by this plugin. ## Why Chaining is Blocked The `bash-command-rejection` plugin blocks these operators: | Operator | Name | Why Blocked | | -------- | ---------- | ----------------------------------------------------------------------------------- | | `&&` | AND chain | Runs cmd2 only if cmd1 su