---
title: "Developing a Custom Kong ACL Plugin Tutorial"
description: "Follow this step-by-step guide to learn how to develop a Kong Custom ACL plugin, from basic concepts to a complete implementation, and build your own custom API permission management system."
canonical_url: "https://blog.markkulab.net/en/post/kong-custom-acl-plugin-development"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/en/author/mark-ku"
site: "Mark Ku's Tech Notes"
date_published: "2025-08-20 01:01:00 +0800"
category: "DevOps"
tags: ["kong", "api gateway", "custom plugin", "lua", "postgresql", "權限管理", "devops", "acl"]
language: "en"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "when reusing or quoting, credit the author and link back to the original"
---

# Developing a Custom Kong ACL Plugin Tutorial

## Introduction

Kong's built-in ACL feature can't fully meet real-world requirements in certain scenarios, so it often needs to be customized according to an enterprise's or project's authorization logic. This article uses a simple POC as an example to demonstrate how to develop a Kong Custom ACL plugin from scratch to implement custom API access control.

## 🎯 Learning Objectives

In this tutorial, you will learn how to:

- Understand the basic concepts of the Kong plugin system
- Design a simple and practical authorization management database
- Develop a custom ACL plugin and connect it to a database
- Deploy and test the entire flow using Docker

## 📋 Prerequisites

Before you begin, please ensure you have the following environment and knowledge:

- Docker and Docker Compose
- Basic concepts of Lua programming
- Basic operations of PostgreSQL databases and fundamental SQL

---

## 🚀 Step 1: Understand the Project Architecture

### Project Structure

```text
custom-acl/
├── handler.lua      # 主要邏輯處理
├── schema.lua       # 配置定義
├── api.lua          # 管理 API
├── init.sql         # 資料庫初始化
├── Dockerfile       # 容器化配置
└── docker-compose.yml
```

### System Flowchart

```text
用戶請求 → Kong Gateway → Custom ACL 插件 → 檢查權限 → 允許 / 拒絕
                ↓
            PostgreSQL 資料庫
```

---

## 🗄️ Step 2: Design the Database Schema

### Create the Authorization Management Table

```sql:custom-acl/init.sql
-- 建立客戶 API 權限表
CREATE TABLE IF NOT EXISTS customer_api_permissions (
    id SERIAL PRIMARY KEY,
    customer_id VARCHAR(100) NOT NULL,
    service_id VARCHAR(100),
    route_id VARCHAR(100),
    api_path VARCHAR(500) NOT NULL,
    method VARCHAR(10) DEFAULT 'GET',
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    created_by VARCHAR(100) DEFAULT 'system',
    updated_by VARCHAR(100) DEFAULT 'system'
);

-- 建立索引提升查詢效能
CREATE INDEX IF NOT EXISTS idx_customer_permissions_customer_id 
ON customer_api_permissions(customer_id);

CREATE INDEX IF NOT EXISTS idx_customer_permissions_api_path 
ON customer_api_permissions(api_path);

CREATE INDEX IF NOT EXISTS idx_customer_permissions_active 
ON customer_api_permissions(is_active);

-- 插入測試資料
INSERT INTO customer_api_permissions (customer_id, api_path, method, is_active) VALUES
('customer_001', '/api/users', 'GET', true),
('customer_001', '/api/orders', 'POST', true),
('customer_002', '/api/products', 'GET', true),
('customer_002', '/api/analytics', 'GET', false);
```

**Field Descriptions:**

- `customer_id`: Consumer identifier
- `api_path`: API path
- `method`: HTTP method
- `is_active`: Whether the permission is enabled
- The index is used to speed up common query conditions (consumer, path, enabled status).

---

## 🔧 Step 3: Develop the Plugin Handler

### Create `handler.lua`

The implementation practice demonstrated here is to directly use OpenResty's underlying components (e.g., `resty.postgres`) to handle database connections. This reduces coupling with Kong version changes, making the plugin relatively stable when upgrading Kong.

