Nginx OpenRestyとRedisを活用した効率的なIPアクセス制御システムの構築

1. はじめに

現代のデジタル環境において、ネットワークセキュリティは企業および個人ユーザーにとって重要な関心事です。IPベースのアクセス制御リストは、Webリソースへのアクセス権限を精密に管理するための効果的な手段です。許可リストを使用することで、信頼できるIPアドレスのみが機密リソースにアクセスできるように確保できます。一方、拒否リストは悪意のあるIPからのアクセスをブロックし、セキュリティリスクを軽減します。

Nginx OpenRestyとRedisをアクセス制御の実装プラットフォームとして選択する理由は以下の通りです:

  • 高性能性:Nginxは軽量で高性能なことで知られ、高並列リクエスト処理に適しています。
  • 柔軟性:OpenRestyはLuaスクリプト統合により、強力なカスタマイズ機能を提供します。
  • 拡張性:Redisはインメモリデータ構造ストアとして、高速な読み書きをサポートし、動的アクセス制御リスト管理に最適です。

2. 概要

OpenRestyとは何か

OpenRestyはNginxベースの完全機能Webプラットフォームであり、精選されたLuaライブラリ群、サードパーティモジュール、LuaJITベースの軽量Webフレームワークを統合しています。OpenRestyの核となるのはNginxですが、Lua言語によりその機能を拡張し、超高並列処理可能な動的Webアプリケーションの構築を可能にします。

OpenRestyとNginxの関係

OpenRestyはNginx上に構築され、Nginxのすべての機能を保持しつつ、Lua言語による能力拡張を実現しています。これにより、OpenRestyを使用してNginxの全機能を実行しつつ、Luaスクリプトによる複雑なビジネスロジックの実装も可能になります。

3. 環境構築

使用バージョン

CentOS 7 
Redis 7.2
Nginx version: openresty/1.25.3.1

インストール手順

1. OpenRestyリポジトリの追加

# OpenRestyはパブリックリポジトリに存在しないため、専用リポジトリを追加
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

# 上記コマンドが存在しない場合、まずインストール
yum install -y yum-utils

2. OpenRestyのインストール

# OpenRestyのインストール
yum install -y openresty
# サードパーティLuaモジュールをインストールする管理ツール
yum install -y openresty-opm

3. ディレクトリ構造
​ デフォルトで/usr/local/openrestyにインストールされます
​ nginxディレクトリ内には通常のnginxと同様の構造が確認でき、OpenRestyはNginxにいくつかのLuaモジュールを統合したものです

4. 起動と実行
OpenRestyはNginxベースで構築されており、OpenRestyディレクトリ内のnginxディレクトリ構造はWindows版nginxと基本的に同じです

5. Redisの設定

sudo yum install redis -y

Redisの設定には永続化オプション、ネットワーク構成、セキュリティ設定などが含まれます。以下は基本的な設定例です:

# redis.conf
port 6379
bind 127.0.0.1
protected-mode yes
requirepass "yourpassword"

4. 許可リストの実装

アーキテクチャ図解:

  • クライアント:HTTPリクエストを送信するユーザーまたはアプリケーション
  • Nginx (OpenResty):HTTPリクエストの処理と管理、Luaスクリプトによるアクセス制御実行
  • Luaスクリプト:Nginx内で実行され、Redisから許可リストを取得してIPチェックを行う
  • Redis:許可リストデータを格納し、高速検索を提供

インタラクションフロー:

  1. クライアントがNginxにリクエストを送信
  2. NginxがLuaスクリプトを呼び出してアクセス制御を実行
  3. LuaスクリプトがRedisに接続し、IPが許可リストに含まれているかを照会
  4. Luaスクリプトが結果をNginxに返却
  5. Nginxが結果に基づいてリクエストを許可または拒否し、クライアントに応答を返す

許可リストの役割と重要性

許可リストは信頼済みIPアドレスやエンティティのグループを定義するセキュリティ戦略であり、特定のリソースやサービスへのアクセスを許可します。Webアプリケーションでは特に重要な役割を果たします:

  • セキュリティ強化:アクセス権限を制限し、特定IPアドレスのみが機密リソースにアクセス可能に
  • 悪用防止:悪意あるユーザーやクローラーによるサービス悪用を減少
  • トラフィック管理:アクセス元を制御することで、ネットワークトラフィックを効果的に管理

OpenResty Luaスクリプトによる許可リストロジック実装

