Skip to content

Nginx ACL

Overview

The Nginx ACL plugin implements an F5-style access control list system for nginx. It uses the njs (nginx JavaScript module) to evaluate rules at request time with near-zero latency.

Key concepts:

  • Ruleset (= policy) - a named collection of rules with a single default action
  • Rule - one or more conditions combined with AND logic
  • Match Mode - how rules within a ruleset are combined (any = OR, all = AND)
  • Condition - a single test (IP match, header match, URI match, etc.)
  • IP Group - a named set of IP addresses/CIDRs that can be referenced in conditions
  • Public vs Private - public rulesets are shared across all websites; private rulesets belong to one website

Plugin Required

Nginx ACL requires the nginx-acl plugin to be enabled. All endpoints below require superuser except the per-website assignment endpoints (which require websites_administration).

Model

Ruleset (policy)
├── action: { type: "deny" | "allow" | "redirect", value: "..." }
├── match_mode: "any" | "all"
└── Rules[]
    ├── enabled: true
    └── Conditions[]  (AND-combined within a rule)
        ├── type: "ip" | "header" | "uri" | "method" | "country" | "ip_group"
        ├── operator: "eq" | "ne" | "contains" | "startswith" | "regex" | "in"
        └── value: "..."

Match Mode Semantics

match_mode Behaviour
any (default) Action fires if any rule matches (OR logic across rules)
all Action fires only if every rule matches (AND logic across rules)

Conditions within a single rule are always AND-combined regardless of match_mode.

njs Module

Before using Nginx ACL, install the njs module:

# Check status
GET /api/nginx-acl/njs/status

# Install
POST /api/nginx-acl/njs/install

Rulesets

List Rulesets

GET /api/nginx-acl/rulesets?website_id=42
Authorization: Bearer <token>

Response:

{
  "data": {
    "private": [
      {
        "id": "rs_1718012345678",
        "name": "Block Bad Bots",
        "enabled": true,
        "is_public": false,
        "match_mode": "any",
        "action_type": "deny",
        "action_value": "",
        "rule_count": 3
      }
    ],
    "public": []
  }
}

Create a Ruleset

POST /api/nginx-acl/rulesets
{
  "name": "Block Bad Bots",
  "is_public": false,
  "match_mode": "any",
  "action_type": "deny",
  "action_value": "",
  "website_id": 42
}
Field Type Description
name string Human-readable name
is_public bool If true, shared across all websites
match_mode string "any" (default) or "all"
action_type string "deny", "allow", or "redirect"
action_value string Redirect URL when action_type is "redirect"
website_id int Website the ruleset belongs to

Response:

{
  "data": {
    "id": "rs_1718012345678",
    "name": "Block Bad Bots",
    ...
  }
}

Update a Ruleset

PUT /api/nginx-acl/rulesets/:id?website_id=42
{
  "enabled": false,
  "match_mode": "all"
}

All fields are optional (partial update).

Delete a Ruleset

DELETE /api/nginx-acl/rulesets/:id?website_id=42

Reorder Rulesets

POST /api/nginx-acl/rulesets/reorder
{
  "website_id": 42,
  "order": ["rs_abc", "rs_def", "rs_ghi"]
}

Apply a Ruleset

Writes the ruleset configuration to the njs config file and reloads nginx:

POST /api/nginx-acl/rulesets/:id/apply?website_id=42

Warning

Changes to rulesets are not live until apply is called.

Rules

List Rules

GET /api/nginx-acl/rulesets/:id/rules?website_id=42

Response:

{
  "data": [
    {
      "id": "rule_1718012345678",
      "name": "Block known bad bots",
      "enabled": true,
      "sort_order": 0,
      "conditions": [
        {
          "type": "header",
          "field": "User-Agent",
          "operator": "contains",
          "value": "scrapy"
        }
      ],
      "action": { "type": "deny" }
    }
  ]
}

Create a Rule

POST /api/nginx-acl/rulesets/:id/rules
{
  "website_id": 42,
  "name": "Block scrapy bot",
  "enabled": true
}

Update a Rule

PUT /api/nginx-acl/rules/:id
{
  "website_id": 42,
  "enabled": false
}

Delete a Rule

DELETE /api/nginx-acl/rules/:id?website_id=42

Reorder Rules

POST /api/nginx-acl/rulesets/:id/rules/reorder
{
  "website_id": 42,
  "order": ["rule_abc", "rule_def"]
}