```lua:custom-acl/handler.lua
local kong_meta = require "kong.meta"
local cjson = require "cjson.safe"
local postgres = require "resty.postgres"

local kong = kong
local _M = {}

local CustomAclHandler = {
  PRIORITY = 1000,  -- 執行優先順序
  VERSION = "1.0.0",
}

-- 步驟 1：從 Kong 取得客戶 ID
local function extract_customer_id_from_keyauth()
  local consumer = kong.client.get_consumer()
  if consumer and consumer.custom_id then
    kong.log.debug("取得到客戶 ID: ", consumer.custom_id)
    return consumer.custom_id
  end
  return nil
end

-- 步驟 2：連接資料庫
local function get_postgres_connection()
  local config = {
    host = os.getenv("KONG_PG_HOST") or "kong-database",
    port = tonumber(os.getenv("KONG_PG_PORT")) or 5432,
    database = os.getenv("KONG_PG_DATABASE") or "kong",
    user = os.getenv("KONG_PG_USER") or "kong",
    password = os.getenv("KONG_PG_PASSWORD") or "kong"
  }

  local db, err = postgres:new()
  if not db then
    return nil, "無法建立資料庫連接: " .. tostring(err)
  end

  db:set_timeout(5000)
  
  local ok, err = db:connect(config.host, config.port, config.database, config.user, config.password)
  if not ok then
    return nil, "連接資料庫失敗: " .. tostring(err)
  end

  return db
end

-- 步驟 3：檢查權限
local function check_permissions(customer_id, api_path)
  local query = string.format([[
    SELECT id, api_path, is_active
    FROM customer_api_permissions
    WHERE customer_id = '%s' AND is_active = true
  ]], customer_id)

  kong.log.debug("檢查權限: ", customer_id, " -> ", api_path)

  local db, err = get_postgres_connection()
  if not db then
    return false, "資料庫連接失敗: " .. tostring(err)
  end

  local res, err = db:query(query)
  db:close()

  if not res or #res == 0 then
    return false, "無權限記錄"
  end

  -- 檢查路徑是否匹配
  for _, permission in ipairs(res) do
    if permission.is_active and permission.api_path then
      -- 完全匹配或路徑前綴匹配
      if permission.api_path == api_path or 
         string.sub(api_path, 1, string.len(permission.api_path)) == permission.api_path then
        kong.log.notice("✅ 權限通過: ", permission.id)
        return true, "權限匹配: " .. permission.id
      end
    end
  end

  return false, "無匹配權限"
end

-- 主要處理函數
function CustomAclHandler:access(conf)
  kong.log.notice("開始 ACL 權限檢查")

  -- 步驟 1：取得客戶 ID
  local customer_id = extract_customer_id_from_keyauth()
  if not customer_id then
    return kong.response.exit(403, {
      error = "需要客戶 ID",
      message = "請先通過身份驗證"
    })
  end

  -- 步驟 2：取得請求路徑
  local api_path = kong.request.get_path()
  kong.log.debug("檢查路徑: ", customer_id, " -> ", api_path)

  -- 步驟 3：檢查權限
  local allowed, reason = check_permissions(customer_id, api_path)

  if not allowed then
    kong.log.warn("❌ 權限拒絕: ", customer_id, " -> ", api_path)
    return kong.response.exit(conf.error_code or 403, {
      error = conf.error_message or "存取被拒絕",
      customer_id = customer_id,
      api_path = api_path,
      reason = reason
    })
  end

  -- 步驟 4：權限通過
  kong.log.notice("✅ 權限檢查通過: ", reason)
end

return CustomAclHandler
```

**Code Highlights:**

1. **Get Consumer ID**: Read `consumer.custom_id` from Kong's existing authentication plugins to use as the consumer identifier.
2. **Establish Database Connection**: Use `resty.postgres` to connect to PostgreSQL based on environment variable settings.
3. **Check Permissions**: Query the available API paths for the consumer according to `customer_id` and determine whether to allow the request through an "exact match" or "prefix match".
4. **Respond with Result**: If the permission is denied, return a 403 status with an error message; otherwise, allow the request to proceed.

---

## ⚙️ Step 4: Define the Plugin Schema

### Create `schema.lua`

```lua:custom-acl/schema.lua
local typedefs = require "kong.db.schema.typedefs"

return {
    name = "custom-acl",
    fields = {
        { protocols = typedefs.protocols_http },
        { config = {
            type = "record",
            fields = {
                -- 錯誤回應設定
                { error_code = { type = "number", default = 403 }},
                { error_message = { type = "string", default = "存取被拒絕" }},
                
                -- 日誌設定
                { enable_logging = { type = "boolean", default = true }},
                
                -- 除錯模式
                { debug = { type = "boolean", default = false }}
            }
        }}
    }
}
```

**Configuration Description:**

- `error_code`: The HTTP status code to return when permission is denied.
- `error_message`: The default error message for when permission is denied.
- `enable_logging`: Whether to output more detailed permission check logs.
- `debug`: Whether to enable debug mode (you can add more logs or debugging behavior as needed).

---

## 🌐 Step 5: Create the Management API

### Create `api.lua`

