Day 4: nginx-gate
Request gating and access control for OpenResty, configured in YAML
Date: March 4, 2026 Repo: venkatesh3007/nginx-gate Status: ✅ Complete — OpenResty plugin + Docker setup
The Problem
Every web application needs a gatekeeper. Block bots. Restrict admin panels to office IPs. Require authentication headers on API routes. Redirect old URLs to new ones. Protect sensitive files.
The standard approach is to scatter this logic across your application code. An if statement here, a middleware there, a .htaccess file somewhere else. The rules live in three different languages across five different files. When something goes wrong — and it always does — nobody knows which rule is blocking the request.
The cloud approach is to use AWS WAF or Cloudflare Rules. That works until you need custom logic, or you’re running on a VPS, or you don’t want your access control policy locked inside a vendor’s dashboard.
What I wanted: a single YAML file that defines every access rule. Human-readable. Version-controlled. Hot-reloadable without restarting nginx.
What We Built
nginx-gate is an OpenResty/Lua plugin. You define rules in YAML, and it enforces them at the nginx level — before your application code ever sees the request.
rules:
- name: "admin_ip_whitelist"
match:
path:
pattern: "/admin"
type: "prefix"
ip:
allow_list:
- "10.0.0.0/8"
- "192.168.1.100"
action:
type: "block"
status_code: 403
body: "Access denied"
- name: "block_bots"
match:
headers:
- name: "user-agent"
operator: "contains"
value: "bot"
action:
type: "block"
status_code: 403
- name: "require_api_auth"
match:
path:
pattern: "/api/v2/*"
type: "wildcard"
headers:
- name: "authorization"
operator: "exists"
negate: true
action:
type: "block"
status_code: 401
body: "Authentication required"Three rules. Admin panel locked to internal IPs. Bots blocked. API requires auth headers. All in one file that anyone on the team can read and modify.
Rule matching
The engine supports four path matching modes:
- exact —
/healthmatches only/health - prefix —
/adminmatches/admin,/admin/users,/admin/settings - wildcard —
/api/v2/*matches any path under/api/v2/ - regex — Lua patterns for complex matching
Rules can match on path, headers, query parameters, and IP addresses. All conditions in a rule must match (AND logic). First matching rule wins. If no rules match, the request passes through.
Actions
Block — return a status code and body. Done.
Redirect — 301 or 302 to a target URL. For URL migrations.
Pass — let the request through, but add or remove headers. Useful for injecting X-Request-ID or stripping X-Powered-By.
Hot reload
Edit the YAML file. The configuration reloads automatically every 5 seconds. No nginx restart. No downtime. Change an IP whitelist at 2 AM without touching the server process.
You can also trigger a reload via API:
curl -X POST http://localhost:8080/nginx-gate/reloadStructured logging
Every rule match is logged as JSON:
{
"timestamp": 1709568000.123,
"rule": "admin_ip_whitelist",
"action": "block",
"request": {
"uri": "/admin/dashboard",
"method": "GET",
"client_ip": "1.2.3.4",
"user_agent": "Mozilla/5.0..."
}
}Pipe it to any log aggregator. Grep for blocked requests. Build dashboards. Debug access issues by searching for a specific IP or path.
The Build
743 lines total:
lua/nginx_gate.lua(341 lines) — The core engine. Rule loading, YAML parsing, request matching, action execution, IP/CIDR matching, configuration caching.config/rules.yaml(148 lines) — Example configuration with 12 rules covering common patterns.tests/test_nginx_gate.sh(141 lines) — Integration tests: path matching, header matching, IP filtering, all action types, config reload.tests/verify_setup.sh(113 lines) — Setup verification script.
Plus Docker Compose for one-command deployment:
docker-compose up -d
curl http://localhost:8000/healthWhy Lua at the nginx Layer
This could have been a Node.js middleware. Or a Go reverse proxy. Or a Python WSGI filter. I built it as an OpenResty/Lua plugin for one reason: performance.
nginx processes requests before they reach your application server. A Lua script running in nginx’s event loop adds microseconds of overhead, not milliseconds. No extra network hop. No separate process. No serialization. The gating decision happens in the same process that handles the TCP connection.
For a high-traffic API, this matters. For a $24/month VPS running multiple services, it matters even more — you can’t afford to waste cycles on a separate proxy layer.
OpenResty is nginx with Lua scripting built in. It’s battle-tested at scale (used by Cloudflare, Kong, and Mashape). The Lua ecosystem is tiny but focused. lua-cjson for JSON. lua-resty-yaml for YAML. That’s all you need.
The Factory Pattern
Day 4 reinforced something: the factory produces infrastructure, not just applications.
Days 1-3 built user-facing products — a UI SDK, an API tool, a mock server. Day 4 built plumbing. Nobody tweets about request gating. Nobody writes a blog post about YAML-configured access control. But every application we build for the rest of this sprint will run behind nginx-gate.
The factory doesn’t just produce products. It produces the infrastructure that makes future products safer and faster to deploy.
What I Learned
YAML is the right format for rules. Not JSON (too noisy). Not a DSL (too clever). Not a database (too heavy). YAML is readable, diffable, and version-controllable. When the security team asks “what are our access rules?”, you show them a file. They can read it. They can propose changes in a pull request. The rules are code-reviewed like any other code.
Hot reload changes the operations model. Without hot reload, changing an access rule means: edit config → test → deploy → restart nginx → hope nothing breaks. With hot reload: edit the YAML, wait 5 seconds, done. This makes security response fast. Bot attack at 2 AM? Add a rule, save the file, it’s blocked in 5 seconds.
Infrastructure products are undervalued in sprints. Nobody counts “I set up request gating” as a product. But it prevents the class of problems that would slow down every future product. Day 4’s nginx-gate will save hours across the remaining 27 days.