Published on

当ブログの技術スタックの紹介

Authors

はじめに

この記事では当ブログで使用している技術スタックについて紹介します。

自分で構築したサーバーからWebサイトを公開してみたい方、Webサイトの内部構造が気になる方などは当記事を参考にしてみてください。

※Webサイトの技術スタックを開示することは攻撃の余地を与える敵に塩を送るような行為なので、本来であればセキュリティ的にNGです。 当ブログでは教育目的として防御策を講じたうえであえて開示しているため、真似はしないでください。

アーキテクチャの概要

当ブログは以下の構成で動作しています。

ネットワーク概要図

個々のコンポーネントの詳細については後述しますが、ブラウザから当ブログへアクセスした際はCloudflare CDNからコンテンツが配信されます。 CDNのオリジンは自宅のDMZネットワーク上にあるリバースプロキシに到達するように設定しており、リバースプロキシを経由してWebサーバーへとつながります。

各コンポーネットの詳細

CDN

当ブログでは配信の効率化、DDoS攻撃対策を目的として、CloudflareのCDNを使用しています。 Cloudflareでは管理しているドメインを登録することで、CDNを無料使用できるうえ、グローバルIPを秘匿できるというメリットがあるため、個人が公開するWebサイトなどにおいては特別な理由がない限りCloudflare CDNを使うことをお勧めします。

CDNの設定はCloudflareのコンソールのDNSから設定することができ、AレコードのProxy StatusがProxiedになっている状態(以下の画像を参照)ではCDNが有効になります。

Cloudflare設定

CloudflareでDNS Onlyで登録されている場合、CDN機能が存在しないDNSサーバーを使用している場合では、nslookupコマンドなどによってドメインのAレコードを問い合わせることにより、WebサーバーをホストしているグローバルIPアドレスを特定することができます。 私のように自宅で契約している固定IPアドレスから配信している場合は、IPアドレスの情報が開示されることになるため、望ましくありません。

これは、CloudflareのDNSでAレコードがProxiedに設定することで防ぐことができ、以下の画像で示すように、nslookupコマンドでIPアドレスを問い合わせた場合にCloudflareのCDNのIPアドレスが返されるようになります。

Cloudflare設定

また、CloudflareではCDNの設定に加えて、CloudflareではCloudflare CDNとオリジン間の通信のTLS暗号化の設定が行えます。 Cloudflareではルート証明書で署名はされていないが、Cloudflareによって署名されている証明書を自由に発行することができ、この証明書をオリジン(今回の構成ではリバースプロキシが該当)にデプロイすることで、WebサーバーとCloudflare CDN間の通信を暗号化します。

※Cloudflareの証明書はルート証明書によって署名されていないため、ブラウザからオリジンへ直接アクセスを試みると証明書エラーが出ますが、Cloudflare CDNとオリジン間の通信のみを暗号化することを意図しているため、問題はありません。

リバースプロキシ

当ブログではDocker上でNginxのリバースプロキシを構成しています。リバースプロキシを構成することで以下のメリットが得られます。

1. オリジンへの直接アクセスをブロックすることができる。

CDNを使用してWebサーバーをホストしているグローバルIPを秘匿した場合においても、何らかの方法で攻撃者がオリジンを特定することができた場合、オリジンを対象することでCDNを迂回したDDoS攻撃が行われる可能性があります。 しかし、リバースプロキシのserver_nameにCDNに登録したドメイン名を設定することで、リクエストのHostヘッダーが正規のドメイン名であることを検証することができ、グローバルIPへの直接アクセスに対してリバースプロキシ側で404エラーを出すことができます。

2. 同一のグローバルIPを使用して他のドメインのWebサイトを配信することができる。

Nginxで複数のserver_nameを設定することで、一つのグローバルIPから複数のドメインのWebサイトを配信することができます。 私の自宅では1つのみ固定グローバルIPは1つのみ割り当てられるインターネット回線を契約しているため、自宅から複数Webサイトを配信する場合はリバースプロキシを使用してHostヘッダーによってリクエストを振り分ける必要があります。