```lua:custom-acl/api.lua
local cjson = require "cjson"
local postgres = require "resty.postgres"

-- 資料庫連接函數
local function get_postgres_connection()
    local config = {
        host = os.getenv("KONG_PG_HOST") or "kong-database",
        port = tonumber(os.getenv("KONG_PG_PORT")) or 5432,
        database = os.getenv("KONG_PG_DATABASE") or "kong",
        user = os.getenv("KONG_PG_USER") or "kong",
        password = os.getenv("KONG_PG_PASSWORD") or "kong"
    }

    local db, err = postgres:new()
    if not db then
        return nil, err
    end
    
    db:set_timeout(5000)
    
    local ok, err = db:connect(config.host, config.port, config.database, config.user, config.password)
    if not ok then
        return nil, err
    end
    
    return db
end

-- 執行查詢
local function execute_query(query, params)
    local db, err = get_postgres_connection()
    if not db then
        return nil, err
    end
    
    local res, err = db:query(query, params)
    db:close()
    
    if not res then
        return nil, err
    end
    
    return res
end

return {
  -- 查詢權限列表
  ["/custom-acl/permissions"] = {
    GET = function(self, dao_factory, helpers)
      local customer_id = kong.request.get_query()["customer_id"]
      
      if not customer_id then
        return kong.response.exit(400, { error = "需要提供 customer_id 參數" })
      end
      
      local query = [[
        SELECT id, customer_id, api_path, method, is_active, created_at
        FROM customer_api_permissions 
        WHERE customer_id = $1 AND is_active = true
        ORDER BY created_at DESC
      ]]
      
      local res, err = execute_query(query, {customer_id})
      if not res then
        return kong.response.exit(500, { 
          error = "資料庫查詢失敗",
          details = tostring(err)
        })
      end
      
      return kong.response.exit(200, {
        customer_id = customer_id,
        permissions = res,
        total = #res
      })
    end,
    
    -- 新增權限
    POST = function(self, dao_factory, helpers)
      local body = kong.request.get_body()
      
      if not body.customer_id then
        return kong.response.exit(400, { error = "需要提供 customer_id" })
      end
      
      if not body.api_path then
        return kong.response.exit(400, { error = "需要提供 api_path" })
      end
      
      local insert_query = [[
        INSERT INTO customer_api_permissions 
        (customer_id, api_path, method, is_active, created_by)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id, customer_id, api_path, method, is_active, created_at
      ]]
      
      local res, err = execute_query(insert_query, {
        body.customer_id,
        body.api_path,
        body.method or 'GET',
        body.is_active ~= false,
        body.created_by or 'system'
      })
      
      if not res then
        return kong.response.exit(500, { 
          error = "建立權限失敗",
          details = tostring(err)
        })
      end
      
      return kong.response.exit(201, {
        message = "權限建立成功",
        permission = res[1]
      })
    end
  },
  
  -- 測試 API
  ["/custom-acl/test"] = {
    GET = function(self, dao_factory, helpers)
      return kong.response.exit(200, {
        message = "Custom ACL 插件運作正常！",
        timestamp = ngx.time(),
        version = "1.0.0"
      })
    end
  }
}
```

**API Functionality:**

- `GET /custom-acl/permissions`: Query the list of enabled permissions for a specified `customer_id`.
- `POST /custom-acl/permissions`: Add a new consumer API permission setting.
- `GET /custom-acl/test`: Quickly check if the Custom ACL plugin is enabled and responsive.

---

## 🐳 Step 6: Containerize the Deployment

### Create `Dockerfile`

```dockerfile:custom-acl/Dockerfile
# 使用官方 Kong 映像
FROM kong:3.4

# 安裝必要工具
USER root
RUN apk add --no-cache postgresql-client

# 複製插件檔案
COPY custom-acl /usr/local/share/lua/5.1/kong/plugins/custom-acl
RUN chown -R kong:kong /usr/local/share/lua/5.1/kong/plugins/custom-acl

# 將插件加入 Kong 插件列表
RUN sed -i '/local plugins *= *{/a\    "custom-acl",' /usr/local/share/lua/5.1/kong/constants.lua

# 設定環境變數
ENV KONG_PLUGINS=bundled,custom-acl

# 切換回 kong 用戶
USER kong

# 啟動命令
CMD ["kong", "docker-start"]
```

### Create `docker-compose.yml`

