PR

【Next.jsブログ構築】VPSのセキュリティ強化とSQLite導入|fail2ban・Nginx設定手順

Next.jsブログサーバーのセキュリティ強化プロセスを描いた、白背景のイラストです。fail2banによる保護を示す盾のアイコン、Nginxのレート制限、SQLiteデータベースのシンボルを配置し、VPSのセキュリティ構成を表現しています。 VPS・RentalServer
この記事は約15分で読めます。
記事内に広告が含まれています。
スポンサーリンク

これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。

前回の第18回では、
Next.js ブログを静的エクスポートモードからサーバーモードに切り替えました。

静的サイトでは、Nginx がファイルを返すだけで処理が完結していました。
しかしサーバーモードに切り替えたことで、Node.js プロセスが常時起動し、
外部からのリクエストを処理するようになります。
これにより、データベースへの読み書きや API の作成が可能になった一方で、
新たなセキュリティ上の考慮が必要 になります。

この記事では以下の内容を解説します。

  • サーバーモード移行前に確認しておきたい Nginx と fail2ban の基本設定
  • API エンドポイント保護のためのレート制限ゾーンの追加方法
  • Nginx のアクセスを監視する fail2ban ジェイルの追加手順
  • 閲覧数カウンター用のデータベース(SQLite)の導入とテーブル作成

記事の前提条件

この記事は以下の環境を前提として手順を解説しています。

  • 第18回本編が完了していること: next start でサーバーモードが動作し、PM2 でプロセス管理されている状態
  • OS: Ubuntu 24.04 LTS
  • Webサーバー: Nginx(リバースプロキシ設定済み)
  • 対象読者: シリーズ第18回本編まで完了し、サーバーモードへの切り替えが完了している方

事前確認:すでに設定済みかどうかを確認する

この補足記事で行う作業には、すでに設定済みの方にとっては不要なものが含まれています。

私の環境ではサーバーモード移行前から Nginx と fail2ban の基本設定が完了していたため、今回の作業では「追加・変更」のみを行いました。
読者の環境によってはまだ設定されていない場合があるため、作業前に以下の項目を確認してください。

Nginx グローバル設定の確認

grep -n "server_tokens\|X-Frame\|X-Content\|limit_req_zone" /etc/nginx/nginx.conf

-n オプションは行番号を一緒に表示するためのものです。

以下の3項目が出力されれば、基本的なセキュリティ設定は済んでいます。

server_tokens off;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";

出力されなかった場合: /etc/nginx/nginx.confhttp {} ブロック内に以下を追加してください。

server_tokens off;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

fail2ban の基本設定の確認

sudo fail2ban-client status

sshdrecidive が Jail list に含まれていれば、SSH 保護の基本設定は済んでいます。