OpenRestyではLuaスクリプトを使用して許可リストロジックを実装できます。LuaスクリプトはNginx設定ファイル内で直接記述するか、外部ファイルに保存して設定ファイルから参照できます。

Luaスクリプト実装手順:

  1. 許可リスト定義:Redisに許可リストIPアドレスを格納
  2. アクセス制御:Nginx設定でaccess_by_lua_blockまたはaccess_by_lua_fileディレクティブを使用してLuaスクリプトを呼び出し
  3. スクリプトロジック:リクエストIPアドレスが許可リストに含まれているかをチェックし、含まれていない場合はアクセスを拒否

実装例

jenkins.ownit.top.conf
これはnginxの設定ファイル
主な内容:access_by_lua_file /opt/nginx/lua_script/white.lua;

upstream jenkins-uat {
        server 192.168.102.20:91;
}
server {
        listen       80;
        server_name  jenkins.ownit.top;
        # 許可リストまたは拒否リスト
        #include /opt/nginx/whitelist/corporation.conf;

        location / {
            rewrite ^/(.*)$ https://$host/$1 permanent;
        }

    access_log  /www/wwwlogs/dns.ownit.top.log;
    error_log  /www/wwwlogs/dns.ownit.top.error.log;

}
server {
        listen       443 ssl;
        server_name  jenkins.ownit.top;
         # 許可リストまたは拒否リスト
        #include /opt/nginx/whitelist/corporation.conf;

        #ssl                   on;
        ssl_certificate      /opt/nginx/ssl/ownit.top.crt;
        ssl_certificate_key  /opt/nginx/ssl/ownit.top.key;
        include ssl.conf;

        location / {
            access_by_lua_file /opt/nginx/lua_script/white.lua; # nginxのluaスクリプト
            proxy_pass  http://jenkins-uat;
            include https_proxy.conf;
        }
    access_log  /www/wwwlogs/dns.ownit.top.log;
    error_log  /www/wwwlogs/dns.ownit.top.error.log;

}

white.luaファイル

Luaスクリプトを使用して、HTTPリクエスト受信時にリクエストIPアドレスがRedisに格納された許可リストに含まれているかをチェックします。IPが許可リストにない場合はアクセスを拒否します。

実装方針

  1. クライアントIPとリクエストパスの取得:LuaスクリプトでクライアントIPアドレスとリクエストパスを取得
  2. Redis接続resty.redisモジュールを使用してRedisデータベースに接続
  3. 認証処理:接続したRedisに対して認証を実行
  4. IP許可リストチェック:RedisからIPが許可リストセットに存在するかをチェック
  5. 結果返却:IPが許可リストに含まれていればアクセスを許可、そうでなければ403 Forbiddenステータスコードを返却してアクセスを拒否
  6. Redis接続解放:Redis接続を接続プールに戻して再利用可能に
-- クライアントIPとリクエストパスの取得
local visitor_ip = ngx.var.remote_addr
local request_path = ngx.var.uri

-- Redis関連設定
local redis_key_allow_list = "map_request_allow_list"

-- Redis接続
local redis_lib = require "resty.redis"
local redis_conn = redis_lib:new()
redis_conn:set_timeout(1000) -- タイムアウト設定(ミリ秒)
local connection_ok, connection_error = redis_conn:connect("127.0.0.1", 6379)
if not connection_ok then
    ngx.log(ngx.ERR, "Redis接続失敗: ", connection_error)
    return ngx.exit(500)
end

-- 認証処理
local auth_result, auth_error = redis_conn:auth("123456")
if not auth_result then
    ngx.say("認証失敗: ", auth_error)
    return
end

-- IP許可リストチェック
local exists_in_allowlist, check_error = redis_conn:sismember(redis_key_allow_list, visitor_ip)
if exists_in_allowlist == 1 then
    ngx.log(ngx.INFO, "IPが許可リストに存在: ", visitor_ip)
else
    ngx.status = ngx.HTTP_FORBIDDEN
    ngx.say("アクセス拒否")
    return ngx.exit(ngx.HTTP_FORBIDDEN)
end

-- Redis接続プールに戻す
local keepalive_ok, keepalive_error = redis_conn:set_keepalive(10000, 100)
if not keepalive_ok then
    ngx.log(ngx.ERR, "keepalive設定失敗: ", keepalive_error)
end

詳細説明

  1. クライアントIPとリクエストパスの取得
local visitor_ip = ngx.var.remote_addr
local request_path = ngx.var.uri