Conditions

Add a Condition to a Rule

POST /api/nginx-acl/rules/:id/conditions
{
  "website_id": 42,
  "type": "ip",
  "operator": "in",
  "value": "198.51.100.0/24"
}
Condition Type field Available Operators
ip - eq, ne, in (CIDR), ip_group
ip_group - in (references IP group by name)
header Header name (e.g. User-Agent) eq, ne, contains, startswith, regex
uri - eq, ne, contains, startswith, regex
method - eq, ne
country - eq, ne

Delete a Condition

DELETE /api/nginx-acl/conditions/:id?website_id=42

IP Groups

IP groups are named sets of IPs/CIDRs that can be referenced in conditions. They are stored globally (shared across all rulesets).

List IP Groups

GET /api/nginx-acl/ip-groups

Response:

{
  "data": {
    "office_ips": ["203.0.113.0/24", "198.51.100.5"],
    "bad_actors": ["192.0.2.1"]
  }
}

Create an IP Group

POST /api/nginx-acl/ip-groups
{ "name": "office_ips" }

Delete an IP Group

DELETE /api/nginx-acl/ip-groups/:name

List IP Entries in a Group

GET /api/nginx-acl/ip-groups/:name/entries

Add IP Entry

POST /api/nginx-acl/ip-groups/:name/entries
{ "ip": "203.0.113.0/24" }

Delete IP Entry

DELETE /api/nginx-acl/ip-entries/:ip

Per-Website ACL Assignment

Public rulesets can be assigned to specific websites without copying them. This allows central management of shared policies.

# List rulesets assigned to a website
GET /api/webapps/:id/acl

# List public rulesets available to assign
GET /api/webapps/:id/acl/available

# Assign a public ruleset to a website
POST /api/webapps/:id/acl/assign
{ "ruleset_id": "rs_1718012345678" }

# Unassign a ruleset from a website
DELETE /api/webapps/:id/acl/:ruleset_id

Data Storage

ACL config is stored per website in /etc/starkone/users_data/<username>/nginx_acl.json. Public rulesets are stored in /etc/starkone/nginx_acl_public.json.

{
  "rulesets": [
    {
      "id": "rs_abc",
      "name": "Block Bad Bots",
      "enabled": true,
      "is_public": false,
      "match_mode": "any",
      "action": { "type": "deny", "value": "" },
      "rules": [
        {
          "id": "rule_def",
          "name": "Scrapy bot",
          "enabled": true,
          "sort_order": 0,
          "conditions": [
            {
              "type": "header",
              "field": "User-Agent",
              "operator": "contains",
              "value": "scrapy"
            }
          ],
          "action": { "type": "deny" }
        }
      ]
    }
  ],
  "ip_groups": {
    "office_ips": ["203.0.113.0/24"]
  }
}

Example: Block a Country

# 1. Create a ruleset
POST /api/nginx-acl/rulesets
{
  "name": "Block CN",
  "is_public": false,
  "match_mode": "any",
  "action_type": "deny",
  "website_id": 42
}

# 2. Create a rule
POST /api/nginx-acl/rulesets/rs_xxx/rules
{ "website_id": 42, "name": "China IPs", "enabled": true }

# 3. Add condition
POST /api/nginx-acl/rules/rule_yyy/conditions
{
  "website_id": 42,
  "type": "country",
  "operator": "eq",
  "value": "CN"
}

# 4. Apply
POST /api/nginx-acl/rulesets/rs_xxx/apply?website_id=42

Example: Allow Only Office IPs

# 1. Create IP group
POST /api/nginx-acl/ip-groups
{ "name": "office" }

POST /api/nginx-acl/ip-groups/office/entries
{ "ip": "203.0.113.0/24" }

# 2. Create ruleset with action "allow" and match_mode "all"
POST /api/nginx-acl/rulesets
{
  "name": "Allow Office Only",
  "is_public": false,
  "match_mode": "all",
  "action_type": "allow",
  "website_id": 42
}

# 3. Create rule with ip_group condition
POST /api/nginx-acl/rulesets/rs_xxx/rules
{ "website_id": 42, "name": "Office IPs", "enabled": true }

POST /api/nginx-acl/rules/rule_yyy/conditions
{
  "website_id": 42,
  "type": "ip_group",
  "operator": "in",
  "value": "office"
}

# 4. Apply
POST /api/nginx-acl/rulesets/rs_xxx/apply?website_id=42