Limen.Plug (limen v0.2.1)

Copy Markdown View Source

The request gate.

Place it in your endpoint before the router (and before Plug.Static if asset requests should count towards client behaviour):

plug Limen.Plug,
  otp_app: :my_app,
  policy: MyApp.BotPolicy,
  routes: [
    {"/health", :off},
    {"/assets", :track},
    {"/login", MyApp.LoginPolicy, mode: :enforce}
  ]

Pipeline

Every request goes through the same stages; the first to settle it wins:

  1. Identify the client: address, prefix and JA4 (see Limen.Signal), and track its behaviour.
  2. Trust: the policy's trust rules, which can use the facts the application stated (see Limen.put_facts/2), allow the request before anything else.
  3. Ban: a banned prefix is denied, or sent to the maze (see Limen.Maze) when its ban says so.
  4. Limit: the policy's hard limits.
  5. Pass: a valid pass cookie (see Limen.Challenge) allows the request right away, without collecting any other signal.
  6. Collect the policy's signals.
  7. Rules: allow and deny rules, then scoring and decide. See Limen.Policy.
  8. Act: continue, or respond on the application's behalf: 403 for denials, 429 with Retry-After for throttling, the challenge page for challenged navigations, a slow maze page for GET requests sent to the maze.

Requests under the challenge path (/__limen by default) are Limen's own endpoints and never reach the application. They are served by the plug's instance, and verify solutions with the keys of whichever instance issued the challenge.

Requests under a trap path of the plug's instance (see Limen.Trap) are settled at the :trap stage, before any route: the client is flagged and sent to the maze, unless a trust rule of the policy covering the path trusts it.

Each evaluation produces a Limen.Decision, emitted as a [:limen, :decision] telemetry event, sampled into Limen.DecisionLog, and stored in conn.private[:limen] (see Limen.decision/1).

The Limen instance is resolved when the plug is initialised (at compile time in an endpoint); at runtime the plug reads it with a single :persistent_term lookup per request.

In the router

In the endpoint, the plug sees every request but not your application's session, so its policies cannot tell signed-in users apart. To gate with facts about the user (see Limen.put_facts/2), put the plug in a router pipeline, after whatever states the facts:

# router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :limen_facts
  plug Limen.Plug, otp_app: :my_app, policy: MyApp.BotPolicy
  ...
end

defp limen_facts(conn, _opts),
  do: Limen.put_facts(conn, signed_in: get_session(conn, :user_token) != nil)

The router only runs a pipeline for paths it has a route for, so Limen's own endpoints and trap paths never reach that plug. Keep one in the endpoint, with policy: :off, to serve them and nothing else:

# endpoint.ex, before the router
plug Limen.Plug, otp_app: :my_app, policy: :off

Requests that no gated pipeline handles (static files, APIs) are then not evaluated at all; give them a pipeline of their own, or gate them in the endpoint with :routes.

Options

  • :instance - the name of the Limen instance to use.
  • :otp_app - use the instance named after this application, as started by {Limen, otp_app: app}. One of :instance or :otp_app is required.
  • :policy - the policy module for requests no route matches, or :off or :track to treat them like routes with those targets (below). Defaults to Limen.Policy.Default. Limen's endpoints and trap paths are served whatever the policy.
  • :mode - :dry_run or :enforce, overriding the policy's and the instance's mode.
  • :routes - per-path overrides, as {path, target} or {path, policy, opts}. A path matches itself and everything below it ("/login" matches /login and /login/otp, not /loginx); the most specific path wins. Targets:
    • :off - Limen does nothing at all, for health checks and the like.
    • :track - track client behaviour and enforce bans, nothing else; for static assets.
    • a policy module, with optional mode: and instance: (to use another instance for that path).

Dry-run

In dry-run mode the pipeline runs unchanged, including every state update (counters, limits, bans) and telemetry event; only the final action is skipped and the request continues. decision.enforced tells the two apart. Bans created in dry-run mode are never enforced, not even on enforcing routes.

The mode of a request is, from the most specific: its own, set by Limen.Test.put_mode/2 in tests; the route's mode:; the plug's :mode; the policy's; the instance's.