Skip to content
camada
Menu

Docs

.NET: ASP.NET Core

Camada enforces your snapshot inside a .NET process: your ordered custom rules, then allow, block and challenge, decided in-process, with events shipped off the request path. One package with an ASP.NET Core middleware and three HttpContext helpers.

Install

Not on NuGet yet. From the folder that holds your .csproj: clone it next to the project, then reference it. Next to it, not inside it: an SDK-style project compiles every .cs file under its folder.

shell
git clone https://github.com/camada-app/camada-dotnet ../camada-dotnet
dotnet add reference ../camada-dotnet/src/Camada/Camada.csproj

.NET 8. No runtime dependencies beyond the ASP.NET Core shared framework.

Mount it

Program.cs
using Camada.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCamada();      // one engine from the environment, drained on shutdown

var app = builder.Build();
app.UseCamada();                   // FIRST: camada answers before routing, static files, anything
app.MapGet("/", (HttpContext ctx) => Results.Content($"<html><head>{ctx.CamadaScriptTag()}</head>…", "text/html"));
app.Run();

AddCamada() builds the engine when the container first resolves it — on the first request through the middleware — from the environment:

shell
export CAMADA_KEY=<ingest_token>.<snap_token>
export CAMADA_INGEST_URL=http://localhost:8787   # dev only; defaults to production ingest

Set these as environment variables: export them in the shell that runs dotnet run, add them to "environmentVariables" in Properties/launchSettings.json, or set them in your host's settings. camada reads only the environment, and ASP.NET does not pass appsettings.json, user-secrets or a .env file to it.

That build starts the snapshot poll on a background task and never blocks, so the request that triggered it is answered cold: it passes (fail open), and so does anything that arrives before the first poll lands. To enforce from request 1, resolve the engine at startup and wait for the boot poll:

warm-up
// beside using Camada.AspNetCore; at the top of Program.cs
using Camada;
using Camada.Snapshot;

// after var app = builder.Build();
var engine = app.Services.GetRequiredService<CamadaEngine>();   // builds it; the boot poll is already running
if (engine.Snap != null)                                       // null when CAMADA_KEY is unset or CAMADA_DISABLED=1
{
    var deadline = Environment.TickCount64 + 5000;
    while (engine.Snap.Verdict(new MatchInput { Ip = "0.0.0.0" }).Reason == "cold" && Environment.TickCount64 < deadline)
    {
        Thread.Sleep(10);   // bounded: an unreachable analyst leaves it cold, and the app still fails open
    }
}

An app that reads its own configuration hands the values in through the options — credentials still travel as an environment map, never as constructor arguments. Outside the generic host, UseCamada() alone falls back to CamadaEngine.Default, a lazy singleton wired from the process environment; CamadaEngine.Configure(options) replaces it.

explicit config
using Camada.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCamada(o =>   // instead of AddCamada(); myKey, myIngest: read from your own configuration
{
    o.Env = new Dictionary<string, string?> { ["CAMADA_KEY"] = myKey, ["CAMADA_INGEST_URL"] = myIngest };
    o.ChallengePath = "/api/__camada/challenge";
});

Quickstart: ASP.NET Core.

The options you will use

OptionDefaultMeaning
Envthe process environmentWhere CAMADA_KEY and friends are read from (the process one is read per request, for the kill switch). Pass a dictionary to configure in code.
TransportHttpClientThe delegate that reaches the analyst; tests hand in an in-process fake.
RefreshSserver-steeredPoll cadence in seconds. Unset, the tenant config's poll_seconds steers it; set, it is pinned.
ChallengetrueServe the proof-of-work page for challenge verdicts (CAMADA_CHALLENGE=0 switches it off too).
ChallengePath/__camada/challengeWhere that page posts its solution.
SnapshotVersion54 drops your ordered custom rules; 3 the allow and challenge sections too.
ScriptPath/_cam/b.jsWhere the first-party beacon script is served.
FpPath/_cam/fpWhere that script posts the beacon. Keep it in the same directory as ScriptPath: the script derives one from the other.

Environment: CAMADA_KEY, CAMADA_INGEST_URL, CAMADA_SNAPSHOT_URL, CAMADA_TRUSTED_PROXY, CAMADA_SERVERLESS=1 (a per-request staleness check, no timer), CAMADA_CHALLENGE=0, and the kill switch CAMADA_DISABLED=1, checked per request (set at boot, no timer starts at all). The docs index lists what each one means.