これらのコード行でNginx変数からクライアントIPアドレスとリクエストパスを取得し、visitor_ipを後の許可リストチェックに使用します。

  1. Redis関連設定
local redis_key_allow_list = "map_request_allow_list"

許可リストを格納するRedisキーを定義します。

  1. Redis接続
local redis_lib = require "resty.redis"
local redis_conn = redis_lib:new()
redis_conn:set_timeout(1000) -- タイムアウト設定(ミリ秒)
local connection_ok, connection_error = redis_conn:connect("127.0.0.1", 6379)
if not connection_ok then
    ngx.log(ngx.ERR, "Redis接続失敗: ", connection_error)
    return ngx.exit(500)
end

resty.redisモジュールを使用してRedis接続オブジェクトを作成し、接続タイムアウト時間を設定します。Redisサーバーへの接続を試み、接続失敗時はエラーログを記録して500エラーを返却します。

  1. 認証処理
local auth_result, auth_error = redis_conn:auth("123456")
if not auth_result then
    ngx.say("認証失敗: ", auth_error)
    return
end

Redisに対する認証を実行し、認証失敗時はエラーメッセージを出力して実行を停止します。

  1. IP許可リストチェック
local exists_in_allowlist, check_error = redis_conn:sismember(redis_key_allow_list, visitor_ip)
if exists_in_allowlist == 1 then
    ngx.log(ngx.INFO, "IPが許可リストに存在: ", visitor_ip)
else
    ngx.status = ngx.HTTP_FORBIDDEN
    ngx.say("アクセス拒否")
    return ngx.exit(ngx.HTTP_FORBIDDEN)
end

SISMEMBERコマンドを使用してIPがRedisの許可リストセットに含まれているかをチェックします。IPが許可リストに含まれている場合は情報ログを記録し、含まれていない場合は403 Forbiddenステータスコードを返却してアクセスを拒否します。

  1. Redis接続解放
local keepalive_ok, keepalive_error = redis_conn:set_keepalive(10000, 100)
if not keepalive_ok then
    ngx.log(ngx.ERR, "keepalive設定失敗: ", keepalive_error)
end

Redis接続を接続プールに戻して、後続のリクエストでこの接続を再利用可能にします。設定失敗時はエラーログを記録します。

NginxでのLuaスクリプト設定

Nginxのlocation設定で、access_by_lua_fileディレクティブを使用して上記Luaスクリプトを呼び出します:

http {
    server {
        listen 80;
        server_name example.com;

        location / {
            access_by_lua_file /opt/nginx/lua_script/white.lua;
            proxy_pass http://backend;
        }
    }
}

上記設定により、HTTPリクエスト受信時にまず/opt/nginx/lua_script/white.luaスクリプトが実行され、許可リストチェックが行われます。チェック通過後、リクエストはバックエンドサーバーに転送されます。

5. 拒否リストの実装

拒否リストの役割と利用シーン

拒否リストは、悪意あるIPアドレスによるサーバーリソースアクセスを識別・阻止するネットワークセキュリティメカニズムです。複数のシナリオで重要な役割を果たします:

  • 悪意ある攻撃防止:既知の攻撃者IPをブロックすることで、サーバーが受ける悪意ある攻撃を減少
  • クローラー悪用対策:クローラーによるウェブリソースの過剰アクセスを制限し、データの悪用から保護
  • サーバーロード軽減:特定IPのアクセス頻度を制限することで、サーバー負荷を軽減しサービス安定性を向上
  • DDoS防御:DDoS攻撃に迅速に対応し、攻撃元IPをブロックしてサービス可用性を保護

LuaスクリプトとRedisによる動的IPブロッキング実装

OpenRestyとRedisを組み合わせることで、効率的な動的IPブロッキングシステムを実装できます。以下の実装キーステップがあります:

  1. 環境設定:Nginx OpenRestyとRedis環境の準備完了
  2. Luaスクリプト作成:Redis内の拒否リスト状態を動的に照会・更新するLuaスクリプトの作成
  3. Nginx設定統合:Nginx設定ファイルにLuaスクリプトを統合し、アクセス制御を実現

実装例

Luaスクリプトを使用して、HTTPリクエスト受信時にリクエストIPアドレスが拒否リストに含まれているかをチェックし、IPのアクセス頻度を制御して適切に処理を行います。

