npm.io
1.0.3 • Published yesterday

sla-wizard-plugin-auth-request-ratelimit

Licence
Version
1.0.3
Deps
1
Size
126 kB
Vulns
0
Weekly
0

sla-wizard-plugin-auth-request-ratelimit

A sla-wizard plugin that augments nginx's static limit_req rate limiting with a dynamic auth_request-based delegation to alma-telemeter.

When alma-telemeter is reachable, every request passes through a subrequest that enforces the rate limit and returns live X-RateLimit-* headers. When alma-telemeter is unreachable, nginx falls back to its own limit_req token-bucket limiter so SLA limits are always enforced.


How it fits in the plugin chain

configNginxAuthRequest
  └─ sla-wizard-plugin-custom-baseurl  (configNginxBaseUrl)
       └─ sla-wizard-plugin-nginx-strip  (configNginxStrip)
            └─ sla-wizard-nginx-confd  (configNginxConfd)

Architecture: dual-layer rate limiting

The plugin deliberately keeps both limiters active at all times:

Client request
  │
  ├─ [nginx preaccess] limit_req zone=… burst=N nodelay
  │     token-bucket guard; burst = SLA_max − 1
  │     → 429 @rate_limited_static (if burst exhausted)
  │
  ├─ [nginx rewrite]  set $rl_limit_static N+1
  │
  └─ [nginx access]   auth_request /internal/rate-limit-check
        │
        ├─ Telemeter UP → 200 (allowed) or 403 (exceeded)
        │     200 → forward upstream + dynamic X-RateLimit-* headers
        │     403 → error_page 403 =429 @rate_limited (dynamic headers)
        │
        └─ Telemeter DOWN (5xx / timeout)
              → /_telemeter_down returns 200
              → request allowed through; limit_req already ran above
              → static X-RateLimit-* headers from map fallback
Path summary
Scenario Enforcer Rate-limit headers
Telemeter UP, within limit telemeter (fixed-window Redis) Dynamic from telemeter
Telemeter UP, limit exceeded telemeter → @rate_limited Dynamic from telemeter
Telemeter DOWN, within burst limit_req (token-bucket) Static ($rl_limit_static)
Telemeter DOWN, burst exceeded limit_req@rate_limited_static Static, partial
Redis DOWN telemeter fail-open → limit_req Static ($rl_limit_static)

Installation

npm install sla-wizard-plugin-auth-request-ratelimit
Runtime dependency

alma-telemeter must be reachable from nginx. It receives subrequests at /internal/rate-limit and responds with X-RateLimit-* headers.

Default endpoint: http://127.0.0.1:2047/internal/rate-limit. Override with --telemeterUrl CLI option or TELEMETER_URL environment variable.


CLI usage

// cli.js
const slaWizard = require('sla-wizard');
const authRequestPlugin = require('sla-wizard-plugin-auth-request-ratelimit');

slaWizard.use(authRequestPlugin);
slaWizard.program.parse(process.argv);
node cli.js --help
config-nginx-auth-request

Generates a full nginx configuration (nginx.conf + conf.d/) with auth_request-based rate limiting applied.

node cli.js config-nginx-auth-request \
  -o <outputDirectory> \
  --oas <pathToOAS>                   \  # default: ./specs/oas.yaml
  --sla <slaFileOrDirectory>          \  # default: ./specs/sla.yaml
  --authLocation <header|query|url>   \  # default: header
  --authName <paramName>              \  # default: apikey
  --proxyPort <port>                  \  # default: 80
  --telemeterUrl <url>                \  # default: http://127.0.0.1:2047/internal/rate-limit
  [--customTemplate <file>]
add-to-auth-request-confd

Generates only conf.d/ files for a new user without touching nginx.conf. Use when nginx.conf was already generated by a previous config-nginx-auth-request run.

node cli.js add-to-auth-request-confd \
  -o <outputDirectory> \
  --oas <pathToOAS> \
  --sla <slaFileOrDirectory>

Programmatic usage

const slaWizard = require('sla-wizard');
const authRequestPlugin = require('sla-wizard-plugin-auth-request-ratelimit');

slaWizard.use(authRequestPlugin);