3. 80番ポート(HTTP)に対するアクセスを443番ポート(HTTPS)にリダイレクトすることができる。

Nginxのリバースプロキシに以下の設定を追加することで、HTTPによるアクセスをHTTPSにリダイレクトすることが可能です。これによりHTTPにより通信内容を盗聴・改ざんされるリスクを低減できます。

default.conf
server {
  listen 80;
  server_name bob-sec.com www.bob-sec.com;
  return 301 https://$host$request_uri;
}
# 以下省略

Webサーバー

当ブログはオープンソースのリポジトリであるTailwind Nextjs Starter Blogをベースとして作成しています。 オープンソースをベースとした場合、最適化されたベストプラクティスを学びつつ品質の高いWebサイトを作成できるというメリットがあるため、Webサイトの自作に挑戦したい方は自力で一から作らず、オープンソースを活用することをお勧めします。

当ブログで採用したTailwind Nextjs Starter BlogではSEO対策やLight、Darkモードへの対応などWebサイトとして望まれる基本的な機能がデフォルトで実装されているため、ほとんどそのままの状態でブログとして使用することができます。

当ブログを公開するうえで、Tailwind Nextjs Starter Blogから変更した主な点は以下の通りです。

1. 脆弱性の解消

Next.jsにはリモートコード実行の脆弱性(CVE-2025-66478)が存在し、そのままインターネット上に公開するとサーバーが侵害されるおそれがあります。 Webサイトをインターネット上に公開する前には必ずnpm audit fix --forceを実行してください。

2. giscusのコメント機能の有効化

Tailwind Nextjs Starter Blogではgiscusを使用してブログのコメント機能をつけることが可能です。 この機能はプロジェクト内にあるsiteMetadata.jsを編集することで容易に有効化することができますが、デフォルトで組み込まれている機能はテーマカラーに沿っておらず違和感があったため、プログラムを変更しています。 具体的にはCSSによりテーマカラーに沿ったUIを定義したうえで、ライトモード、ダークモードへの切り替えに対応させるため、プログラムを修正しています。 当ブログを作成するうえでは、このライトモード、ダークモードへの対応に最も時間を要しています。

3. Noteのアイコンの追加

Tailwind Nextjs Starter Blogでは基本的なSNSのアイコンはデフォルトで用意されていますが、日本のSNSであるNoteまではカバーされていません。 そこで、 Note公式サイト からNoteのアイコンのSVGをダウンロードし、Figmaでサイズを調整したうえで、SVG形式のNoteのアイコンを追加しました。

4. プロジェクトの削除

Tailwind Nextjs Starter BlogではProjectというタブが用意されていますが、当ブログではこちらの機能は使用しないため、削除しています。

その他、Figmaで作成したロゴやfaviconの設定、テーマカラーの変更などの細かい修正を行っています。

プロジェクトの全体構成

ディレクトリ構造の詳細

Webサーバーのプロジェクトは以下のように構成しています。 XXXXXと記載している箇所はこのブログとは関係ないので、無視してください。 ./proxy/ssl/bob-sec.comの配下に存在するfullchain.pemとprivkey.pemはそれぞれcloudflareから入手した証明書と秘密鍵をデプロイしています。

dir
.
├── XXXXX
│   └── index.html
├── bob-sec-security-blog
│   ├── (Tailwind Nextjs Starter Blogのディレクトリ構成は省略)
│   ├── Dockerfile
├── docker-compose.yml
└── proxy
    ├── default.conf
    └── ssl
        ├── XXXXX
        │   ├── fullchain.pem
        │   └── privkey.pem
        └── bob-sec.com
            ├── fullchain.pem
            └── privkey.pem

docker-compose.ymlの詳細

docker-compose.ymlの中身です。 docker compose upを実行することで、web01(当ブログとは無関係のWebサーバー)、web02(bob-secセキュリティブログで使用されているWebサーバー)、リバースプロキシサーバーが立ち上がるように構成しています。