実装方針

  1. クライアントIP取得:LuaスクリプトでクライアントIPアドレスを取得
  2. Redis接続resty.redisモジュールを使用してRedisデータベースに接続
  3. 認証処理:接続したRedisに対して認証を実行
  4. IP拒否リストチェック:RedisからIPが拒否リストセットに存在するかをチェック
  5. アクセス頻度制御:各IPのアクセス頻度を制限し、指定回数を超えた場合はIPを拒否リストに追加
  6. 結果返却:IPが拒否リストに含まれている場合は403 Forbiddenステータスコードを返却してアクセスを拒否、それ以外はリクエスト継続を許可

Nginx設定ファイル

upstream jms-uat {
    server 192.168.82.105:81;
}
server {
    listen 80;
    server_name jms.ownit.top;
    # 許可リストまたは拒否リスト
    # include /opt/nginx/whitelist/corporation.conf;
    rewrite ^/(.*)$ https://$host/$1 permanent;
    access_log /www/wwwlogs/dns.ownit.top.log;
    error_log /www/wwwlogs/dns.ownit.top.error.log;
}
server {
    listen 443 ssl;
    server_name jms.ownit.top;
    # 許可リストまたは拒否リスト
    # include /opt/nginx/whitelist/corporation.conf;
    ssl_certificate /opt/nginx/ssl/ownit.top.crt;
    ssl_certificate_key /opt/nginx/ssl/ownit.top.key;
    include ssl.conf;
    
    location = /core/auth/login/ {
        access_by_lua_file /opt/nginx/lua_script/login.lua;
        proxy_pass http://jms-uat;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location / {
        if ($request_uri !~ \.(html|htm|jpg|png|ico|js|css)$) {
            access_by_lua_file /opt/nginx/lua_script/rule.lua;
        }
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://jms-uat;
        include https_proxy.conf;
        client_max_body_size 0;
    }
    access_log /www/wwwlogs/dns.ownit.top.log;
    error_log /www/wwwlogs/dns.ownit.top.error.log;
}

location = /core/auth/login/