Status
|- Number of jail:      3
`- Jail list:   recidive, sshd, ...

お使いの環境によって表示されるジェイルの数や内容は異なります。
WordPress をインストールしている場合は nginx-wp など追加のジェイルが表示されます。
sshdrecidive の2つが含まれているかどうかを確認してください。

含まれていない場合: /etc/fail2ban/jail.local に以下を追加し、sudo systemctl restart fail2ban を実行してください。

[sshd]
enabled = true
port = 22
bantime  = 86400
findtime  = 600
maxretry = 3

[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
backend = auto
bantime  = 1w
findtime = 3d
maxretry = 2

SSH ポートを変更している場合は port = 22 をご自身のポート番号に変更してください。

STEP 1:Nginx に API レート制限ゾーンを追加する

サーバーモードになると、次の回から実装する /api/views のような API エンドポイントが生まれます。
悪意のあるアクセスから API を守るために、レート制限ゾーンを追加します。

レート制限ゾーンとは: 同じ IP アドレスから短時間に大量のリクエストが届いた場合にブロックする仕組みです。
limit_req_zone でゾーン(制限のルールセット)を定義しておき、後から各エンドポイントに適用する使い方をします。

まず現在の設定を確認します。

grep -n "limit_req" /etc/nginx/nginx.conf

zone=login がすでに存在する場合

WordPress のログイン保護などですでに limit_req_zone が設定されている場合は、その行の直後に API 用のゾーンを追加します。

以下のコマンドは zone=login の行を検索して、その直後に新しい行を挿入します。
s|検索文字列|置換文字列| という構文で、マッチした行を新しい内容に置き換えます。

sudo sed -i 's|limit_req_zone \$binary_remote_addr zone=login:10m rate=5r/m;|limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;\n\t# API エンドポイント用レート制限ゾーン(閲覧数カウンター等)\n\tlimit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;|' /etc/nginx/nginx.conf

zone=login が存在しない場合

/etc/nginx/nginx.confhttp {} ブロック内に直接以下を追加してください。

# API エンドポイント用レート制限ゾーン
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;

設定値の意味

  • zone=api:このゾーンの名前。後から limit_req zone=api と書いてエンドポイントに適用します
  • 10m:このゾーンが使うメモリ量(約1万 IP アドレスを管理できます)
  • rate=30r/m:1分間に30リクエストまで許可(2秒に1回)

30リクエスト/分とした根拠:記事ページを1つ開くたびに API が1回呼ばれます。
1分間に10ページ閲覧しても10リクエストです。余裕を持って30に設定しています。
厳しすぎると熱心に記事を読んでいる読者の閲覧数がカウントされなくなるため、このバランスにしました。

追加されたことを確認します。

grep -n "limit_req" /etc/nginx/nginx.conf

zone=api の行が追加されていれば成功です。設定のテストを行います。

sudo nginx -t

test is successful と表示されたら Nginx をリロードします。

sudo systemctl reload nginx

STEP 2:fail2ban に Nginx 監視用のジェイルを追加する

fail2ban の「ジェイル(jail)」は、ログファイルを監視して不正アクセスを繰り返す IP アドレスを自動的にブロックする設定のことです。

これまでは SSH への不正アクセスを監視していましたが、サーバーモードへの移行に合わせて Web アクセスの監視も追加します。

使用するフィルターの確認

fail2ban には標準でいくつかの Nginx 用フィルターが付属しています。
まず使用するフィルターが存在することを確認します。

ls /etc/fail2ban/filter.d/ | grep nginx

以下の2つが含まれていれば問題ありません。

nginx-botsearch.conf
nginx-limit-req.conf

ジェイルを追加する

/etc/fail2ban/jail.local の末尾に2つのジェイルを追加します。

以下のコマンドの tee -a は、-a(append=追記)オプションにより、ファイルを上書きせず末尾に内容を追加します。
このコマンドは一度だけ実行してください。複数回実行すると同じ内容が重複して追記されます。

sudo tee -a /etc/fail2ban/jail.local > /dev/null << 'EOF'

[nginx-limit-req]
# Nginx のレート制限を繰り返し超えるIPをブロックする
enabled  = true
port     = http,https
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 5
findtime = 300
bantime  = 86400

[nginx-botsearch]
# 存在しないURLを繰り返しスキャンするボットをブロックする
enabled  = true
port     = http,https
filter   = nginx-botsearch
logpath  = /var/log/nginx/access.log
maxretry = 10
findtime = 300
bantime  = 86400
EOF

各設定値の説明

設定項目nginx-limit-reqnginx-botsearch
監視対象Nginx エラーログ(レート制限超え)Nginx アクセスログ(存在しない URL)
maxretry5回10回
findtime5分(300秒)5分(300秒)
bantime24時間(86400秒)24時間(86400秒)

nginx-limit-req の役割: STEP 1 で設定したレート制限を繰り返し超える IP をブロックします。
レート制限を超えた正常なユーザーはほぼ存在しないため、maxretry = 5 と厳しめに設定しています。

nginx-botsearch の役割: .env.git/config などの存在しないパスを繰り返しスキャンするボットをブロックします。
このようなスキャンは私の環境では毎日数十〜数百件記録されます。10回アクセスした時点でボットと判断します。

bantime を24時間にした理由: 1時間のブロックでは翌日また同じ IP が来てしまいます。
既存の SSH 保護(sshd)・WordPress 保護(nginx-wp)と同じ24時間に統一することで、一貫したセキュリティポリシーを維持できます。

また、これらの新しいジェイルで24時間バンされた IP が3日以内に再度バンされた場合、既存の recidive ジェイルが自動的に1週間のブロックに格上げします。
2段構えの防御になっています。

設定を反映して確認します。

sudo systemctl restart fail2ban
sudo fail2ban-client status

Jail list に nginx-botsearchnginx-limit-req が追加されていれば成功です。

Status
|- Number of jail:      5
`- Jail list:   nginx-botsearch, nginx-limit-req, nginx-wp, recidive, sshd

STEP 3:SQLite をインストールする

次の回で実装する閲覧数カウンターのために、データベースを準備します。

なぜ SQLite を選んだか:
VPS にはすでに WordPress 用に MariaDB がインストールされていますが、Next.js 側には SQLite を使うことにしました。
理由は3つあります。

1つ目は、WordPress のデータベースと完全に切り離せることです。
両者を MariaDB で共有すると、設定ミスや障害が互いに影響するリスクがあります。

2つ目は、設定が簡単なことです。
MariaDB を使う場合、データベース・ユーザー・権限の作成が追加で必要になります。
SQLite はファイル1つがそのままデータベースになるため、この手間がありません。

3つ目は、個人ブログの閲覧数カウントという用途では SQLite で十分なことです。
MariaDB の「大量同時アクセスへの強さ」が活きるのは1秒間に何百件もの書き込みが発生する場合です。
個人ブログの規模ではその必要はありません。

まず SQLite3 コマンドラインツールをインストールします。

sudo apt install sqlite3
sqlite3 --version

バージョン番号が表示されればインストール成功です。

続けて、Next.js のコードから SQLite を操作するための Node.js 用ライブラリをインストールします。

cd ~/example-blog && npm install better-sqlite3
npm install --save-dev @types/better-sqlite3

