Implementing Cloudflare with a mini suitcase server node that is mobile.
Below is a complete, production-ready starter you can drop into your stack. It hides your ASP.NET Core app behind Cloudflare, restores real client IPs, verifies Cloudflare Access JWTs, and boots cloudflared as a daemon. I listed every file and fully regenerated code (with comments) per your rules.
credentials-file: /etc/cloudflared/membership-core.json
ingress:
enabled: false
metrics: 127.0.0.1:40657
loglevel: info
protocol: quic # Faster, resilient edge↔tunnel transport
Description=Cloudflare Tunnel daemon
After=network-online.target
Wants=network-online.target
[Service]
User=cloudflared
Group=cloudflared
Environment=LOGNAME=cloudflared
ExecStart=/usr/local/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
CapabilityBoundingSet=
AmbientCapabilities=
CacheDirectory=cloudflared
[Install]
WantedBy=multi-user.target
services:
web:
build: ./src
ports:
- "5000:8080" # Kestrel inside container on 8080
environment:
- ASPNETCORE_URLS=http://0.0.0.0:8080
- ACCESS_AUD=urn:manoffocus:membership
- ACCESS_ISS=https://yourteam.cloudflareaccess.com
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel run --config /etc/cloudflared/config.yml
volumes:
- ./infrastructure/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
- ./infrastructure/cloudflared/creds.json:/etc/cloudflared/membership-core.json:ro
depends_on:
- web
// 1) Forwarded headers to restore real client IP from Cloudflare
builder.Services.Configure<ForwardedHeadersOptions>(opts =>
{
// Cloudflare sets CF-Connecting-IP; also provides standard X-Forwarded-For/Proto
opts.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// (Optional) trust loopback since tunnel terminates locally
opts.KnownProxies.Clear();
opts.KnownNetworks.Clear();
});
// 2) Cloudflare Access JWT validator service (see AccessJwtValidator)
builder.Services.AddSingleton<AccessJwtValidator>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseForwardedHeaders();
// 3) Middleware: validate Cloudflare Access JWT on all protected routes
app.Use(async (ctx, next) =>
{
// Allow health and static files without auth if you want
var path = ctx.Request.Path.Value?.ToLowerInvariant() ?? "";
if (path.StartsWith("/health") || path.StartsWith("/public"))
{
await next();
return;
}
<span><span><span>// Cloudflare sends JWT in Cf-Access-Jwt-Assertion</span></span><span><br></span><span><span>var</span></span><span> jwt = ctx.Request.Headers[</span><span><span>"Cf-Access-Jwt-Assertion"</span></span><span>].ToString();<br></span><span><span>if</span></span><span> (</span><span><span>string</span></span><span>.IsNullOrEmpty(jwt))<br>{<br> ctx.Response.StatusCode = </span><span><span>401</span></span><span>;<br> </span><span><span>await</span></span><span> ctx.Response.WriteAsync(</span><span><span>"Missing Cloudflare Access token."</span></span><span>);<br> </span><span><span>return</span></span><span>;<br>}<br><br></span><span><span>var</span></span><span> validator = ctx.RequestServices.GetRequiredService<AccessJwtValidator>();<br></span><span><span>var</span></span><span> result = </span><span><span>await</span></span><span> validator.ValidateAsync(jwt);<br><br></span><span><span>if</span></span><span> (!result.IsValid)<br>{<br> ctx.Response.StatusCode = </span><span><span>401</span></span><span>;<br> </span><span><span>await</span></span><span> ctx.Response.WriteAsync(</span><span><span>"Invalid Cloudflare Access token."</span></span><span>);<br> </span><span><span>return</span></span><span>;<br>}<br><br></span><span><span>// Attach principal for app-level authorization</span></span><span><br>ctx.User = </span><span><span>new</span></span><span> ClaimsPrincipal(result.ClaimsIdentity);<br><br></span><span><span>await</span></span><span> next();<br></span></span>
});
app.MapControllers();
app.MapGet("/", () => "Membership core behind Cloudflare Tunnel — OK");
app.MapGet("/health/live", () => Results.Ok(new { status = "live" }));
app.MapGet("/health/ready", () => Results.Ok(new { status = "ready" }));
app.Run();
{
private readonly HttpClient _http = new HttpClient();
// Configure via env or appsettings
private readonly string _issuer = Environment.GetEnvironmentVariable("ACCESS_ISS") ?? "https://yourteam.cloudflareaccess.com";
private readonly string _aud = Environment.GetEnvironmentVariable("ACCESS_AUD") ?? "urn:manoffocus:membership";
private JsonWebKeySet? _jwks;
private DateTime _jwksFetchedAt = DateTime.MinValue;
<span><span><span>public</span></span><span> </span><span><span>async</span></span><span> Task<(</span><span><span>bool</span></span><span> IsValid, ClaimsIdentity ClaimsIdentity)> ValidateAsync(</span><span><span>string</span></span><span> jwt)<br>{<br> </span><span><span>try</span></span><span><br> {<br> </span><span><span>var</span></span><span> handler = </span><span><span>new</span></span><span> JwtSecurityTokenHandler();<br> </span><span><span>var</span></span><span> parameters = </span><span><span>new</span></span><span> TokenValidationParameters<br> {<br> ValidIssuer = _issuer,<br> ValidateIssuer = </span><span><span>true</span></span><span>,<br> ValidateIssuerSigningKey = </span><span><span>true</span></span><span>,<br> ValidateAudience = </span><span><span>true</span></span><span>,<br> ValidAudience = _aud,<br> ValidateLifetime = </span><span><span>true</span></span><span>,<br> ClockSkew = TimeSpan.FromMinutes(</span><span><span>2</span></span><span>),<br> IssuerSigningKeys = </span><span><span>await</span></span><span> GetSigningKeysAsync()<br> };<br><br> </span><span><span>var</span></span><span> principal = handler.ValidateToken(jwt, parameters, </span><span><span>out</span></span><span> _);<br> </span><span><span>return</span></span><span> (</span><span><span>true</span></span><span>, (ClaimsIdentity)principal.Identity!);<br> }<br> </span><span><span>catch</span></span><span><br> {<br> </span><span><span>return</span></span><span> (</span><span><span>false</span></span><span>, </span><span><span>new</span></span><span> ClaimsIdentity());<br> }<br>}<br><br></span><span><span>private</span></span><span> </span><span><span>async</span></span><span> Task<IEnumerable<SecurityKey>> GetSigningKeysAsync()<br>{<br> </span><span><span>// Refresh JWKS every 12h</span></span><span><br> </span><span><span>if</span></span><span> (_jwks == </span><span><span>null</span></span><span> || (DateTime.UtcNow - _jwksFetchedAt) > TimeSpan.FromHours(</span><span><span>12</span></span><span>))<br> {<br> </span><span><span>var</span></span><span> jwksUrl = </span><span><span>$"<span>{_issuer}</span></span></span><span>/cdn-cgi/access/certs";<br> _jwks = </span><span><span>await</span></span><span> _http.GetFromJsonAsync<JsonWebKeySet>(jwksUrl);<br> _jwksFetchedAt = DateTime.UtcNow;<br> }<br> </span><span><span>return</span></span><span> _jwks!.Keys;<br>}<br></span></span>
}
{
private readonly RequestDelegate _next;
<span><span><span><span>public</span></span></span><span> </span><span><span>CloudflareRealIpMiddleware</span></span><span>(</span><span><span>RequestDelegate next</span></span><span>) => _next = next;<br><br></span><span><span><span>public</span></span></span><span> </span><span><span>async</span></span><span> Task </span><span><span>Invoke</span></span><span>(</span><span><span>HttpContext context</span></span><span>)<br>{<br> </span><span><span>if</span></span><span> (context.Request.Headers.TryGetValue(</span><span><span>"CF-Connecting-IP"</span></span><span>, </span><span><span>out</span></span><span> </span><span><span>var</span></span><span> cfIp) &&<br> IPAddress.TryParse(cfIp.ToString(), </span><span><span>out</span></span><span> </span><span><span>var</span></span><span> ip))<br> {<br> context.Connection.RemoteIpAddress = ip;<br> }<br> </span><span><span>await</span></span><span> _next(context);<br>}<br></span></span>
}
// In Program.cs, add: app.UseMiddleware<CloudflareRealIpMiddleware>(); before auth.
[Route("api/member")]
public class MemberController : ControllerBase
{
[HttpGet("me")]
public IActionResult Me()
{
var name = User.Identity?.Name ?? "(no name claim)";
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return Ok(new
{
user = name,
claims = User.Claims.Select(c => new { c.Type, c.Value }),
clientIp = ip
});
}
}
WORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ./ ./
RUN dotnet restore
RUN dotnet publish -c Release -o /out
FROM base AS final
WORKDIR /app
COPY --from=build /out ./
ENTRYPOINT ["dotnet", "Membership.Core.dll"]
Below is a complete, production-ready starter you can drop into your stack. It hides your ASP.NET Core app behind Cloudflare, restores real client IPs, verifies Cloudflare Access JWTs, and boots cloudflared as a daemon. I listed every file and fully regenerated code (with comments) per your rules.
File list & full code
- infrastructure/cloudflared/config.yml
Purpose: declarative Tunnel config. One tunnel, two services: web (HTTPS) and SSH (TCP). mTLS to CF, no inbound ports on your host.
tunnel: membership-corecredentials-file: /etc/cloudflared/membership-core.json
ingress:
Public hostname → local service
- hostname: app.yourdomain.com
service: http://localhost:5000
originRequest:
http2Origin: true # Enable HTTP/2 between CF edge and origin (via tunnel)
noTLSVerify: true # If you terminate TLS at CF and run HTTP to Kestrel locally
connectTimeout: 10s
keepAliveTimeout: 1m
disableChunkedEncoding: false
httpHostHeader: app.yourdomain.com
Optional: TCP/SSH over Tunnel via Cloudflare Access SSH
- hostname: ssh.yourdomain.com
service: ssh://localhost:22 - service: http_status:404
enabled: false
metrics: 127.0.0.1:40657
loglevel: info
protocol: quic # Faster, resilient edge↔tunnel transport
- infrastructure/cloudflared/cloudflared.service
Purpose: systemd unit to run the daemon at boot and auto-restart.
[Unit]Description=Cloudflare Tunnel daemon
After=network-online.target
Wants=network-online.target
[Service]
User=cloudflared
Group=cloudflared
Environment=LOGNAME=cloudflared
ExecStart=/usr/local/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
CapabilityBoundingSet=
AmbientCapabilities=
CacheDirectory=cloudflared
[Install]
WantedBy=multi-user.target
- infrastructure/cloudflare/zero-trust-access-policy.md
Purpose: define who can hit app.yourdomain.com before it ever reaches your origin.
- Application: app.yourdomain.com
- Policy: “Members Only”
- Decision: Allow if ALL:
- Identity Provider group: Members (Okta/AzureAD/Google Workspace)
- Device posture: (optional) WARP healthy AND OS ≠ rooted AND Disk encrypted
- Country: NOT (CN, RU, KP, IR) # adjust to taste
- Session duration: 12h
- JWT audience (aud): urn:manoffocus:membership
- JWT issuers: https://<your-team>.cloudflareaccess.com
- Headers added by CF edge:
- Cf-Access-Jwt-Assertion: <JWT>
- Cf-Connecting-Ip: <client-ip>
- Cf-Ipcountry: <country>
- Cf-Ray: <trace id>
- docker-compose.yml
Purpose: local/dev orchestration: your app + cloudflared (optional for dev).
version: "3.9"services:
web:
build: ./src
ports:
- "5000:8080" # Kestrel inside container on 8080
environment:
- ASPNETCORE_URLS=http://0.0.0.0:8080
- ACCESS_AUD=urn:manoffocus:membership
- ACCESS_ISS=https://yourteam.cloudflareaccess.com
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel run --config /etc/cloudflared/config.yml
volumes:
- ./infrastructure/cloudflared/config.yml:/etc/cloudflared/config.yml:ro
- ./infrastructure/cloudflared/creds.json:/etc/cloudflared/membership-core.json:ro
depends_on:
- web
- src/Program.cs
// Purpose: minimal ASP.NET Core 8 app wiring Cloudflare headers, Access JWT validation, and a health endpoint.
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
// 1) Forwarded headers to restore real client IP from Cloudflare
builder.Services.Configure<ForwardedHeadersOptions>(opts =>
{
// Cloudflare sets CF-Connecting-IP; also provides standard X-Forwarded-For/Proto
opts.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// (Optional) trust loopback since tunnel terminates locally
opts.KnownProxies.Clear();
opts.KnownNetworks.Clear();
});
// 2) Cloudflare Access JWT validator service (see AccessJwtValidator)
builder.Services.AddSingleton<AccessJwtValidator>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseForwardedHeaders();
// 3) Middleware: validate Cloudflare Access JWT on all protected routes
app.Use(async (ctx, next) =>
{
// Allow health and static files without auth if you want
var path = ctx.Request.Path.Value?.ToLowerInvariant() ?? "";
if (path.StartsWith("/health") || path.StartsWith("/public"))
{
await next();
return;
}
<span><span><span>// Cloudflare sends JWT in Cf-Access-Jwt-Assertion</span></span><span><br></span><span><span>var</span></span><span> jwt = ctx.Request.Headers[</span><span><span>"Cf-Access-Jwt-Assertion"</span></span><span>].ToString();<br></span><span><span>if</span></span><span> (</span><span><span>string</span></span><span>.IsNullOrEmpty(jwt))<br>{<br> ctx.Response.StatusCode = </span><span><span>401</span></span><span>;<br> </span><span><span>await</span></span><span> ctx.Response.WriteAsync(</span><span><span>"Missing Cloudflare Access token."</span></span><span>);<br> </span><span><span>return</span></span><span>;<br>}<br><br></span><span><span>var</span></span><span> validator = ctx.RequestServices.GetRequiredService<AccessJwtValidator>();<br></span><span><span>var</span></span><span> result = </span><span><span>await</span></span><span> validator.ValidateAsync(jwt);<br><br></span><span><span>if</span></span><span> (!result.IsValid)<br>{<br> ctx.Response.StatusCode = </span><span><span>401</span></span><span>;<br> </span><span><span>await</span></span><span> ctx.Response.WriteAsync(</span><span><span>"Invalid Cloudflare Access token."</span></span><span>);<br> </span><span><span>return</span></span><span>;<br>}<br><br></span><span><span>// Attach principal for app-level authorization</span></span><span><br>ctx.User = </span><span><span>new</span></span><span> ClaimsPrincipal(result.ClaimsIdentity);<br><br></span><span><span>await</span></span><span> next();<br></span></span>
});
app.MapControllers();
app.MapGet("/", () => "Membership core behind Cloudflare Tunnel — OK");
app.MapGet("/health/live", () => Results.Ok(new { status = "live" }));
app.MapGet("/health/ready", () => Results.Ok(new { status = "ready" }));
app.Run();
- src/Services/AccessJwtValidator.cs
// Purpose: verify the CF Access JWT (issuer/audience, signature). Uses JWKS fetch + cache.
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Net.Http.Json;
{
private readonly HttpClient _http = new HttpClient();
// Configure via env or appsettings
private readonly string _issuer = Environment.GetEnvironmentVariable("ACCESS_ISS") ?? "https://yourteam.cloudflareaccess.com";
private readonly string _aud = Environment.GetEnvironmentVariable("ACCESS_AUD") ?? "urn:manoffocus:membership";
private JsonWebKeySet? _jwks;
private DateTime _jwksFetchedAt = DateTime.MinValue;
<span><span><span>public</span></span><span> </span><span><span>async</span></span><span> Task<(</span><span><span>bool</span></span><span> IsValid, ClaimsIdentity ClaimsIdentity)> ValidateAsync(</span><span><span>string</span></span><span> jwt)<br>{<br> </span><span><span>try</span></span><span><br> {<br> </span><span><span>var</span></span><span> handler = </span><span><span>new</span></span><span> JwtSecurityTokenHandler();<br> </span><span><span>var</span></span><span> parameters = </span><span><span>new</span></span><span> TokenValidationParameters<br> {<br> ValidIssuer = _issuer,<br> ValidateIssuer = </span><span><span>true</span></span><span>,<br> ValidateIssuerSigningKey = </span><span><span>true</span></span><span>,<br> ValidateAudience = </span><span><span>true</span></span><span>,<br> ValidAudience = _aud,<br> ValidateLifetime = </span><span><span>true</span></span><span>,<br> ClockSkew = TimeSpan.FromMinutes(</span><span><span>2</span></span><span>),<br> IssuerSigningKeys = </span><span><span>await</span></span><span> GetSigningKeysAsync()<br> };<br><br> </span><span><span>var</span></span><span> principal = handler.ValidateToken(jwt, parameters, </span><span><span>out</span></span><span> _);<br> </span><span><span>return</span></span><span> (</span><span><span>true</span></span><span>, (ClaimsIdentity)principal.Identity!);<br> }<br> </span><span><span>catch</span></span><span><br> {<br> </span><span><span>return</span></span><span> (</span><span><span>false</span></span><span>, </span><span><span>new</span></span><span> ClaimsIdentity());<br> }<br>}<br><br></span><span><span>private</span></span><span> </span><span><span>async</span></span><span> Task<IEnumerable<SecurityKey>> GetSigningKeysAsync()<br>{<br> </span><span><span>// Refresh JWKS every 12h</span></span><span><br> </span><span><span>if</span></span><span> (_jwks == </span><span><span>null</span></span><span> || (DateTime.UtcNow - _jwksFetchedAt) > TimeSpan.FromHours(</span><span><span>12</span></span><span>))<br> {<br> </span><span><span>var</span></span><span> jwksUrl = </span><span><span>$"<span>{_issuer}</span></span></span><span>/cdn-cgi/access/certs";<br> _jwks = </span><span><span>await</span></span><span> _http.GetFromJsonAsync<JsonWebKeySet>(jwksUrl);<br> _jwksFetchedAt = DateTime.UtcNow;<br> }<br> </span><span><span>return</span></span><span> _jwks!.Keys;<br>}<br></span></span>
}
- src/Middleware/CloudflareRealIpMiddleware.cs (optional, if you prefer explicit CF header handling)
// Purpose: if you want to read CF-Connecting-IP specifically and stamp HttpContext.Connection.RemoteIpAddress.
using System.Net;
{
private readonly RequestDelegate _next;
<span><span><span><span>public</span></span></span><span> </span><span><span>CloudflareRealIpMiddleware</span></span><span>(</span><span><span>RequestDelegate next</span></span><span>) => _next = next;<br><br></span><span><span><span>public</span></span></span><span> </span><span><span>async</span></span><span> Task </span><span><span>Invoke</span></span><span>(</span><span><span>HttpContext context</span></span><span>)<br>{<br> </span><span><span>if</span></span><span> (context.Request.Headers.TryGetValue(</span><span><span>"CF-Connecting-IP"</span></span><span>, </span><span><span>out</span></span><span> </span><span><span>var</span></span><span> cfIp) &&<br> IPAddress.TryParse(cfIp.ToString(), </span><span><span>out</span></span><span> </span><span><span>var</span></span><span> ip))<br> {<br> context.Connection.RemoteIpAddress = ip;<br> }<br> </span><span><span>await</span></span><span> _next(context);<br>}<br></span></span>
}
// In Program.cs, add: app.UseMiddleware<CloudflareRealIpMiddleware>(); before auth.
- src/Controllers/MemberController.cs
// Purpose: sample protected API endpoint returning identity and client IP.
using Microsoft.AspNetCore.Mvc;
[Route("api/member")]
public class MemberController : ControllerBase
{
[HttpGet("me")]
public IActionResult Me()
{
var name = User.Identity?.Name ?? "(no name claim)";
var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return Ok(new
{
user = name,
claims = User.Claims.Select(c => new { c.Type, c.Value }),
clientIp = ip
});
}
}
- src/Dockerfile
Purpose: containerize the ASP.NET Core app for clean deployments behind Tunnel.
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS baseWORKDIR /app
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ./ ./
RUN dotnet restore
RUN dotnet publish -c Release -o /out
FROM base AS final
WORKDIR /app
COPY --from=build /out ./
ENTRYPOINT ["dotnet", "Membership.Core.dll"]
- src/appsettings.json
{
"Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
"AllowedHosts": "*"
} - README.md
Purpose: operator runbook.
Bootstrapping Cloudflare Tunnel
- Install cloudflared:
- Linux: curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared
- Login & create tunnel:
- cloudflared tunnel login
- cloudflared tunnel create membership-core
- This outputs a credentials JSON; place it at /etc/cloudflared/membership-core.json
- Configure DNS:
- cloudflared tunnel route dns membership-core app.yourdomain.com
- Put config at /etc/cloudflared/config.yml (see file above).
- systemd:
- useradd --system --home /etc/cloudflared --shell /usr/sbin/nologin cloudflared
- chown -R cloudflared:cloudflared /etc/cloudflared
- cp infrastructure/cloudflared/cloudflared.service /etc/systemd/system/
- systemctl daemon-reload && systemctl enable --now cloudflared
- Verify:
- systemctl status cloudflared
- curl -I https://app.yourdomain.com (should return 200 or 302 to Access).
Zero Trust (Access) policy
- Create an “Application” for app.yourdomain.com in Cloudflare Zero Trust.
- Add an Access policy for your Members group and set AUD to urn:manoffocus:membership.
- Save the team domain (ISS) and set env vars on your app host/container:
- ACCESS_ISS=https://yourteam.cloudflareaccess.com
- ACCESS_AUD=urn:manoffocus:membership
ASP.NET integration checklist
- Restore real client IPs: ForwardedHeaders + (optional) CloudflareRealIpMiddleware.
- Trust only Cloudflare: make sure your firewall blocks inbound; only cloudflared dials out.
- JWT verification: Cf-Access-Jwt-Assertion header → AccessJwtValidator.
- WebSockets/SignalR: supported through Tunnel out of the box.
- gRPC: works over HTTP/2 via Tunnel (http2Origin: true).
- Health: /health/live and /health/ready for probes.
- Logs: correlate with CF-Ray header for request tracing.
Design notes (plain English)
- “Edge node” vs “origin”: your server is not a Cloudflare POP, but with Tunnel + Access + WAF the edge makes decisions (identity, country, bot, rate-limit) and only then forwards to your private origin through encrypted outbound channels you control. That’s the secure, modern pattern.
- Redundancy: you can run multiple cloudflared instances (same tunnel) on different boxes; CF will load-balance to healthy connections automatically.
- Private services: you can add more hostnames (e.g., api.yourdomain.com, boardroom.yourdomain.com) in config.yml and route each to different local ports/containers.
- Device posture (optional but strong): require WARP + posture checks for admins; members can stay identity-only.
- Performance: protocol: quic is resilient; keep HTTP/2 to Kestrel for multiplexing.
- Secrets: do not bake the Access issuer/audience into code in prod; use env or Key Vault.
- CF Turnstile or Bot Management for signup/abuse control
- Tiered caching rules for your images/videos
- Web analytics + logs (Cloudflare Logpush to your SIEM)
- A second tunnel for SSH/RDP bastion with per-user just-in-time access.