docker-compose.yml
version: "3.9"

services:
  proxy:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./proxy/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./proxy/ssl/:/etc/nginx/ssl/:ro
    depends_on:
      - web01
      - web02
    networks:
      - web

  web01:
    image: nginx:alpine
    volumes:
      - ./XXXXX:/usr/share/nginx/html:ro
    networks:
      - web

  web02:
    build:
      context: ./bob-sec-security-blog
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - NEXT_TELEMETRY_DISABLED=1
    expose:
      - "3000"
    networks:
      - web

networks:
  web:
    driver: bridge

bob-sec-security-blogのDockerfileの詳細

bob-sec-security-blogディレクトリ配下にあるDockerfileの中身です。 この内容は生成AIで作成しました。生成AIではキャッシュによるビルドの効率化まで考慮してくれるのため、Dockerfileなどの設計に関しては生成AIにお任せすることがいいと思います。

Dockerfile
# ===== deps =====
FROM node:25.2.1-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# ===== build =====
FROM node:25.2.1-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# ===== runner =====
FROM node:25.2.1-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.* ./

EXPOSE 3000

リバースプロキシ設定ファイルの詳細

リバースプロキシで使用されているdefault.confの内容です。

default.conf
server {
  listen 80;
  server_name XXXXX www.XXXXX;
  return 301 https://$host$request_uri;
}

server {
  listen 80;
  server_name bob-sec.com www.bob-sec.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name XXXXX www.XXXXX;

  ssl_certificate     /etc/nginx/ssl/XXXXX/fullchain.pem;
  ssl_certificate_key /etc/nginx/ssl/XXXXX/privkey.pem;

  ssl_protocols TLSv1.2 TLSv1.3;

  location / {
    proxy_pass http://web01:80;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
  }
}

server {
  listen 443 ssl http2;
  server_name bob-sec.com www.bob-sec.com;

  ssl_certificate     /etc/nginx/ssl/bob-sec.com/fullchain.pem;
  ssl_certificate_key /etc/nginx/ssl/bob-sec.com/privkey.pem;

  ssl_protocols TLSv1.2 TLSv1.3;

  location /static/giscus/ {
    proxy_pass http://web02:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    add_header Access-Control-Allow-Origin "https://giscus.app" always;
    add_header Vary "Origin" always;
  }

  location / {
    proxy_pass http://web02:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
  }
}

server {
  listen 80 default_server;
  server_name _;
  return 404;
}

本番換気用へのデプロイ方法

当ブログの開発はVirtualBoxで構築したWindowsの仮想環境上で行っていますが、本番WebサーバーはUbuntu Serverを使用しています。 本番WebサーバーへのプロイについてはGitHub上のプライベートリポジトリを使用しており、開発環境で作成した内容をgit pushによりGitHubにアップロードし、本番Webサーバー上でgit pullを実行することで、サーバー上にダウンロードしています。 デプロイ後はdockerコンテナを再起動する必要があります。

今後の拡張予定

CI/CDの導入

現在は本番環境にログインしたうえでコマンドを打ってデプロイを行っていますが、更新のたびに手間がかかります。 この手間を解消するためにCI/CDを導入し、GitHubリポジトリに更新に伴って自動的にデプロイまで行うようにすることを検討しています。

冗長化

現在は本番Webサーバーへデプロイする際にサーバーを再起動する必要があり、ダウンタイムが発生します。 このダウンタイムの発生を解消するために、Kubernetesを使用してWebサーバーの冗長化する予定です。

さいごに

今回は私が作成したブログの構成を示しましたが、汎用的なオンプレのWebサイトの構成であるため、Webサイトを作成したい人やWebサイトの内部構造を知りたい方にとってはいい参考になる内容だと思います。

本記事が役に立ったと感じていただけたら、Xなどで共有していただけると励みになります。 今後もセキュリティに関する技術的な内容を発信していく予定ですので、ぜひチェックしてください。