location = /core/auth/login/ {
    access_by_lua_file /opt/nginx/lua_script/login.lua;
    proxy_pass http://jms-uat;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}
説明
  • location = /core/auth/login/
    • これは完全一致のlocationブロックで、リクエストパスが/core/auth/login/と厳密に一致するリクエストのみに適用されます。
  • access_by_lua_file /opt/nginx/lua_script/login.lua;
    • OpenRestyのLuaモジュールを使用し、アクセス制御段階で/opt/nginx/lua_script/login.luaスクリプトを実行します。このスクリプトは通常、認証、権限チェック、その他の前処理ロジックを実行するために使用されます。
  • proxy_pass http://jms-uat;
    • 一致したリクエストをアップストリームサーバーhttp://jms-uatにプロキシします。
  • proxy_set_header Upgrade $http_upgrade;
    • Upgradeリクエストヘッダーを設定し、WebSocketなどのプロトコルアップグレードをサポートします。$http_upgrade変数には元のリクエストのUpgradeヘッダーフィールド値が含まれます。
  • proxy_set_header Connection "upgrade";
    • Connectionリクエストヘッダーをupgradeに設定し、通常Upgradeヘッダーと共に使用され、接続アップグレードを確実にします。

location /

location / {
    # このlocation下に静的リソースファイルが存在する場合、条件判定を行う
    if ($request_uri !~ \.(html|htm|jpg|png|ico|js|css)$) {
        access_by_lua_file /opt/nginx/lua_script/rule.lua; # この設定により、rule.luaのルールに基づいてレート制限を実施
    }
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://jms-uat;
    include https_proxy.conf;
    client_max_body_size 0;
}
説明
  • location /
    • これはワイルドカードマッチのlocationブロックで、すべてのパスのリクエストがこのブロックにマッチします(より正確なマッチブロックがない場合)。
  • if ($request_uri !~ \.(html|htm|jpg|png|ico|js|css)$)
    • ifディレクティブを使用してリクエストパスをチェックします。リクエストパスが指定された静的リソースファイル拡張子(.html, .htm, .jpg, .png, .ico, .js, .css)にマッチしない場合にのみ、後続のLuaスクリプトを実行します。このチェックにより、動的リソースと静的リソースを区別できます。
  • access_by_lua_file /opt/nginx/lua_script/rule.lua;
    • OpenRestyのLuaモジュールを使用し、アクセス制御段階で/opt/nginx/lua_script/rule.luaスクリプトを実行します。このスクリプトは通常、レート制限、ファイアウォール、その他の動的アクセス制御ロジックを実装するために使用されます。
  • proxy_set_header Upgrade $http_upgrade;
    • Upgradeリクエストヘッダーを設定し、WebSocketなどのプロトコルアップグレードをサポートします。$http_upgrade変数には元のリクエストのUpgradeヘッダーフィールド値が含まれます。
  • proxy_set_header Connection "upgrade";
    • Connectionリクエストヘッダーをupgradeに設定し、通常Upgradeヘッダーと共に使用され、接続アップグレードを確実にします。
  • proxy_pass http://jms-uat;
    • 一致したリクエストをアップストリームサーバーhttp://jms-uatにプロキシします。
  • include https_proxy.conf;
    • 追加の設定ファイルhttps_proxy.confをインクルードします。このファイルにはHTTPSプロキシに関連する他の設定項目が含まれている可能性があります。
  • client_max_body_size 0;
    • クライアントリクエストボディの最大サイズを0に設定し、リクエストボディサイズを制限しないことを意味します。

これらのlocationブロックは、異なるパスのリクエストを処理し、アクセス制御段階でLuaスクリプトを使用して適切なロジック処理を実行します。完全一致の/core/auth/login/パスは特定の認証または前処理専用であり、ワイルドカードマッチの/パスはその他のすべてのリクエストを処理し、動的アクセス制御ロジックを実行します。両者は共同でNginxサーバーの柔軟性とセキュリティを確保します。

Luaスクリプト (rule.lua)

-- 接続プールタイムアウト回収時間(ミリ秒)
local pool_idle_timeout = 10000
-- 接続プールサイズ
local pool_capacity = 100
-- redis接続タイムアウト時間
local redis_connect_timeout = 100
-- redisホスト
local redis_server = "192.168.102.20"
-- redisポート
local redis_port_num = "6379"
-- redis認証
local redis_password = "123456"
-- IPブロック時間(秒)
local block_duration = 120
-- 指定IPアクセス頻度時間範囲(秒)
local time_window = 10
-- 指定IPアクセス頻度最大回数(回)
local max_requests = 60

-- エラーログ記録
local function log_error(message, exception)
    ngx.log(ngx.ERR, message, exception)
end

-- 接続プール解放
local function release_redis_connection(connection)
    if not connection then
        return
    end
    local success, error_msg = connection:set_keepalive(pool_idle_timeout, pool_capacity)
    if not success then
        ngx.say("redis接続エラー:", error_msg)
        return connection:close()
    end
end

-- redis接続
local redis_module = require "resty.redis"
local db_connection = redis_module:new()
local connect_success, connect_error = db_connection:connect(redis_server, redis_port_num)
-- 接続失敗時はサーバーエラーを返却
if not connect_success then
    return
end
-- タイムアウト時間設定
db_connection:set_timeout(redis_connect_timeout)

-- 認証操作最適化
local reuse_count, reuse_error = db_connection:get_reused_times()
-- 新規接続のため、認証が必要
if 0 == reuse_count then
    local auth_success, auth_error = db_connection:auth(redis_password)
    if not auth_success then
        log_error("認証失敗: ", auth_error)
        return
    end
elseif reuse_error then
    log_error("再利用回数取得失敗: ", reuse_error)
    return
end

-- リクエストIP取得
local function retrieve_client_ip()
    local source_ip = ngx.req.get_headers()["X-Real-IP"]
    if source_ip == nil then
        source_ip = ngx.req.get_headers()["x_forwarded_for"]
    end
    if source_ip == nil then
        source_ip = ngx.var.remote_addr
    end
    return source_ip
end

local current_client_ip = retrieve_client_ip()

local counter_key = "rate_limit:requests:" .. current_client_ip
local ban_key = "rate_limit:ban:" .. current_client_ip

-- IPがアクセス禁止されているかを照会、存在する場合は403エラーコードを返却
local is_banned, query_error = db_connection:get(ban_key)
if tonumber(is_banned) == 1 then
    ngx.exit(ngx.HTTP_FORBIDDEN)
    release_redis_connection(db_connection)
end

local request_count, count_error = db_connection:incr(counter_key)
if tonumber(request_count) == 1 then
    db_connection:expire(counter_key, time_window)
end
-- 単位時間制限を超えるアクセス回数の場合は制限識別子を追加し、制限時間はblock_duration
if tonumber(request_count) > tonumber(max_requests) then
    db_connection:set(ban_key, 1)
    db_connection:expire(ban_key, block_duration)
end

release_redis_connection(db_connection)

タグ: nginx openresty redis lua Security

7月14日 17:46 投稿