@types/better-sqlite3 は TypeScript 用の型定義ファイルです。
コードエディタ上での補完が効くようになります。

インストールされたことを確認します。

grep "better-sqlite3" ~/example-blog/package.json

以下のように両方が表示されれば成功です。
バージョン番号はインストール時期によって異なる場合があります。

"better-sqlite3": "^12.9.0",
"@types/better-sqlite3": "^7.6.13"

STEP 4:データベースファイルとテーブルを作成する

データベースの保存場所

~/example-blog/data/ というディレクトリを作成し、その中に views.db ファイルを置きます。

この場所にした理由は、public/ フォルダの外だからです。
public/ の中にあるファイルは Nginx 経由で外部から直接アクセスできてしまいます。
data/ ディレクトリはウェブ公開ディレクトリの外になるため、外部から到達できません。

mkdir -p ~/example-blog/data

3つのテーブルを作成する

閲覧数カウンターでは以下の3つのテーブルを使います。

テーブル名用途保存期間
daily_views日別閲覧数(当日表示・過去30日ランキング用)60日で自動削除
monthly_views月別閲覧数(月別集計用)永続保存
total_views全期間閲覧数(ランキング・総閲覧数用)永続保存

3テーブルに分けている理由は、用途に応じてデータの持ち方を変えるためです。
「今日の閲覧数」は毎日蓄積すると量が増えるため60日で削除します。
「月別」と「全期間」は集計結果として永続保存します。
これにより必要なデータは失わずにディスクの使用量を抑えられます。

以下のコマンドでデータベースファイルを作成し、3つのテーブルを作成します。

sqlite3 ~/example-blog/data/views.db << 'EOF'
CREATE TABLE IF NOT EXISTS daily_views (
  url TEXT NOT NULL,
  date TEXT NOT NULL,
  count INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (url, date)
);

CREATE TABLE IF NOT EXISTS monthly_views (
  url TEXT NOT NULL,
  year_month TEXT NOT NULL,
  count INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (url, year_month)
);

CREATE TABLE IF NOT EXISTS total_views (
  url TEXT PRIMARY KEY NOT NULL,
  count INTEGER NOT NULL DEFAULT 0
);
EOF

テーブル構造の説明

daily_views urldate の組み合わせが主キーです。
同じ URL・同じ日付のレコードは1つしか作れないため、閲覧のたびに count を +1 していきます。
date"2026-04-26" のような文字列で保存します。

monthly_views urlyear_month の組み合わせが主キーです。
year_month"2026-04" のような年月の文字列で保存します。
月をまたいでも途切れずに集計できるため、月別のグラフや表示に利用します。

total_views url 単体が主キーです。
記事1本につき1レコードだけ存在し、アクセスのたびに count を +1 していきます。
全期間の総閲覧数やランキング表示に使います。

テーブルが作成されたことを確認します。

sqlite3 ~/example-blog/data/views.db ".tables"
daily_views    monthly_views    total_views

3つのテーブル名が表示されれば成功です。

データベースがデプロイで消えないことを確認する

deploy.shnpm run buildpm2 restart だけの構成になっています。
data/views.db を操作する処理は一切ないため、デプロイのたびにデータが消える心配はありません。

deploy.bat(Windows のバッチファイル)が転送する対象は posts/public/images/ だけです。
data/ ディレクトリは転送対象外のため、こちらも問題ありません。

まとめ

LIFEWORK Blogの中でも書いていますが、VPS でサーバーを外部公開していると、驚くぐらいの数の攻撃を毎日受けます。

BAN されても執拗に攻撃してくるケースや、なんとかして脆弱性を突こうとさまざまな攻撃パターンを仕掛けてくるケース、中には fail2ban に感知されないように時間をかけて、ゆっくりと攻撃してくるようなケースもあります。

完全なセキュリティというものは存在しないと思いますが、今回の作業を実施すれば、個人のブログサイトとしては、結構高いレベルのセキュリティ対策になるはずです。

LIFEWORK Blog では、自身で運営している VPS 上の Next.js サイト(LIFEWORK Blog Next )に対して実施される攻撃を監視しながら、新しい対策が必要になったら、今後の記事で紹介していきます。

今回の作業で完了したことをまとめます。

作業内容
Nginx レート制限ゾーン追加zone=api(30リクエスト/分)を nginx.conf に追加
fail2ban ジェイル追加nginx-limit-reqnginx-botsearch を追加(各24時間バン)
SQLite インストールsqlite3 コマンドツールをインストール
better-sqlite3 インストールNode.js から SQLite を操作するライブラリをインストール
テーブル作成閲覧数カウンター用の3テーブルを作成

今回は「器を作った」段階です。
データベースのテーブルは作成しましたが、実際の閲覧数カウントはまだ動いていません。
テーブルの中はまだ空の状態です。

次の回(第19回)では、/api/views という API ルートを作成し、実際にページへのアクセス時に閲覧数を書き込む処理を実装します。

コメント

タイトルとURLをコピーしました