```yaml:custom-acl/docker-compose.yml
version: '3.8'

services:
  # PostgreSQL 資料庫
  kong-database:
    image: postgres:15-alpine
    container_name: kong-database
    environment:
      POSTGRES_USER: kong
      POSTGRES_DB: kong
      POSTGRES_PASSWORD: kong
    volumes:
      - kong-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U kong"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - kong-net

  # Kong API Gateway
  kong:
    build: .
    container_name: kong-gateway
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: kong-database
      KONG_PG_PORT: 5432
      KONG_PG_DATABASE: kong
      KONG_PG_USER: kong
      KONG_PG_PASSWORD: kong
      KONG_PLUGINS: bundled,custom-acl
      KONG_ADMIN_LISTEN: 0.0.0.0:8001
      KONG_PROXY_LISTEN: 0.0.0.0:8000
    ports:
      - "8000:8000"  # Proxy
      - "8001:8001"  # Admin API
    depends_on:
      kong-database:
        condition: service_healthy
    networks:
      - kong-net

volumes:
  kong-data:

networks:
  kong-net:
    driver: bridge
```

---

## 🚀 Step 7: Deploy and Test

### 1. Start the Services

```bash
# 建立並啟動所有服務
docker-compose up --build

# 檢查服務狀態
docker-compose ps
```

### 2. Check the Database

```bash
# 連接到資料庫容器
docker exec -it kong-database psql -U kong -d kong

# 檢查表是否建立
\dt customer_api_permissions

# 查看測試資料
SELECT * FROM customer_api_permissions;
```

### 3. Test the Plugin

```bash
# 測試插件是否正常運作
curl http://localhost:8001/custom-acl/test

# 查詢權限列表
curl "http://localhost:8001/custom-acl/permissions?customer_id=customer_001"

# 新增權限
curl -X POST http://localhost:8001/custom-acl/permissions \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "customer_003",
    "api_path": "/api/test",
    "method": "GET",
    "is_active": true
  }'
```

---

## 🔍 Step 8: Common Troubleshooting

### Problem 1: Plugin Fails to Load

**Symptom:** A plugin loading error appears when Kong starts.

**Troubleshooting Steps:**

```bash
# 檢查插件檔案與權限
docker exec -it kong-gateway ls -la /usr/local/share/lua/5.1/kong/plugins/custom-acl

# 檢查 Kong 容器日誌
docker-compose logs kong
```

---

### Problem 2: Database Connection Fails

**Symptom:** The plugin cannot connect to PostgreSQL.

**Troubleshooting Steps:**

```bash
# 檢查資料庫服務狀態
docker-compose ps kong-database

# 檢查 Kong 容器中的資料庫相關環境變數
docker exec -it kong-gateway env | grep KONG_PG
```

---

### Problem 3: Permission Check Results Are Not as Expected

**Symptom:** A request that should be permitted is denied, or a request that should be denied is allowed.

**Troubleshooting Steps:**

```bash
# 檢查資料庫中的權限資料是否設定正確
docker exec -it kong-database psql -U kong -d kong -c "SELECT * FROM customer_api_permissions WHERE customer_id = 'your_customer_id';"

# 檢視 Kong 日誌中的權限檢查流程
docker-compose logs kong | grep "權限檢查"
```

---

## 🎉 Congratulations!

At this point, you have completed a working Kong Custom ACL plugin and have gone through the following key steps:

- ✅ Planned and created the authorization management database schema
- ✅ Developed a custom Kong plugin using Lua
- ✅ Completed containerized deployment using Docker / Docker Compose
- ✅ Performed testing and validation using the management API and actual requests

This POC can serve as a foundation for future extensions, such as:

- More granular permission dimensions (by Service / Route / Method)
- A backend management interface (integrated with a CMS or backend system)
- Auditing and logging for permission changes

### Related Resources