// Full config (nginx.conf + conf.d/)
slaWizard.configNginxAuthRequest({
  outDir:       './nginx-output',
  oas:          './specs/oas.yaml',
  sla:          './specs/slas',
  telemeterUrl: 'http://telemeter-host:2047/internal/rate-limit', // optional
});

// conf.d only (nginx.conf already exists)
slaWizard.addToAuthRequestConfd({
  outDir: './nginx-output',
  oas:    './specs/oas.yaml',
  sla:    './specs/slas/new-user.yaml',
});
Direct import
const {
  DEFAULT_TELEMETER_URL,
  applyAuthRequestToConfd,                   // (content: string) → string
  applyAuthRequestToNginxConf,               // (content: string, telemeterUrl?: string) → string
  applyAuthRequestTransformations,           // (outDir: string,  telemeterUrl?: string) → void
  applyAuthRequestTransformationsConfdOnly,  // (outDir: string) → void
} = require('sla-wizard-plugin-auth-request-ratelimit');

const transformed = applyAuthRequestToConfd(fs.readFileSync('user.conf', 'utf8'));
applyAuthRequestTransformations('./nginx-output', 'http://telemeter-host:2047/internal/rate-limit');

nginx transformations applied

nginx.conf changes
Element After transformation
limit_req_zone … (kept — nginx-native fallback when telemeter is down)
limit_req_status 429; (kept — fallback)
Before server { Two map directives injected (header fallback logic)
Before include conf.d/*.conf; location /internal/rate-limit-check injected
Before include conf.d/*.conf; location = /_telemeter_down injected
Before include conf.d/*.conf; location @rate_limited injected
Before include conf.d/*.conf; location @rate_limited_static injected

Injected map directives (http context, before server {):

map $rl_limit $rl_limit_header {
    ""      $rl_limit_static;   # telemeter down or Redis fail-open → static value
    default $rl_limit;          # telemeter up → dynamic value
}

map $rl_remaining $rl_remaining_header {
    ""      -1;                 # telemeter down → -1 signals unavailable
    default $rl_remaining;      # telemeter up → dynamic value
}

Injected /internal/rate-limit-check block:

location /internal/rate-limit-check {
    internal;
    proxy_pass              http://127.0.0.1:2047/internal/rate-limit;
    proxy_pass_request_body off;
    proxy_set_header        X-API-Key  $http_apikey;
    proxy_set_header        X-Endpoint $uri_original;
    proxy_set_header        X-Method   $request_method;
    proxy_connect_timeout   500ms;
    proxy_read_timeout      1s;
    proxy_intercept_errors  on;
    error_page 500 502 503 504 =200 /_telemeter_down;
}

Injected /_telemeter_down block (telemeter unreachable → allow through, fall back to limit_req):

location = /_telemeter_down {
    internal;
    return 200;
}

Injected @rate_limited block (telemeter rejected the request → dynamic 429):

location @rate_limited {
    default_type application/json;
    add_header X-RateLimit-Limit     $rl_limit always;
    add_header X-RateLimit-Remaining 0         always;
    add_header X-RateLimit-Reset     $rl_reset always;
    add_header Retry-After           $rl_retry always;
    return 429 '{"error":"TooManyRequests","message":"Rate limit exceeded","status":429}';
}

Injected @rate_limited_static block (limit_req rejected the request → static 429):

location @rate_limited_static {
    default_type application/json;
    add_header X-RateLimit-Remaining 0   always;
    add_header Retry-After           60  always;
    return 429 '{"error":"TooManyRequests","message":"Rate limit exceeded","status":429}';
}

conf.d/*.conf changes (per rate-limited location block)

The original limit_req line is kept as the first directive (nginx-native fallback). The auth_request block is injected immediately after it.

Before:

location /sla-user1_endpoint_POST {
    rewrite … break;
    proxy_pass http://localhost:8000;
    limit_req zone=sla-user1_endpoint_POST burst=4 nodelay;
}

After:

location /sla-user1_endpoint_POST {
    rewrite … break;
    proxy_pass http://localhost:8000;
    limit_req zone=sla-user1_endpoint_POST burst=4 nodelay;   # kept as fallback

    set $rl_limit_static          5;   # burst+1 = SLA max; static header fallback
    auth_request      /internal/rate-limit-check;
    auth_request_set  $rl_limit     $upstream_http_x_ratelimit_limit;
    auth_request_set  $rl_remaining $upstream_http_x_ratelimit_remaining;
    auth_request_set  $rl_reset     $upstream_http_x_ratelimit_reset;
    auth_request_set  $rl_retry     $upstream_http_retry_after;

    add_header X-RateLimit-Limit     $rl_limit_header     always;
    add_header X-RateLimit-Remaining $rl_remaining_header always;
    add_header X-RateLimit-Reset     $rl_reset            always;

    error_page 403 =429 @rate_limited;
    error_page 429      @rate_limited_static;
}

$rl_limit_header and $rl_remaining_header resolve via the map directives in nginx.conf:

  • Telemeter UP: take dynamic values from telemeter response headers.
  • Telemeter DOWN: $rl_limit_header$rl_limit_static; $rl_remaining_header-1.

Response headers

On successful (2xx) responses
Telemeter state Header Value
UP X-RateLimit-Limit Dynamic — SLA max from telemeter
UP X-RateLimit-Remaining Dynamic — requests left in current window
UP X-RateLimit-Reset Dynamic — Unix timestamp of window reset
DOWN X-RateLimit-Limit Static — $rl_limit_static (SLA burst + 1)
DOWN X-RateLimit-Remaining -1 (sentinel: telemeter unavailable)
DOWN X-RateLimit-Reset (absent — limit_req does not expose window time)
On rate-limited (429) responses

Via @rate_limited (telemeter rejected the request):

Header Value
X-RateLimit-Limit Dynamic — SLA max
X-RateLimit-Remaining 0
X-RateLimit-Reset Dynamic — Unix timestamp of window reset
Retry-After Dynamic — seconds until window resets

Via @rate_limited_static (limit_req burst exhausted before auth_request ran):

Header Value
X-RateLimit-Remaining 0
Retry-After 60 (conservative hardcoded fallback)
X-RateLimit-Limit (absent — set $rl_limit_static runs in rewrite phase, after limit_req preaccess rejection)
X-RateLimit-Reset (absent — limit_req does not expose window time)

Response body (both 429 paths):

{"error":"TooManyRequests","message":"Rate limit exceeded","status":429}
Headers not added

Responses rejected before any location block runs — 401 Unauthorized (missing API key) and 403 Forbidden (unknown API key) — do not carry X-RateLimit-* headers because the auth_request subrequest never fires for those requests.


Exported API reference

Export Signature Description
apply (program, ctx) → void sla-wizard plugin entry point
DEFAULT_TELEMETER_URL string http://127.0.0.1:2047/internal/rate-limit
configNginxAuthRequest (options, ctx) → void Full config generation (nginx.conf + conf.d/)
addToAuthRequestConfd (options, ctx) → void conf.d-only generation
applyAuthRequestToConfd (content: string) → string Transforms one conf.d file content
applyAuthRequestToNginxConf (content: string, telemeterUrl?: string) → string Transforms nginx.conf content
applyAuthRequestTransformations (outDir: string, telemeterUrl?: string) → void Transforms all files in outDir
applyAuthRequestTransformationsConfdOnly (outDir: string) → void Transforms only conf.d/ files

Testing

# Unit tests + container integration tests (requires Docker)
npm test

# Unit tests only (no Docker required)
npx mocha ./tests/tests.js
Test suite overview
Suite Tests Type
applyAuthRequestToConfd 17 Unit (pure string)
applyAuthRequestToNginxConf 17 Unit (pure string)
applyAuthRequestTransformations 6 Unit (filesystem)
applyAuthRequestTransformationsConfdOnly 4 Unit (filesystem)
Module shape + plugin registration 9 Unit
configNginxAuthRequest programmatic 23 Integration
addToAuthRequestConfd programmatic 6 Integration
CLI usage 10 CLI (execSync)
Container: X-RateLimit headers under limit 8 Container (Docker)
Container: 429 with correct headers 6 Container (Docker)
Container: independent counters per key 3 Container (Docker)
Container: authentication checks 2 Container (Docker)
Total 111

The container tests spin up three Docker containers (echo backend, mock alma-telemeter, nginx with the generated config) and verify the full request cycle end-to-end, including live header values and rate-limit enforcement.