What it does per request

  1. Keeps the snapshot fresh off the request path: a background task on a PeriodicTimer at the cadence your tenant config sets (gzip on the wire, 304 when nothing changed), asking for snapshot v5, the container that carries your ordered custom rules. CAMADA_SERVERLESS=1 checks staleness per request instead.
  2. Resolves the client from the socket peer (Connection.RemoteIpAddress), combined with X-Forwarded-For only under your trusted-proxy setting. A raw forwarded header is never trusted without one. The SDK never reads the peer through the forwarded-headers middleware: if you add UseForwardedHeaders(), keep UseCamada() before it, so camada judges the raw header itself.
  3. Runs your ordered custom rules, then the allow, block and challenge lists. A blocked request gets 403 with x-block-reason (and x-block-rule when a rule decided) before your app runs. The event still ships, marked as an SDK block so it is not confused with your own 403s.
  4. A challenged browser gets the proof-of-work page; an API call reaches your app, and its response carries x-camada-challenge: 1 for your frontend to act on. A request whose IP cannot be resolved is let through. See challenge.
  5. Serves the beacon endpoints, then runs your app with x-rid and the _sfp session cookie on its response (stamped from Response.OnStarting, so a handler that clears headers still carries them), the context in HttpContext.Items["camada"], and when the response is done ships one batched, redacted event with its real status, latency and the matched route pattern. An exception in your app ships as st: 500 and propagates unchanged.

Add the beacon

Bots that never run JavaScript are the cheapest to catch. Put the tag in the <head> of the pages you render and the middleware does the rest:

beacon
// Program.cs, above app.Run(): Mount it already maps this route
app.MapGet("/", (HttpContext ctx) =>
    Results.Content($"<html><head>{ctx.CamadaScriptTag()}</head><body>…</body></html>", "text/html"));

ctx.CamadaScriptTag() returns a <script src="/_cam/b.js?r=…" async> tag carrying this request's event id, so the beacon is joined to the page view it came from. The middleware serves the script at GET /_cam/b.js (an embedded resource, cacheable for an hour) and relays POST /_cam/fp onto the event batch, stamped with the client address camada resolved — never the one the body claims. Both endpoints sit behind the verdict, so a blocked client gets 403 there too. Turn the beacon off in the project's settings and the tag becomes empty and both endpoints fall through to your app.

Report what your app knows

The wire shows a POST /login; only your handler knows whether it failed. Tell camada from the handler:

outcomes
// Program.cs, above app.Run(); LoginForm (with an Email) and SignIn are your own form type and sign-in check
app.MapPost("/login", async (HttpContext ctx, LoginForm form) =>
{
    var ok = await SignIn(form);
    if (!ok) ctx.CamadaTrack("login_failed", user: form.Email);   // hashed in-process; never throws
    return ok ? Results.Redirect("/") : Results.Json(new { error = "invalid" }, statusCode: 401);
});

ctx.CamadaTrack(event, user: …) ships the outcome joined to this request's event and session. The user identifier is hashed in-process with your ingest token before it is queued — the raw value never leaves the process. It never throws; on a request the middleware did not run for it still ships the event, with no rid, sid or ip to join it to. The event name is free-form; the analyst's rules read this vocabulary:

EventWhen
login_failed / login_succeededa credential check settled
signupan account was created
password_reseta reset was requested
mfa_faileda second factor was rejected
payment_failed / payment_succeededa charge settled
coupon_faileda promo code was rejected

A route you want to gate yourself can serve the challenge on demand; the answer is an IResult to return from the handler:

gating a route
// Program.cs, above app.Run(): the proof-of-work page (or 403 JSON) until this browser holds a valid _cch, then null
app.MapGet("/export", (HttpContext ctx) => ctx.CamadaServeChallenge() ?? BigFile());   // BigFile(): your response, an IResult

What this position sees

The same position as @camada/node, in a .NET process: the final status and latency, the session, the route pattern, the beacon's browser signals and your app's outcomes. The event carries the header order as IHeaderDictionary reports it, which is not the wire's order under Kestrel — camada knows what this tap can see from the position itself and never scores the absence of header order as evidence against a request. No ASN, country or TLS fingerprint reaches the middleware, so those conditions fail open in-app and the analyst resolves ASN and country itself. matches patterns are JS regexes run on Regex in ECMAScript mode (retried on the default engine when that mode refuses a spelling; one neither accepts never matches here while it does at the edge), each under a 50 ms timeout that reads as no match. What a rule can key on is on rules and list entries.

Deploying it

  • Every process polls its own snapshot (about 5 MB resident) and flushes its own batches; the tenant's poll_seconds keeps the cadence honest across a fleet.
  • Pending events drain on the host's ApplicationStopping within half a second, and every engine also drains on ProcessExit (what a bare CamadaEngine.Default relies on). No signal handlers are installed — an app owns its own shutdown — so a process killed outright may drop its last batch.
  • Serverless hosts: CAMADA_SERVERLESS=1. A cold invocation fails open and catches up on the next one.
  • The poll is a plain background task, not an IHostedService: it needs no host to run under and starts with the engine.

It fails open

Without a key the middleware is inert: one log line, no requests, no enforcement, so an unprovisioned environment behaves exactly as if camada were not installed. Beyond that, a dead ingest (logged at most once a minute, through ILogger under AddCamada() or stderr otherwise — the message only, never a stack trace), a corrupt snapshot or a bug in the package costs you the telemetry, never the request — camada can never turn one of your responses into a 5xx. The whole stance is on the security page.