- [Kong Official Documentation](https://docs.konghq.com/)
- [Programming in Lua Guide](https://www.lua.org/pil/)
- [PostgreSQL Official Documentation](https://www.postgresql.org/docs/)

---

## About this article and its author

Originally published on [Mark Ku's Tech Notes](https://blog.markkulab.net/en/post/kong-custom-acl-plugin-development)

License: [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — when reusing or quoting, credit the author and link back to the original

### About the author

**[Mark Ku](https://blog.markkulab.net/en/author/mark-ku)** — Software Solution Provider

- 10+ years senior software engineer, now an AI Builder
- Focused on large-platform architecture — North-American e-commerce, AI SaaS subscription billing
- Combining AI Agents and automation to build evolvable product foundations

### Free tools built by the author

All of these are free to use:

- [Free PDF Sign Tool](https://blog.markkulab.net/en/tools/pdf-sign): Online PDF sign tool — draw, type, or upload a signature, then drag, resize, and download. Everything runs in your browser; nothing is uploaded.
- [VS Code Refactory](https://blog.markkulab.net/en/tools/refactory): Refactory is a VS Code refactoring extension: 34 actions plus a 37-rule code-smell inspection layer with a Code Health dashboard, across 18 languages, backed by 534 tests. It learns your repo's conventions: where interfaces live, where DI is registered, whether 'use client' belongs. It ranks files by git churn × complexity so you know what to fix first, and hands any smell to the Claude Code already on your machine. Free to use, and your source never leaves your computer.
- [DB-Kit Database Manager](https://blog.markkulab.net/en/tools/db-kit): DB-Kit is a lightweight, cross-platform database manager built with Tauri + Rust + React. Manage MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, MongoDB, Redis, Kafka, Elasticsearch and RabbitMQ from one consistent interface: passwords encrypted in the OS keychain, SSH tunnels, full CRUD, a visual query builder, stacked multi-statement result sets, cross-connection data transfer and compare/sync, Excel / CSV import & export, visualized execution plans, ER diagrams, scheduled backups, SQL stress testing with p50–p99 latency percentiles, a 15-rule SQL review engine, Kafka message browsing with monitoring & alerts, a bilingual UI (Traditional Chinese / English), a built-in AI assistant (natural-language SQL, AI review and tuning advice) and the dbk CLI. Free and open source (MIT), with installers for Windows, macOS and Linux.
- [VS Code Super Mermaid](https://blog.markkulab.net/en/tools/super-mermaid): Super Mermaid is a VS Code extension for beautiful Mermaid diagrams out of the box: auto-colored live preview, mouse pan & zoom, high-res PNG / SVG export, 21 templates and multiple themes. Free and open source (MIT).
- [React Super Mermaid](https://blog.markkulab.net/en/tools/react-super-mermaid): react-super-mermaid is an open-source React component library: render beautiful Mermaid diagrams with a single <MermaidViewer>, with built-in colorful / sketch themes, pan & zoom, in-diagram search, and high-res SVG / PNG export. Lightweight, SSR-safe, fully typed. Free and open source (MIT).
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/en/tools/jira-super-mermaid): An Atlassian Forge app: write Mermaid syntax directly inside a Jira issue or a Confluence page and get flowcharts, sequence diagrams, state machines and Gantt charts. 11 diagram types, SVG / PNG export, light and dark themes, full CJK support. Runs on Atlassian: your diagrams live in your own site and the app calls no third-party service. Free, coming soon to the Atlassian Marketplace.
- [Mermaid Live Preview](https://blog.markkulab.net/en/tools/mermaid-preview): Write Mermaid in your browser, see it render instantly, and share the whole diagram as a single link. No sign-up, nothing uploaded to a server, and mermaid.live share links work as-is.
- [React Intl Phone Number](https://blog.markkulab.net/en/tools/react-intl-phone-number): react-intl-phone-number is an open-source React component: framework-agnostic and antd-free, with E.164 in/out, a searchable flag / country-code dropdown, configurable validation levels (strict / mobile-strict / loose), themeable CSS, and i18n — phone logic powered by google-libphonenumber. Lightweight and fully typed. Free and open source (MIT).
- [Uptime Kuma Cluster](https://blog.markkulab.net/en/tools/uptime-kuma-cluster): Turn single-node Uptime Kuma into a highly available cluster: OpenResty + Lua smart load balancing, shared MariaDB state, health checks and automatic failover, plus cluster-management REST APIs. One Docker Compose command to start. Free and open source (MIT).
- [Special Education](https://blog.markkulab.net/en/education): Learning materials crafted for special education students

### Daily podcasts

- [Mark's Tech Insights — Daily AI News](https://blog.markkulab.net/en/category/tech-news): Daily curated AI and tech trends. Catch the latest developments via audio summaries — covering AI applications, software architecture, DevOps, and engineering practice. — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/en/category/ai-stock-chat): Every trading day, an AI-analyzed take on the Taiwan stock market, delivered as a two-host conversation covering the session and the next-day outlook. — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml
- [開源好物週報](https://blog.markkulab.net/en/category/open-source-weekly): A weekly two-host pick of free open-source tools surfaced from real Hacker News, GitHub, and Reddit buzz — what pain they solve and the fastest way to get started. — RSS: https://blog.markkulab.net/open-source-weekly/feed.xml

### Newsletter

[Subscribe to the newsletter](https://blog.markkulab.net/en/subscribe) — Be the first to know about new posts. No spam, unsubscribe anytime.
