タグ: Linux Page 1 of 39

UbuntuServerでのプロキシ設定をコマンドラインで設定する。

Ubuntu Server(GUIなし環境)において、PACファイル(プロキシ自動設定ファイル)が導入されている環境でネットワーク通信(主にAPTパッケージ管理)を有効化するための作業メモです。

背景と注意点

割と頻出する状況だと思います。

  • 検証環境をローカルNWに立てたい
  • でも、社内ポリシーでプロキシーを通す必要がある
  • Windows端末等はGUIで操作できるが、Linuxはそうでない

Ubuntu ServerのCLI環境(apt コマンドなど)は、デフォルトではJavaScriptで記述されたPACファイルを直接解釈できません。

そのため、「PACファイルの内容を確認し、そこに記載されている実際のプロキシサーバーのIPとポートを直接設定する」方法が確実です。

手順1: PACファイルから実際のプロキシ情報を抽出する

まず、サーバー上で curl コマンドを使用してPACファイルの内容を読み込み、転送先となっているプロキシサーバーの「IPアドレス」と「ポート番号」を特定します。

curl -s http://192.168.1.10/proxy.pac

出力例の確認

表示例

function FindProxyForURL(url, host) {
        var clientIP = myIpAddress();
        if (
                isInNet(host, "10.0.0.0", "255.0.0.0") ||
                isInNet(host, "192.168.0.0", "255.255.0.0")
        ) {
                return "DIRECT"; // ローカル通信はプロキシを通さない
        }
        else { 
                // 外部インターネット通信用のプロキシサーバー情報
                return "PROXY 192.168.1.50:8080"; 
        }
}

確認ポイント:

return "PROXY ..." の部分に書かれている情報(上記例では 192.168.1.50:8080)を控えます。

手順2: APT(パッケージ管理)用プロキシの設定

特定したプロキシサーバー情報を、APTの設定ファイルに書き込みます。

設定ファイルの作成・編集

教義・信仰に沿ったエディタを用いて、以下のファイルを編集/作成します。

/etc/apt/apt.conf.d/99proxy

ファイル内に以下の2行を記述します(手順1で確認したIPとポートに置き換えてください)。

Acquire::http::Proxy "http://192.168.1.50:8080/";
Acquire::https::Proxy "http://192.168.1.50:8080/";

gsettingswpad+ などの不要な記述が残っている場合は、すべて削除して上記のみにします。

手順3: 接続テスト(設定の確認)

設定が正しく反映され、インターネット経由でパッケージ情報が取得できるかテストします。

sudo apt update

「無視(Ignored)」や「タイムアウト」のエラーが多発せず、ヒット(Hit)や取得(Get)が進み、最後に 読み込み完了(Reading package lists… Done)と表示されれば設定完了です。

(参考)一般コマンド用(環境変数)の一時設定

もし curlwget など、apt 以外の一般コマンドでもインターネット通信を行いたい場合は、現在のターミナルセッションに対して一時的に以下の環境変数を適用してください。

export http_proxy="http://192.168.1.50:8080"
export https_proxy="http://192.168.1.50:8080"
export no_proxy="localhost,127.0.0.1"

吠えなかった犬の挙動。(POSTに隠されたRedTail)

はじめに

「吠えなかった犬の推理」

という言葉があります。シャーロック・ホームズ『白銀号事件』にある有名なやりとり、

「その他何か私の注意すべきことはないでしょうか?」
「あの晩の犬の不思議な行動に御注意なさるといいでしょう」
「犬は全然何もしなかったはずですが」
「そこが不思議な行動だと申すのです」

から来た、「一見すると不自然ではない(何も起きていない)ことが、状況を踏まえて考えれば極めて不自然であること」という、ミステリの定理とも言えるロジック。

筆者は公開しているVPSで不審なエラーログ(攻撃の検知ログ)は毎日のように見ていますが、先日、エラーではなく通常のアクセスログに、極めて不審な(というか完全にアウトな)一行を発見しました。

今回は、そのログの正体と、その裏に隠された攻撃者の意図について解説します。

見つかった「不自然な1行」のログ

見つかったのは、以下のようなログです(※IPアドレスやホストなどの情報はダミーに無害化しています)。

203.0.113.45 - - [02/Jun/2026:05:23:14 +0900] "POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1" 404 3097 "-" "libredtail-http"

Webサイトの運用経験がある方なら、この1行を見ただけでいくつか異様な点に気づくかと思います。

  • 見覚えのない海外IPからのアクセス
  • /.%2e/.%2e/ という怪しげな文字列の連続
  • 通常のブラウザでは使われない libredtail-http というUser-Agent
  • そして、何より/bin/sh(OSのシェル)に対して「POST」リクエストを送っているという異常さ

幸い、ステータスコードは 404(Not Found) なので、攻撃はサーバー側で弾かれています。では、この攻撃者は一体何をやろうとしていたのでしょうか?

このログの正体:パストラバーサル攻撃(CVE-2021-41773)

このリクエストは、過去に広く報道された Apache HTTP Serverの脆弱性(CVE-2021-41773など) を狙った自動スキャン(攻撃)ツールによるものです。

URLに含まれる .%2e をURLデコードすると、親ディレクトリを指す .. になります。
攻撃者は、公開フォルダ(cgi-bin)から強制的に外へ飛び出し、サーバーのルートにあるOSの実行ファイル(/bin/sh:シェル)に直接アクセスしようとしていたのです。古典的ですが強力なパストラバーサル(ディレクトリトラバーサル)攻撃です。

なぜ「GET」ではなく「POST」なのか?

「情報を盗み見るだけなら GET なのでは?」と思いましたが、調べてみると攻撃者の意図が浮かび上がりました。

① サーバー上で「コマンドを実行」させるため(RCE)

攻撃者の最終目的は、ファイルを覗き見ることではなく、サーバーを乗っ取ることです。
POST リクエストの「ボディ(本文)」部分に、実行させたい悪意あるLinuxコマンド(マルウェアのダウンロード命令など)を乗せて送信するのが真の目的です。

② 痕跡(ログ)を隠すため

GET メソッドの場合、実行したいコマンドをURLの後ろ(クエリパラメータ)に付ける必要があります。しかし、それだとアクセスログのURL部分に攻撃コマンドが丸見えになってしまいます。

Webサーバーの標準ログ設定は「ヘッダー」しか記録せず、「ボディ」は記録しません(パスワードやカード情報などの機密情報が含まれるため)。
攻撃者はこれを利用し、「POST/bin/sh を叩こうとした」という最低限の事実だけをログに残し、肝心の悪意ある命令(ボディ)をログから隠蔽しているのです。

攻撃の背景にあった、巨大な暗号資産マイニングマルウェア

さらにこのログの User-Agent にある libredtail-http を調べると、明確な犯行の背景が浮かび上がってきました。

これは、感染したサーバーのパワーを勝手に使って暗号資産(仮想通貨)を強制採掘させるマルウェア「RedTail」の拡散ツールです。

もし、サーバーに脆弱性が存在し、この POST が実行(200 OK)されてしまっていた場合、以下のような身の毛もよだつシナリオが進行していました。

  • マルウェア(RedTail)の強制インストール:
    • バックドアが設置され、サーバーが完全に乗っ取られます。
  • CPU使用率が「100%」に張り付く:
    • 裏でマイニングツール(XMRig)が暴走し、サイトが激重になります。クラウドの場合は莫大な従量課金が請求されます。
  • 報酬の隠蔽(マイニングプロキシの中継):
    • 採掘データを直接プールに送らず、攻撃者が用意した中継サーバーを経由させる。これによりウォレットアドレス(足跡)を隠蔽し、リサーチ会社や捜査の手から逃れる工作まで行っています。
      次の攻撃の「踏み台」にされる:
    • 被害者であると同時に加害者になり得ます。世界中の他のサーバーへ向けて、同様の不審な POST リクエストを無差別に送り始めます。

なぜ「犬は吠えなかった」のか?

筆者のVPSには、強力なWAFである ModSecurity を導入して不審な攻撃をシャットアウトしていますが、不思議なことに、今回この件に関するエラーログ(拒否ログ)は現れませんでした。

「攻撃が届いているのに、なぜWAFは吠えなかったのか?」

その理由は、以下の2つの可能性が考えられます。

1. 表面上は正しいドメイン名とヘッダを指定していた

筆者の環境では、ModSecurityで以下のようなカスタムルールを設けています。

# IPアドレス直打ちアクセス対策
SecRule REQUEST_HEADERS:Host "@rx ^[\d.]+(:\d+)?$" \
    "id:10001,\
    phase:1,\
    deny,\
    status:404,\
    log,\
    msg:'[CUSTOM RULE] Host header is a numeric IP address (incl port). Blocked immediately.',\
    tag:'application-attack',\
    tag:'PROTOCOL_VIOLATION/INVALID_HREQ'"

# Hostヘッダーが存在しない場合は即ブロック
SecRule &REQUEST_HEADERS:Host "@eq 0" \
    "id:10002,\
    phase:1,\
    deny,\
    status:404,\
    log,\
    msg:'[CUSTOM RULE] Missing Host Header. Blocked immediately.'"

「IPアドレス直打ち」や「Hostヘッダが無い」という、通常のWebブラウズではまず存在しないアクセスは、このルールで瞬殺(phase 1でブロック)されます。自動スキャナーの多くはこれで引っかかります。

この攻撃者は、筆者のサーバーの防御手段を見抜いていたのか、ご丁寧に正しいドメイン名をHostヘッダーに指定して、この網を通り抜けてきたと考えられます。

2. WAFが動く前に、Apacheのコア機能が処理を終わらせていた

もう一つの有力な可能性は、Apache自体の挙動です。
ModSecurityがURLの中身を深く精査(パース)するよりも前の段階で、Apacheのコア機能がURLのパスを処理した結果、「物理的にファイルが存在しない(404 Not Found)」と判断して処理を終了したパターンです。

エラー(拒否)ではなく、純粋に「そんなファイルは無いよ」として正常に(?)404を返したため、WAFの検知ログには残らなかった、というわけです。

Apacheにおいて、URLの文字列をデコードして物理的なファイルパスにマッピングする処理(ap_directory_walk)は、ModSecurityの phase:1(ヘッダー検査)と phase:2(ボディ検査)の間、あるいはその手前で行われます。

【Apacheの処理フロー】
1. リクエスト受信
2. ModSecurity (phase:1)  ← Hostヘッダーチェック(ドメインが正しいので通過!)
3. Apacheコア (パスの解決) ← 「/.%2e/」を解析しようとするが、そんなCGIは存在しない(404確定)
4. ModSecurity (phase:2)  ← 実行される前に、Apacheが「404」として即レスポンスを返して終了

つまり、攻撃者はドメインチェック(貴方のカスタムルール10001)を賢くすり抜けたものの、Apache自体のパス解決の壁に激突して、WAFが本格的に牙を剥く前に死んでいたわけです。

 3. 攻撃者が「POST」を選んだもう一つの邪悪な理由

それは「GETの文字数制限を回避するため」です。

RedTailのようなマルウェアは、侵入成功と同時に「Base64で難読化した巨大なシェルスクリプト」を送り込んできます。URLの末尾(GET)にこれを付けると、Apacheの最大URL長制限(LimitRequestLine:通常8KB)に引っかかり、コマンドが途中で切れて実行できません。 そのため、数万文字の「汚い攻撃コード」を確実に一発で流し込むために、ボディ制限が緩い POST を選択せざるを得ないのです。

まとめ:たまには「正常系のログ」も見ましょう

今回、攻撃の予兆に気づけたのは、「運良く」アクセスログを見ていた結果です。

自分の身やデータを守るため、そして自分が踏み台(加害者)にならないためにも、以下の基本を徹底しましょう。

  • Webサーバー(ApacheやNginxなど)を常に最新バージョンにアップデートしておく
  • 不要なCGI設定(cgi-binなど)は無効化・削除しておく
  • もはや必須となっているWAF(ModSecurityなど)の導入
  • クローラーや無駄なアクセスを拾わないログの整理

「エラーがない=安全」とは限らない。
攻撃があった兆候は、静かに普通のアクセスログにも現れる、というお話でした。

設定したApacheによる動きの解説

先ほどのエントリーの続きです。

「実際に筆者が施している」

Apache設定を元に「どういう動きをしているのか」を紹介します。

サンプル例

サイト名やIPアドレスなどはダミーにしています。

# ============================================================
# 【HTTP: 80番ポート】常時HTTPS(SSL/TLS)へのリダイレクト
# ============================================================
<VirtualHost *:80>
    ServerName example.com

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>

# ============================================================
# 【HTTPS: 443番ポート】要塞化されたメインサーバー設定
# ============================================================
<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/html/public

    # --------------------------------------------------------
    # 1. アクセスログの間引き(ノイズカット)設定
    # --------------------------------------------------------
    # 自身の監視用IPなど、ログに記録させたくないIPを指定
    SetEnvIf Remote_Addr "192.168.1.100" dontlog
    SetEnvIf Remote_Addr "10.0.0.1" dontlog

    # 検索エンジンのクローラー(健全なBot)もログから除外してノイズを減らす
    SetEnvIfNoCase User-Agent "Googlebot" dontlog
    SetEnvIfNoCase User-Agent "GoogleOther" dontlog

    # 【外部ファイル連携】定期的・自動的に更新する悪質ボットのリストを読み込む
    # ※ファイル内で「SetEnv bad_bot 1」などのフラグを立てる想定
    Include /etc/apache2/conf-available/blacklist-bots.txt

    # dontlogフラグが付いたアクセスは記録しない
    CustomLog /var/log/apache2/example_access.log combined env=!dontlog
    ErrorLog /var/log/apache2/example_error.log

    # --------------------------------------------------------
    # 2. メインディレクトリ制御(mod_rewrite × 環境変数)
    # --------------------------------------------------------
    <Directory /var/www/html/public>
        Options -MultiViews
        AllowOverride All

        # 大量の拒否アクセスによるエラーログ肥大化を抑制(重大エラーのみ記録)
        LogLevel authz_core:crit

        <IfModule mod_rewrite.c>
            RewriteEngine On

            # 【攻撃検知】特定の動的ページに対する「大量のタグ連結」などのボット挙動を迎撃
            # 例:特定のURLパスにアクセスがあり、かつクエリにURLエンコードされたカンマ等が大量に含まれる場合
            SetEnvIf Request_URI "^/search/tags" is_target_page
            SetEnvIf Query_String "tag=.*(%2c|,|%e3%80%81).*(%2c|,|%e3%80%81).*(%2c|,|%e3%80%81)" bad_tag_stacking

            # 条件に合致したら、ログを残さず「404 Not Found(存在しない)」を返して虚無へ葬る
            RewriteCond %{ENV:is_target_page} 1
            RewriteCond %{ENV:bad_tag_stacking} 1
            RewriteRule ^ - [E=dontlog:1,R=404,L]

            # 【外部ファイル連携】スパムIPアドレスのブラックリストを読み込んで404を返す
            Include /etc/apache2/conf-available/spam-ips.txt
        </IfModule>

        # 【アクセス拒否】上記の設定や外部リストで「bad_bot」と判定された通信を遮断
        <RequireAll>
            Require not env bad_bot
            Require all granted
        </RequireAll>
    </Directory>

    # 403 Forbidden(拒否)を返すと攻撃者に「防御されている」とバレるため、
    # 403の際も「404 Not Found(最初から何も無い)」として処理する
    ErrorDocument 403 /404.html

    # --------------------------------------------------------
    # 3. セキュリティヘッダーの強化(mod_headers)
    # --------------------------------------------------------
    Header always set Strict-Transport-Security "max-age=63072000"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set X-XSS-Protection "1; mode=block"

    # --------------------------------------------------------
    # 4. Nepenthes(ウツボカズラ)トラップ設定(mod_alias)
    # --------------------------------------------------------
    # 攻撃者が盲目的にスキャンしてくる「脆弱性のあるパス」を、ダミーの静的ファイルへ吸い込ませる
    <IfModule mod_alias.c>
        # WordPressを使っていない(または構成が違う)のに狙われるパス
        Alias /wp-login.php /var/www/nepenthes/dummy_login.html
        Alias /wp-admin     /var/www/nepenthes/dummy_login.html
        Alias /wordpress    /var/www/nepenthes/dummy_login.html

        # 漏洩すると致命的な .git ディレクトリへのスキャン対策
        Alias /.git         /var/www/nepenthes/dummy_git.html

        # robots.txtで「巡回禁止」にしているにもかかわらず、
        # 行儀悪くスクラップ(収集)しにくるAI自動巡回エージェント用のトラップ
        Alias /assets-archive /var/www/nepenthes/dummy_login.html

        # トラップ用ディレクトリへのアクセスを許可
        <Directory /var/www/nepenthes>
            Require all granted
        </Directory>
    </IfModule>

    # --------------------------------------------------------
    # 5. SSL/TLS暗号化プロトコル設定
    # --------------------------------------------------------
    SSLEngine on
    Protocols h2 http/1.1

    # 安全性の低い古いプロトコルを徹底排除(TLS 1.2 / 1.3 のみ許可)
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384
    SSLHonorCipherOrder off
    SSLSessionTickets off

    SSLCertificateFile    /etc/ssl/certs/example.com.crt
    SSLCertificateKeyFile /etc/ssl/private/example.com.key

    # --------------------------------------------------------
    # 6. WAF(ModSecurity)のチューニング
    # --------------------------------------------------------
    <IfModule security2_module>
        SecRuleEngine On

        # ファイルアップロードの容量制限を引き上げ(必要に応じて調整)
        SecRequestBodyInMemoryLimit 524288000
        SecRequestBodyLimit         524288000
        SecRequestBodyNoFilesLimit  524288000

        # --- 偽陽性(誤検知)の除外ルール ---
        # ※システム(RedmineやWordPressなど)の特性に応じて、正常な通信が遮断されないよう調整
        SecRuleRemoveById 920420 # 特定JavaScriptの誤検知対策
        SecRuleRemoveById 200004 # マルチパート解析エラーの除外
        SecRuleRemoveById 920300 # HTTPプロトコル違反の除外
        SecRuleRemoveById 920260
        SecRuleRemoveById 920270
        SecRuleRemoveById 920240

        # --- 記憶抹消刑(Damnatio Memoriae)システム ---
        # あらかじめリスト化した凶悪なIP(negativelist.txt)からのアクセスは、
        # 404エラーを返しつつ、Apacheのログ(auditlog等含む)に一切記録させず存在を抹消する
        SecRule REMOTE_ADDR "@pmFromFile negativelist.txt" "phase:1,id:2,status:404,msg:'Damnatio Memoriae',nolog,noauditlog"
    </IfModule>
</VirtualHost>

では、各設定の意図は後回しにして、「どういう動きなのか」を見てみましょう。

パターン1: http→httpsへの変更

http(80番)で来たアクセスを強制的にhttps(443)に転送します。

sequenceDiagram autonumber participant Client as ユーザー / 攻撃者 participant Apache80 as Apache (ポート80) Client->>Apache80: HTTPでアクセス (http://example.com) Note over Apache80: RewriteEngine On<br/>RewriteCond %{HTTPS} off Apache80-->>Client: 301 Moved Permanently (https://...)

パターン2: 通常アクセス

悪意を持たないアクセスには正常に通信します。

sequenceDiagram autonumber participant User as 通常ユーザー (または Googlebot) participant Apache443 as Apache (ポート443) participant WebPage as メインコンテンツ (/public) User->>Apache443: HTTPSでアクセス (正常なリクエスト) alt Googlebot や 監視IP の場合 Note over Apache443: SetEnvIf で「dontlog」フラグを付与 Note over Apache443: アクセスログ(example_access.log)へは記録しない else 一般の通常ユーザーの場合 Note over Apache443: 通常通りアクセスログに記録 end Apache443->>WebPage: コンテンツ読み込み WebPage-->>Apache443: ページデータ返却 Note over Apache443: セキュリティヘッダーを付与<br/>(HSTS / X-Content-Type-Options / X-Frame-Options / X-XSS) Apache443-->>User: 200 OK (安全な通信でWebページを表示)

パターン3:攻撃者、ボットなど

スクレイピングや事前に設定していた悪質なボットにどう立ち向かうのかがこちらです。

sequenceDiagram autonumber participant Attacker as 攻撃者 / 悪質ボット participant Apache443 as Apache (ポート443) participant Trap as トラップエリア (/nepenthes) alt パターンA:特定の動的ページへの大量タグ攻撃 Attacker->>Apache443: /search/tags に大量のカンマを含むクエリでアクセス Note over Apache443: bad_tag_stacking 検知 Note over Apache443: E=dontlog:1 (ログを残さない) Apache443-->>Attacker: 404 Not Found (虚無へ葬る) else パターンB:ブラックリスト(blacklist-bots.txt / spam-ips.txt)に該当 Attacker->>Apache443: スパムIPや悪質Botからのアクセス Note over Apache443: Require not env bad_bot で拒否 Note over Apache443: ErrorDocument 403 /404.html Apache443-->>Attacker: 404 Not Found (403を隠蔽して最初から何も無いと騙す) else パターンC:ウツボカズラ(Nepenthes)トラップへの盲目スキャン Attacker->>Apache443: /wp-login.php や /.git へのアクセス Note over Apache443: mod_alias でダミーファイルへ転送 Apache443->>Trap: dummy_login.html などを読み込み Trap-->>Apache443: ダミーデータ Apache443-->>Attacker: 200 OK (ダミーファイルを返して時間を稼ぐ) else パターンD:凶悪なIPリスト(negativelist.txt)に該当 [WAF制御] Attacker->>Apache443: 凶悪リストに載っているIPからのアクセス Note over Apache443: ModSecurity: 'Damnatio Memoriae' 発動 Note over Apache443: nolog, noauditlog (全てのログ・監査ログから存在を抹消) Apache443-->>Attacker: 404 Not Found (記憶抹消刑) end

と、このように、

「“アクセス”は通す
 “攻撃”は阻止する
 ミドルウェアで“両方”やるというのは
 そう難しいことじゃあないな」

と言えるのが「個人VPSでApacheを使う理由」と言えます。

次は、それらを可能にする仕組みについて解説します。

NginxとApacheの違い(と筆者がApacheを使う理由)

Webサーバの中核ミドルウェアは

  • Apache
  • Nginx

の二つですが、近年のトレンドは間違いなくNginx。

Nginxはなぜここまでもてはやされるのかを軽くひもといて見ます。

Nginxがもてはやされる理由

Nginx(エンジンエックス)が現在、Webサーバーのシェアでトップ争いをするほどもてはやされ、多くの開発者や企業に支持されているのには明確な理由があります。

主な意見やメリットをWeb上の情報からまとめると、理由は大きく分けて以下の4点に集約されます。

1. 「C10K問題」を解決する圧倒的な同時接続処理能力

かつて主流だったWebサーバー(Apacheなど)は、アクセス(リクエスト)ごとに「プロセス」や「スレッド」を立ち上げて処理する方式でした。そのため、同時に大量のアクセスが来るとサーバーのメモリを使い果たしてしまい、サーバーがダウンしたり極端に遅くなったりする「C10K問題(クライアント1万台問題)」が発生していました。

これに対し、Nginxは「イベント駆動型(非同期シングルスレッド)」というアーキテクチャを採用しています。

  • 1つのプロセスで、大量のリクエストをイベントとして次々と効率よく処理します。
  • アクセスが急増してもメモリの消費量がほとんど増えないため、「大量の同時接続があっても、少ないメモリで高速に処理を維持できる」という最大の強みを持っています。

2. 静的コンテンツの配信が劇的に速い

HTMLファイル、画像、CSS、JavaScriptなどの「静的コンテンツ」をユーザーに返すスピードが圧倒的です。
ベンチマークによっては、従来のWebサーバーと比べて数倍〜十数倍以上のパフォーマンスを発揮すると言われており、リソースの消費を抑えながらWebサイトの表示速度を大幅に向上させることができます。

3. リバースプロキシ・ロードバランサーとしての優秀さ

Nginxは単なるWebサーバーとしてだけでなく、

  • リバースプロキシ(中継サーバー)」
  • ロードバランサー(負荷分散装置)

としての機能が非常に優れています。

現代のWebシステムでは、以下のような「役割分担」をする構成(構成例を以下に示します)がトレンドとなっています。

sequenceDiagram autonumber participant User as ユーザー<br/> (大量のアクセス) participant Nginx as Nginx<br/>(リバースプロキシ) participant App as アプリサーバー<br/>(Node.js / PHP等) User->>Nginx: リクエスト送信 alt 静的ファイルの場合 Nginx-->>User: 静的ファイルを自分で高速に返す else 動的処理の場合 Nginx->>App: 処理を丸投げ (プロキシ) App-->>Nginx: 処理結果を返却 Nginx-->>User: レスポンスを返却 end

動的な処理(WordPressや各種Webアプリなどの重い処理)は後ろのアプリケーションサーバーに任せ、Nginxは「表に立って大量のアクセスを交通整理する」という役割を担うことで、システム全体の安定性と速度を極限まで高めることができます。

4. 設定ファイルがシンプルで扱いやすい

長年の歴史を持つApacheなどは、多機能である反面、設定ファイルが複雑化しがちでした。
一方、Nginxの設定は記述ルールがスマートで、直感的で分かりやすい構成になっています。軽量でコンテナ(Dockerなど)との相性も抜群に良いため、現代のクラウドネイティブな開発環境において非常に好まれています。

では、なぜ筆者はApacheを使っているのか?

と、ここまで書いたらメリットしかないように感じられるNginxですが、

  • 小規模サイト
  • がっつりカスタマイズしたい

方にはApacheの方が優れているケースが多々あります。

「単一VPS(リソースが限られている)での個人利用」という条件に絞ると、実はApacheを選ぶ明確なメリット(むしろApacheの方が楽で優れている点)があります。

なぜ筆者のように単一VPSの個人利用ではApacheが有利になるのか、主な理由を3つに分けて解説します。

1. .htaccess (include)による手軽さと柔軟性

個人利用、特に1台のVPSでブログやポートフォリオなど複数のサイトを試行錯誤しながら運用する場合、Apacheの .htaccess(分散設定ファイル)や include(内部ファイル) は圧倒的に便利です。

  • ディレクトリ単位で設定できる:
    • リダイレクト(転送)の設定、Basic認証(パスワード制限)、アクセス拒否などを、特定のフォルダ内に .htaccessinclude(内部ファイル)ファイルを置くだけで即座に反映できます。
    • Apacheの .htaccess(分散設定ファイル)や、設定を細かく分割して管理できる Include 機能は圧倒的に便利です。
  • Nginxの場合:
    • 全て中央の設定ファイル(nginx.conf)に記述する必要があり、変更のたびにサーバーの再起動(リロード)が必要です。記述を間違えるとサーバー全体が落ちるリスクがあります。

2. WordPressなどの動的サイト(PHP)との相性が抜群

個人VPSでよく使われるWordPress(PHP)を動かす場合、Apacheの方が構築が圧倒的にシンプルです。

  • mod_php による一体型処理:
    • Apacheはサーバー内部にPHPを組み込んで(モジュールとして)動かすことができるため、Apacheをインストールするだけで、PHPがそのままサクサク動きます。
    • ちなみに筆者は、さらなるパフォーマンス最適化のためにApache+PHP-FPMという『いいとこ取り』の構成で運用していますが、こうした高度な組み合わせへの柔軟性が高いのもApacheの魅力です。
  • Nginxの場合:
    • Nginx単体ではPHPを動かせないため、必ず「PHP-FPM」という別の仕組みと連携させる必要があります。この連携設定(Unixソケットやポートの設定)が初心者にはやや難解で、トラブルシューティングのハードルが上がります。

3. Web上の「知見(情報量)」が圧倒的に多い

歴史が長いApacheは、ネット上にあるトラブルシューティングや逆引きの設定情報が膨大です。

  • コピペで動く情報が多い:
    • 特にWordPressのプラグイン(セキュリティ系やキャッシュ系、SEO系)の多くは、「インストール時に .htaccess を自動書き換えする」 仕様になっています。Apacheであればプラグインを入れるだけで勝手に設定が完了します。
  • Nginxの場合:
    • プラグインが自動生成した .htaccess の記述(Apache用)を、自分でNginx用の設定構文に翻訳して nginx.conf に手動で追記しなければならないケースが多々あります。

単一VPSにおける「性能差」の現実

「でも、Nginxの方が軽いし速いのでは?」と思われるかもしれませんが、「個人利用のアクセス規模」であれば、体感できるほどの差はまず出ません。

  • 同時アクセスが数十〜数百程度なら:
    • Apacheでもメモリは十分に足りますし、ページ表示速度もほぼ変わりません。
  • むしろメモリ消費:
    • Nginx+PHP-FPMの構成は、プロセス管理を適切に設定しないと、限られたVPSのメモリをじわじわと圧迫することがあります。設定の手間を考えると、Apacheの方が「ほったらかし」で安定して動きやすいです。

筆者がApacheを選ぶ最大の理由

  • mod_rewrite
  • mod_security

が余りにも強いから、に尽きます。

1. mod_rewrite という「最強のアーミーナイフ」

mod_rewrite は、URLの書き換えやリダイレクトを自由自在に行える、Apache史上最も強力なモジュールのひとつです。個人VPSでWebサイトを運営する際、これほど頼りになるツールはありません。

  • 正規表現による超柔軟なコントロール:
    • 「特定のIPアドレス以外からのアクセスを全てメンテナンス画面に飛ばす」
    • 「スマホからのアクセスだけ専用ページにリダイレクトする」
    • 「拡張子なしのURLを裏側でPHPにマッピングする(美しいURLの実現)」
      といった複雑な処理が、数行の記述で完結します。
  • .htaccess(inlude) との相乗効果:
    • これを各ディレクトリの .htaccess に書けるため、Webサーバー全体の設定を汚すことなく、Webアプリケーション側(WordPressなど)が自身をコントロールするためにフル活用できます。
  • Nginxの場合:
    • Nginxにも rewrite 指令はありますが、Apacheの mod_rewrite ほど複雑な条件分岐(複数条件の組み合わせや、ファイル・ディレクトリの存在チェックを挟んだ高度なルーティング)をスマートに書くのが難しく、設定が肥大化・複雑化しやすいという弱点があります。

2. mod_security(WAF)との「歴史的・機能的な相性の良さ」

個人VPSは常に世界中からのサイバー攻撃(ブルートフォースアタック、SQLインジェクション、クロスサイトスクリプティングなど)に晒されるため、オープンソースのWAF(Webアプリケーションファイアウォール)である mod_security の存在は非常に大きいです。

  • Apacheが「本家」という安心感:
    • もともと mod_security はApacheのモジュールとして開発された歴史があるため、Apacheとの親和性は抜群です。インストールも容易で、挙動も非常に安定しています。
  • OWASP CRS(コアルールセット)の運用が楽:
    • セキュリティの標準ルールであるOWASP CRSなどを導入する際、Apacheであればドキュメントや実績が豊富にあるため、個人でも比較的迷わずに導入・チューニング(誤検知の修正など)が可能です。
  • Nginxの場合:
    • Nginxで mod_security(最新はv3)を動かすには、多くの場合ソースコードからモジュールを自分でコンパイル(ビルド)して組み込む必要があり、バージョンアップのたびにメンテナンスの手間が発生するなど、個人運用のハードルがかなり高くなります。

結論:個人VPSにおけるApacheは「要塞であり、何でも屋」

Nginxが「大量の荷物を最速で右から左へ捌くプロフェッショナル」だとすれば、
Apacheは「どんな要求にもその場で柔軟に応え、自分で自分の身を守る武装も完璧な万能戦士」です。

個人VPSという「限られた1台の環境」だからこそ、

  • mod_security でガッチリ身を守り、
  • mod_rewrite で複雑なURL設計をスマートにこなし、
  • .htaccess で手軽に設定を変える

というApacheの特性が120%活きます。「もてはやされているから」という理由だけでNginxを選ぶと、このあたりの「痒いところに手が届く便利さ」を全て手動の泥臭い設定(あるいはコンパイル作業)で補うことになり、途中で挫折してしまうケースも少なくありません。

では次回の記事から、筆者が実際にVPSで運用している「mod_rewrite による超柔軟なURL制御」と「mod_security による鉄壁の防御設定」の具体例を、コードを交えてご紹介します

改良版:Linuxサーバのプロセスを見るためのスクリプト。

自分のサーバでその場で障害状況を見るための道具を洗練させています。

#!/bin/bash

# --- カラー定義 ---
GREEN='\033[0;32m'  # 緑:正常・低負荷
RED='\033[0;31m'    # 赤:高負荷・警告
BLUE='\033[0;34m'   # 青:ヘッダ・情報
YELLOW='\033[1;33m' # 黄:中負荷・注意
NC='\033[0m'        
BOLD='\033[1m'

# --- デフォルト設定 ---
TOP_N=10
MODE="all"

# --- ヘルプ表示 ---
usage() {
    echo -e "${BLUE}Usage: $0 [-c] [-m] [-n NUM] [-i]${NC}"
    echo "  -c    : CPU使用率順で表示"
    echo "  -m    : メモリ使用率順で表示"
    echo "  -n    : 表示行数 (Default: ${TOP_N})"
    echo "  -i    : 対話式モード (Interactive)"
    exit 0
}

# --- 対話モード (Interactive Mode) ---
interactive_mode() {
    echo -e "${BLUE}${BOLD}=== 対話設定モード ===${NC}"
    
    # モード選択
    echo -e "表示モードを選択してください:"
    echo "  1) 全て表示 (デフォルト: Enter)"
    echo "  2) CPUのみ"
    echo "  3) メモリのみ"
    read -r -p "選択 [1-3]: " mode_choice
    case "$mode_choice" in
        2) MODE="cpu" ;;
        3) MODE="mem" ;;
        *) MODE="all" ;;
    esac

    # 行数指定
    read -r -p "表示行数を入力してください (デフォルト: 10): " input_n
    if [[ "$input_n" =~ ^[0-9]+$ ]]; then
        TOP_N="$input_n"
    else
        TOP_N=10
    fi
    echo ""
}

# --- CPU情報の表示 ---
show_cpu_info() {
    echo -e "${BLUE}${BOLD}--- 💻 CPU Information ---${NC}"
    if [ -f /proc/cpuinfo ]; then
        # メーカー名 / モデル名
        local model_name
        model_name=$(grep -m 1 "model name" /proc/cpuinfo | cut -d: -f2 | sed 's/^[ \t]*//')
        # 物理コア数 (無い場合はソケット数等からフォールバック)
        local cores
        cores=$(grep -c "^processor" /proc/cpuinfo)
        # 周波数 (MHz から GHz に変換して見やすく)
        local cpu_mhz
        cpu_mhz=$(grep -m 1 "cpu MHz" /proc/cpuinfo | cut -d: -f2 | sed 's/^[ \t]*//')
        
        echo -e "  ${YELLOW}Model:${NC} ${model_name:-Unknown}"
        if [ -n "$cpu_mhz" ]; then
            # 簡易的にGHz変換(小数点以下2桁)
            local cpu_ghz
            cpu_ghz=$(awk -v mhz="$cpu_mhz" 'BEGIN {printf "%.2f", mhz/1000}')
            echo -e "  ${YELLOW}Speed:${NC} ${cpu_ghz} GHz (${cpu_mhz} MHz)"
        fi
        echo -e "  ${YELLOW}Cores:${NC} ${cores} threads"
    else
        echo -e "${RED}/proc/cpuinfo が見つかりません。${NC}"
    fi
    echo ""
}

# --- Memory & zRAM ---
show_memory_status() {
    echo -e "${BLUE}${BOLD}--- 🧠 Memory & zRAM Status ---${NC}"
    
    # free -h の表示
    echo -e "${YELLOW}[Physical Memory]${NC}"
    free -h | awk 'NR==1{print "              " $0} NR>1{print $0}'
    
    echo ""
    # zramctl の表示
    if command -v zramctl > /dev/null; then
        echo -e "${YELLOW}[zRAM Compression Status]${NC}"
        zramctl --output-all
    else
        echo -e "${RED}zramctlはこの環境にはありません。${NC}"
    fi
    echo ""
}

# --- ディスク使用量 (Storage Status) ---
show_disk_status() {
    echo -e "${BLUE}${BOLD}--- 💾 Disk Usage Status ---${NC}"
    # 主要なファイルシステム(tmpfs等を除く実ディスク)を抽出して表示
    df -h -x tmpfs -x devtmpfs -x squashfs 2>/dev/null || df -h
    echo ""
}

# --- プロセス監視 (Process Tracking) ---
show_top() {
    local sort_key="$1"
    local title="$2"
    
    echo -e "${BLUE}${BOLD}--- ${title} (Top ${TOP_N}) ---${NC}"
    
    # Header (見出し列の幅を調整してズレを防止)
    printf "${BLUE}%-6s %-6s %-8s %-12s %-20s %s${NC}\n" "%CPU" "%MEM" "PID" "USER" "UNIT" "COMMAND"
    echo "------------------------------------------------------------------------------------------"

    # ps コマンド。head/tailのロジックを整理し、空文字による awk の挙動エラーを防止
    ps -e -o pcpu,pmem,pid,user,unit:20,args --sort="${sort_key}" | \
    tail -n +2 | head -n "$TOP_N" | \
    awk -v red="$RED" -v green="$GREEN" -v yellow="$YELLOW" -v nc="$NC" '
    {
        cpu=$1; mem=$2; pid=$3; user=$4; unit=$5;
        
        # カラー判定
        color=nc;
        if (cpu > 50.0 || mem > 50.0) color=red;
        else if (cpu > 10.0 || mem > 10.0) color=yellow;
        else color=green;

        # 6列目以降のコマンド引数を結合
        cmd=""; for(i=6;i<=NF;i++) cmd=cmd" "$i;
        # 先頭の余分なスペースを削除
        sub(/^ /, "", cmd);
        
        printf "%s%-6s %-6s %-8s %-12s %-20s %-50s%s\n", color, cpu, mem, pid, user, unit, substr(cmd,1,50), nc
    }'
}

# --- 引数解析 ---
# オプション指定がない場合はデフォルトで対話モードを起動
if [ $# -eq 0 ]; then
    interactive_mode
else
    while getopts "cmn:ih" opt; do
        case $opt in
            c) MODE="cpu" ;;
            m) MODE="mem" ;;
            n) TOP_N="$OPTARG" ;;
            i) interactive_mode ;;
            h|*) usage ;;
        esac
    done
fi

# --- 実行 ---
clear
echo -e "${GREEN}監視を開始します。${NC}\n"

# システム情報の表示 (常に表示)
show_cpu_info
show_memory_status
show_disk_status

# プロセス情報の表示
case $MODE in
    cpu) show_top "-pcpu" "CPU Consumers" ;;
    mem) show_top "-pmem" "Memory Consumers" ;;
    all) 
        show_top "-pcpu" "CPU Consumers"
        echo ""
        show_top "-pmem" "Memory Consumers"
        ;;
esac

echo -e "\n${GREEN}プロセスが表示されました${NC}"

全体の動きのシナリオ

  1. 初期設定: 出力する色や、デフォルトの表示件数(10件)などを決めます。
  2. 引数のチェック(モード切り替え): * スクリプト実行時にオプション(-c-m など)が指定された場合は、その設定に従います。
  • 何も指定せず実行した場合は「対話モード」が起動し、「どの情報を」「何件表示するか」を画面上で質問されます。
  1. 画面のクリア: ターミナルを一度綺麗にしてから測定結果を表示します。
  2. ハードウェア情報の表示: CPUの型番やコア数、メモリの使用量、ディスクの空き容量を順番に表示します。
  3. 高負荷プロセスの表示: 設定されたモード(CPU順、メモリ順、または両方)に合わせて、リソースを多く消費しているプロセスをランキング形式で色付き表示します。

📦 各関数の役割と解説

1. usage(ヘルプ表示)

使い方を間違えた時や、-h オプションを付けた時に、コマンドのオプション一覧を表示してスクリプトを終了します。

2. interactive_mode(対話設定モード)

引数なしで実行された時に呼ばれます。read コマンドを使ってユーザーに入力を促し、表示したいモード(全て/CPUのみ/メモリのみ)や、ランキングを何件表示するか(デフォルト10件)を動的に決定します。

3. show_cpu_info(CPU情報の表示)

Linuxのシステムファイル /proc/cpuinfo を読み込んで解析します。

  • CPUのモデル名、スレッド数(コア数)を取得します。
  • 周波数(MHz)を取得し、awk を使って計算し GHz 単位に変換して見やすく表示します。

4. show_memory_status(メモリとzRAMの状況)

  • free -h コマンドで、人間が見やすい単位(GBやMB)で物理メモリの空き状況を表示します。
  • zramctl コマンドがある環境(メモリ圧縮技術 zRAM が有効な環境)では、その圧縮ステータスも同時に表示します。

5. show_disk_status(ディスク使用量)

  • df -h コマンドでストレージの残量を表示します。
  • その際、-x tmpfs などを指定することで、メモリ上の仮想ファイルシステムを除外した「実際の物理ディスク(SSDやHDD)だけ」を狙って表示する工夫がされています。

6. show_top(プロセス監視のコア処理)

  • ps コマンドで現在動いているプロセスの一覧を、指定されたリソース(CPUまたはメモリ)の消費量が多い順に並び替えて取得します。
  • tailhead を使って、指定された件数(デフォルト10件)だけを切り出します。
  • awk を使ってデータを1行ずつ処理し、CPUまたはメモリの使用率が50%を超えていれば「赤」、10%を超えていれば「黄」、それ以下なら「緑」に文字色をリアルタイムに変化させて出力します。

スクリプトの使い方

実行する際のオプションによって、動きを切り替えることができます。

オプション実行例動き
(なし)./script.sh対話モード。画面の指示に従って入力する。
-c./script.sh -cCPU使用率の高いプロセスをトップ10で表示する。
-m./script.sh -mメモリ使用率の高いプロセスをトップ10で表示する。
-n [数値]./script.sh -c -n 5CPUの高いプロセスを上位5件だけ表示する。
-h./script.sh -hヘルプ(使い方の説明)を表示する。

LinuxサーバーやPCの健康状態を、コマンド一つでパッと色鮮やかに確認できる非常に実用的かつ便利なものに仕上げた自負があります。

BBC Newsの見出しを表示するシェルスクリプトの改修(メニューつき)

BBC NewsのRSSフィードを取得するスクリプトを改修しました。

改修した点

  • -m フラグの処理:
    • 引数に -m が指定された場合、インタラクティブモード(対話式)の関数を呼び出します。
  • セクションの番号選択:
    • 統合された全セクションを1から順にナンバリングして表示し、ユーザーが数字で選べるようにしました。
  • 件数の入力:
    • デフォルト値を提示しつつ、エンターを押すだけでデフォルト値(3件)が適用される親切設計にしています。

改修版スクリプト

#!/bin/bash

# デフォルト値の設定
default_section="world"
default_count=3

# メインセクションのリスト
main_sections=("world" "uk" "business" "politics" "health" "education" "science_and_environment" "technology" "entertainment_and_arts")

# グローバルセクションのリスト
global_sections=("africa" "asia" "europe" "latin_america" "middle_east" "us_and_canada")

# 全セクションのリストを統合
all_sections=("${main_sections[@]}" "${global_sections[@]}")

# --- [新規] 対話式メニュー関数 ---
interactive_menu() {
    echo "=== BBC News RSS セクション選択 ==="
    # セクション一覧を番号付きで表示
    for i in "${!all_sections[@]}"; do
        printf "%2d) %s\n" "$((i+1))" "${all_sections[$i]}"
    done

    # セクションの入力受付
    while true; do
        read -p "セクションの番号を選択してください (1-${#all_sections[@]}): " sec_num
        if [[ "$sec_num" =~ ^[0-9]+$ ]] && [ "$sec_num" -ge 1 ] && [ "$sec_num" -le "${#all_sections[@]}" ]; then
            section="${all_sections[$((sec_num-1))]}"
            break
        else
            echo "無効な入力です。正しい番号を入力してください。"
        fi
    done

    # 件数の入力受付
    read -p "表示する件数を入力してください (デフォルト: ${default_count}): " input_count
    if [[ "$input_count" =~ ^[0-9]+$ ]] && [ "$input_count" -gt 0 ]; then
        count=$input_count
    else
        count=$default_count
    fi
    echo "--------------------------------------------------"
}

# --- 引数の処理 ---
if [[ "$1" == "-m" ]]; then
    # 対話モードの起動
    interactive_menu
elif [[ "$1" =~ ^[0-9]+$ ]]; then
    section=$default_section
    count=$1
else
    section=${1:-$default_section} # 引数1が指定されていない場合はデフォルト値を使用
    count=${2:-$default_count}     # 引数2が指定されていない場合はデフォルト値を使用
fi

# 引数の短縮形を対応する正式名に変換 (通常モード用)
case "$section" in
    "usa" | "n-usa") section="us_and_canada" ;;
    "me") section="middle_east" ;;
    "latam" | "la") section="latin_america" ;;
    "eu") section="europe" ;;
    "science") section="science_and_environment" ;;
    "entertainment") section="entertainment_and_arts" ;;
    *) section=$section ;; # その他はそのまま
esac

# セクションの検証
if [[ ! " ${all_sections[@]} " =~ " ${section} " ]]; then
    echo "Error: Invalid section '${section}'. Valid sections are: ${all_sections[*]}"
    exit 1
fi

# URLの構築
if [[ " ${main_sections[@]} " =~ " ${section} " ]]; then
    url="https://feeds.bbci.co.uk/news/${section}/rss.xml"
else
    url="https://feeds.bbci.co.uk/news/world/${section}/rss.xml"
fi

# 最初に一度だけRSSフィードをダウンロードし、変数に格納する
xml_content=$(curl -s "$url")

# コンテンツが取得できなかった場合はエラー終了
if [ -z "$xml_content" ]; then
    echo "Error: No headlines found for section '${section}'. Please check the section name or try again later."
    exit 1
fi

# フィードの最終更新日時を取得し、フォーマットする
feed_date_raw=$(echo "$xml_content" | xmllint --xpath "string(//channel/lastBuildDate)" - 2>/dev/null)
if [ -n "$feed_date_raw" ]; then
    # JSTに変換して表示フォーマットを整える
    feed_date_formatted=$(date -d "$feed_date_raw" '+%Y/%m/%d %H:%M:%S %Z')
fi

# 見出しを取得
headlines=$(echo "$xml_content" | xmllint --xpath "//item/title/text()" - 2>/dev/null | sed -e 's/<!\[CDATA\[//g' -e 's/\]\]>//g' | head -n "$count")

# 見出しの表示
echo "BBC News - ${section} section (${count} headlines)"
# 取得した日付を表示
if [ -n "$feed_date_formatted" ]; then
    echo "As of: ${feed_date_formatted}"
fi
echo "--------------------------------------------------" #区切り線
echo "$headlines"

使い方

  1. 通常モード(従来通り)

引数をそのまま渡せば、セクションごとに件数を表示してくれます。

./bbc_headline.sh business 5
  1. 対話式メニューモード(新機能)

-m を付けて実行します。

./bbc_headline.sh -m

実行イメージ:

=== BBC News RSS セクション選択 ==
 1) world
 2) uk
...
15) us_and_canada
セクションの番号を選択してください (1-15): 3
表示する件数を入力してください (デフォルト: 3): 5
--------------------------------------------------
BBC News - business section (5 headlines)
...

graph TD A("スクリプト開始") --> B{"引数の判定"} %% 引数の分岐 B -->|"-m"| C["対話式メニュー関数 interactive_menu"] B -->|"数字"| D["セクション: デフォルト<br>件数: 引数1の数字"] B -->|"その他"| E["セクション: 引数1<br>件数: 引数2"] %% 対話モードの詳細 subgraph "対話モードの内部処理" C --> C1["セクション一覧を番号付きで表示"] C1 --> C2["ユーザーが番号を入力"] C2 --> C3{"有効な番号か?"} C3 -->|"No"| C2 C3 -->|"Yes"| C4["セクション名を確定"] C4 --> C5["ユーザーが表示件数を入力"] C5 --> C6{"数字が入力されたか?"} C6 -->|"Yes"| C7["入力された件数を採用"] C6 -->|"No"| C8["デフォルトの3件を採用"] end %% 合流後の共通処理 C7 --> F["短縮名の変換ケース文"] C8 --> F D --> F E --> F F --> G{"有効なセクション名か?"} G -->|"No"| H("エラーを表示して終了") G -->|"Yes"| I{"メインかグローバルか?"} %% URL構築 I -->|"メイン"| J["URL: news/セクション/rss.xml"] I -->|"グローバル"| K["URL: news/world/セクション/rss.xml"] %% 取得と出力 J --> L["curl で RSS XML をダウンロード"] K --> L L --> M{"データが空でないか?"} M -->|"No"| N("エラーを表示して終了") M -->|"Yes"| O["xmllint で最終更新日時を取得・JST変換"] O --> P["xmllint と head で見出しを抽出"] P --> Q["結果を画面に出力"] Q --> R("スクリプト終了") %% スタイルの調整 style C fill:#f9f,stroke:#333,stroke-width:2px style C1 fill:#fff2cc,stroke:#d6b656 style C2 fill:#fff2cc,stroke:#d6b656 style C5 fill:#fff2cc,stroke:#d6b656

このスクリプトは役に立つのか?

私が便利だと思ったから乗せています。実際、/etc/update-motdに仕込むことで、「今はこういう出来事が起こっている」をリアルタイムで知ることができるからです。

改良版・SSLの有効期限をチェックするRubyスクリプト。

サイトの命綱であるSSL証明書を簡単にチェックするRubyスクリプトを更に改良しました。

スクリプト内容

このRubyスクリプトの動きを解説してください

#!/usr/bin/env ruby

require 'openssl'
require 'socket'
require 'date'
require 'uri'
require 'timeout'
require 'net/http'

# ANSIカラーコード
COLORS = {
  red:    "\e[31m",
  yellow: "\e[33m",
  green:  "\e[32m",
  reset:  "\e[0m"
}.freeze

# 1. 引数のURL/ドメインを適切にパースするメソッド
def parse_to_uri(input)
  clean_input = input.strip
  url = clean_input.match?(%r{\Ahttps?://}) ? clean_input : "https://#{clean_input}"
  URI.parse(url)
rescue URI::InvalidURIError
  nil
end

# 2. リダイレクトを追跡して最終的なURIを返すメソッド
def fetch_effective_uri(uri, limit = 5)
  return nil if limit == 0

  response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.open_timeout = 5
    http.read_timeout = 5
    http.head(uri.path.empty? ? '/' : uri.path)
  end

  case response
  when Net::HTTPRedirection
    redirect_uri = URI.join(uri, response['location'])
    fetch_effective_uri(redirect_uri, limit - 1)
  else
    uri
  end
rescue => e
  nil
end

# 3. 証明書の有効期限を取得するメソッド(エラー箇所を修正)
def fetch_certificate_expiry(uri)
  Timeout.timeout(5) do
    # ポートが明示されていない場合は、スキーマがhttpsなら443、それ以外なら80にする
    port = uri.port || (uri.scheme == 'https' ? 443 : 80)

    # TCPSocket.open はブロックを渡すと自動クローズしてくれます
    TCPSocket.open(uri.host, port) do |tcp_socket|
      ctx = OpenSSL::SSL::SSLContext.new
      
      # .open ではなく .new を使用し、ブロックの代わりに ensure でクローズする形に修正
      ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
      ssl_socket.hostname = uri.host
      
      begin
        ssl_socket.connect
        cert = ssl_socket.peer_cert
        
        expiration_date = cert.not_after.to_date
        days_remaining = (expiration_date - Date.today).to_i

        [expiration_date, days_remaining]
      ensure
        ssl_socket.close rescue nil
      end
    end
  end
rescue Timeout::Error
  [nil, "サーバーへの接続がタイムアウトしました。"]
rescue => e
  [nil, e.message]
end

# 4. 結果を表示するメソッド
def print_result(uri, expiration_date, days_remaining)
  if expiration_date
    formatted_date = expiration_date.strftime("%Y/%m/%d")
    
    color = case days_remaining
            when ...14 then COLORS[:red]
            when ...30 then COLORS[:yellow]
            else            COLORS[:green]
            end

    puts "サイト #{uri} の有効期限は #{formatted_date} です。#{color}残り #{days_remaining} 日です。#{COLORS[:reset]}"
  else
    puts "#{COLORS[:red]}サイト #{uri} の証明書取得に失敗しました: #{days_remaining}#{COLORS[:reset]}"
  end
end

# メイン処理
def main
  inputs = ARGV.empty? ? [print("チェックしたいサイト(ドメイン/URL)を入力してください: "), gets].last.to_s : ARGV

  inputs.each do |input|
    next if input.strip.empty?

    uri = parse_to_uri(input)
    if uri.nil?
      puts "#{COLORS[:red]}無効な入力です: #{input}#{COLORS[:reset]}"
      next
    end

    puts "Checking: #{uri} ..."
    
    final_uri = fetch_effective_uri(uri)

    if final_uri.nil?
      puts "#{COLORS[:red]}サイト #{uri} にアクセスできませんでした。#{COLORS[:reset]}"
      next
    end

    unless final_uri.scheme == 'https'
      puts "#{COLORS[:yellow]}最終遷移先がHTTPSではないため、スキップします: #{final_uri}#{COLORS[:reset]}"
      next
    end

    expiration_date, days_remaining = fetch_certificate_expiry(final_uri)
    print_result(final_uri, expiration_date, days_remaining)
  end
end

main if __FILE__ == $PROGRAM_NAME

スクリプトの使い方

このスクリプトを実行するには、Rubyがインストールされた環境(MacのターミナルやLinux、Windowsのコマンドプロンプトなど)が必要です。

ステップ1: ファイルに保存する

上記のコードをコピーし、任意の場所にファイルとして保存します。

  • ファイル名(例): check_cert.rb

ステップ2: スクリプトを実行する

使い方は「引数でまとめて指定する」か「実行後に入力する」の2パターンあります。

  • パターンA:引数でドメインを指定して実行(おすすめ)

調べたいサイトをスペース区切りで並べて一気に実行できます。URLでもドメインだけでもOKです。

ruby check_cert.rb google.com github.com https://rubygems.org
  • パターンB:対話形式で実行

引数を何も渡さずに実行すると、画面上で入力を求められます。

ruby check_cert.rb

実行すると チェックしたいサイト(ドメイン/URL)を入力してください: と表示されるので、そこに yahoo.co.jp などを入力してEnterを押します。

実際の動き(出力のイメージ)

スクリプトを実行すると、内部で通信が行われ、結果がターミナルに以下のように表示されます。残り日数に応じて自動で文字に色がつくため、一目で状況がわかります。

Checking: https://google.com ...
サイト https://google.com の有効期限は 2026/07/15 です。残り 50 日です。(←緑色で表示)

Checking: https://example.com ...
サイト https://example.com の有効期限は 2026/06/05 です。残り 10 日です。(←赤色で表示)

Checking: http://http-only-site.com ...
最終遷移先がHTTPSではないため、スキップします: http://http-only-site.com (←黄色で表示)

Checking: https://invalid-domain-xyz.com ...
サイト https://invalid-domain-xyz.com の証明書取得に失敗しました: Failed to open TCP connection to invalid-domain-xyz.com:443(←赤色で表示)

エラーハンドリングの動き

  • リダイレクトがある場合:
    • http://github.com と入力しても、自動的に https://github.com へ転送(リダイレクト)されたことを検知し、最終的なHTTPSのページに対して証明書をチェックします。
  • 接続できない場合:
    • ドメインが間違っていたり、サーバーがダウンしている場合は、プログラムがクラッシュせずに「Failed to open TCP connection(接続に失敗しました)」とエラー理由を教えてくれます。

まとめというか本題

筆者はLinuxターミナルの/etc/update-motd.dに仕込むことで自サイトの証明書更新のタイミングを計っています。何せ、Let's Encryptはわずか90日。更に短くなるというのもほぼ確定しましていますから。

そして、筆者が、ここまで執拗にこれをチェックするのは「私怨」が本質。

自分をかなり追い詰めていき、体と心を大きく崩してしまった前の職場。その元職場が

  • 2020年代であるにもかかわらず常時SSL化していない

という、IT系会社にあるまじき状態を鼻で笑うためというのもこのスクリプト作成のきっかけ。

  • しかも、最近、屋台骨であるWebサービスの証明書が失効してタイムアウトになっている

という更にお察し状態なのはまた別のお話。

ONE OUTS システム番外:AI時代の下品なスクレイパー「Paprika」から、3ステップでWebサイトを護るApache防衛のケーススタディ。

概要:新型AI自動化プラットフォーム「Paprika」とは?

現Xで見かけてしまったツール、Paprika

Paprikaは、分散ワーカー上のChrome(実ブラウザ)をPlaywright経由で操作し、LLM/Vision(AI)を使ってページ内の画像・動画・構造化データを根こそぎ剥ぎ取る、極めて執拗な自動化プラットフォームです。

なぜ「趣味が悪い」のか:

  • 境界線の蹂躙:
    • ログイン必須サイトや年齢確認、JavaScript描画など、管理者が明示的に引いた「機械的な巡回を拒む壁」を、クッキー偽装やセッション維持で強引に突破することを目指している点。
  • 品性のない執着:
    • CSSの変更によるクローラー避けが効かない。AIが「人間の目で画面を見て」ボタンを探し、クリックしてくるため、これまでの構造的な防護策を無効化しようとする点。
  • 他者リソースへの強欲な寄生:
    • scroll=True で遅延ロードを発火させ、画像や動画ストリームを「丸ごと一括ダウンロード」するため、サーバーの帯域やCPU(コスト)に莫大な負荷をかける点。
  • リーガルリスク: 「正当な目的のための自動化」を気取っている連中であるため、下手に通信を拒否(403等)したり速度制限をかけたりすると、「正当なアクセスを妨害された」などと言いがかり(難癖)をつけてくる厚顔無恥なリスク.

悪用できる建前

仕様書には「利用規約の遵守」や「正当な目的での利用」と美しく免責事項が書かれていますが、提供されている機能はあきらかに「大量かつ高速なコンテンツのブッコ抜き」を目的としています。この「建前と本音の圧倒的なギャップ」が「趣味が悪い」と断じた理由です。

一般的なクローラー(Googlebotなど)は、robots.txt のルールを守り、正体を名乗って巡回します。
しかしPaprikaは、JavaScriptの完全実行、遅延ロード(スクロール動作)への追従、Cookieの永続化による会員限定ページの突破を平然と行います。

サイト側が「毎日ボタンの配置(CSS)を変える」といったボット対策をしても、AIエージェントがそれを学習して乗り越えてきます。さらに、分散IPで「一見、たくさんの一般ユーザーが同時にアクセスしてきた」ように見せかけるため、従来のWAFやIP制限が非常に効きにくいのが最大の問題です。

AIエージェントが画面スクロールや walk(サイト内巡回)を繰り返すことで、一般ユーザーの快適な閲覧環境を圧迫する、一種のDoS状態のツールです。

3. ここからサイトを護るための「Apacheの防衛」

以下、筆者環境です。

  • Apache
  • mod_rewrite
  • Ubuntu 24.04

の2つがあれば基本的には対処可能です。ApacheのMod_rewriteは、アーミーナイフのような問題です。

sudo a2enmod rewrite
sudo systemctl reload apache2.service

ステップ1:User-Agentによる水際対策(デフォルトを即座に切って捨てる)

彼らがもしデフォルトの名称(PaprikaやPlaywright)をUser-Agentに残して突っ込んできた場合、もっとも軽量な処理で済みます。

apacheの.confファイルに以下を突っ込んでおきます。

# ─── 層1: 既知のAI自動化ツール・ライブラリのUAを拒否 ───
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTP_USER_AGENT} (paprika|Playwright|Stagehand|Browser-Use|Browserable) [NC]
    RewriteRule ^ - [F,L]
</IfModule>

ステップ2:利用規約(ポリシー)の明文化(法的な盾)

「言いがかり」を完全に無力化するため、サイトのフッター等に「AIエージェントや実ブラウザ偽装による一括ダウンロードをDoS行為とみなし、検知した場合は即座にアクセス制限を適用する」旨を記載しておきましょう。

そもそも、上記のDoSツールを使ってくるものが「正常な閲覧者」である理由はありません。

ステップ3:robots.txtを逆手に取った「404ハニートラップ」

  • robots.txt に罠を仕掛ける:

まともなクローラーは rogotx.txt に書かれたDisallow を守ります。しかし、こんな輩がこれを律儀に守るということはまずありません。

そこで、それを逆手に取り、品性のないAIエージェントだけを炙り出すための罠のパスを設定します。

robots.txtに以下のような罠を設けておきます。

User-agent: *
Disallow: /assets-archive/

悪意あるクローラーは「robots.txtにわざわざ書くということは、ここに大事な情報があるに違いない」と判断します。

  • Apache(.htaccess または .conf)に罠の行き先を刻む:

踏んだクローラーに対し、403(拒否)ではなく「404(存在しない)」を返すことで、「そんなものはありません」と先んじておきます。

# robots.txtを無視して歩き回る(walk)AIエージェントへの罠 
<IfModule mod_rewrite.c>
    RewriteEngine On
    # robots.txtで禁止した領域に足を踏み入れた者は、一律で404(Not Found)
    RewriteRule ^assets-archive/?$ - [R=404,L]
</IfModule>

4. まとめ

「正当な目的」を謳えば他人のリソースを奪えばいいというツール。

崇高な目的とやらにDoSツールをばらまくというその厚顔無恥ぶりは『メリー・ポピンズ』の

Not at all attractive to my way of thinking

この言葉を借りるまでもなく悪趣味の一言。

Web管理者側もただ怯えるのではなく、相手の仕様の「執着」を逆手に取り、エレガントに虚無(404)へ引きずり込む防衛術がお役に立てれば幸いです。

firewalldのゾーンと設定方法のケーススタディ

RHEL系Linuxに備わっているfirewalld。非常に柔軟で(比較的)直感的に使える仕組みだったのでメモを残します。

特に驚きだったのがzoneの概念。

ufwが基本的に「システム全体に対してポートを開けるか閉じるか」をシンプルに管理するのに対し、firewalldは「接続するネットワークの信頼度に応じて、ファイアウォールのルールを瞬時に切り替える」という柔軟性がありました。

そもそもfirewalldにおけるゾーンとは?

一言で言うと、ゾーンとは「接続元のネットワークやインターフェース(LANカードなど)の『信頼度』に応じたグループ分け」のことです。

従来の iptables などでは、「このIPアドレスからのこのポートへの通信を許可する」といった細かいルールを1つずつ書く必要があります。

一方、firewalld では以下のようなステップで考えます。

  1. あらかじめ「拒否」「自宅用」「パブリック(公共)」といった、ルールの異なる箱(ゾーン)を用意しておく。
  2. ネットワークインターフェース(例: eth0)や、特定のIPアドレスをその箱に割り当てます。

これにより、「カフェのWi-Fiに繋いだ時は『パブリック』ゾーンに切り替える」「会社のLANに繋いだ時は『社内』ゾーンに切り替える」といった管理が、一瞬でできるようになります。

代表的なプリセットゾーン

firewalld には、最初からいくつかのゾーンが用意されています。

ゾーン名信頼度主な用途・特徴
drop最低すべての受信パッケージを破棄します(応答すら返さない)。こちらからの送信は可能です。
blockすべての受信を拒否します。drop と違い、相手に「拒否しました」という通知(ICMP)を返します。
public低〜中デフォルトのゾーン。 不特定多数がいる公共のネットワーク用。自分が許可した通信(sshなど)だけを通します。
externalルーターとして使う場合の「外側(インターネット側)」用。マスカレード(NAT)が有効になります。
**home / internal**自宅や社内LANなど、周囲のコンピューターを信頼できる場合用。お互いの通信が少し緩く許可されています。
trusted最高すべてを許可します。完全に安全だと分かっているネットワーク専用です。

ゾーンの3つの重要なルール

インターフェースは必ずどこかのゾーンに属する

ネットワークカード(eth0wlan0 など)は、必ずいずれか1つのゾーンに紐付けられます。何も設定していない場合、自動的に public(デフォルトゾーン)に属します。

IPアドレス単位での割り当ても可能

「インターフェース全体は public だけど、上司のPCのIPアドレス(192.168.1.50)だけは trusted ゾーンとして扱う」といった柔軟な設定が可能です。

ルールはゾーンごとに設定する

Webサーバー(80番ポート)を開放したい」となったら、「public ゾーンに対して80番ポートを許可する」というように、ゾーンに対して設定を紐付けます。

もっと有り体に言うと

『ジョジョの奇妙な冒険』第5部における『マン・イン・ザ・ミラー』です。

ここには『スタンド力』は おれの許可なくしては入る事はできない
『おまえ本体』だけ入る事を許可した
ここにある物は全て命のない『物質』だけだ―――おまえとオレだけ!他に『生きてる物』はいない………

という「許可」と「許可しない」を、鏡の世界ではなく「ゾーン」ごとに決められる能力、と言っていいでしょう。

よく使う基本コマンド

ゾーンの状態を確認・操作するための、代表的な firewall-cmd コマンドです。

現在のデフォルトゾーンを確認する

firewall-cmd --get-default-zone

すべてのゾーンの設定を確認する

firewall-cmd --list-all-zones

特定のインターフェース(例: eth0)のゾーンを変更する

firewall-cmd --zone=home --change-interface=eth0

特定のゾーン(例: public)にサービス(例: http)を許可する

 firewall-cmd --zone=public --add-service=http --permanent

`

  • --permanent をつけた後は、設定を反映させるために以下が必要です
firewall-cmd --reload

Linuxサーバーを構築する際は、まず「このサーバーはどこに置かれていて、どのゾーンを適用すべきか」を考えていきましょう。

そのケーススタディを行っていきます。

ケーススタディ

各部署が持っているサーバを管理するためシステム部が一括でzabbixのエージェントを入れたいという状況。

  • システム部だけにzabbixサーバへのssh接続(ターミナル操作)と管理画面(Web管理画面)の閲覧と操作を許可
  • それ以外の部署が所属するNWには上記2つを拒否。
  • zabbixサーバそのものを司る大本のzabbixサーバはエージェントの通信を許可。
  • ただし、zabbixのエージェントは通るようにします。

Step 1: システム部専用ゾーンの作成と「IP」の紐付け

まずは 192.168.1.0/24 だけが所属する専用のゾーン illuso を作ります。

  • ゾーンの新規作成
sudo firewall-cmd --permanent --new-zone=illuso
  • 一度リロードして、OSに新しいゾーンを認識させる(重要)
sudo firewall-cmd --reload

作成したゾーンに「部内NWのIPセグメント」を紐付ける

sudo firewall-cmd --permanent --zone=illuso --add-source=192.168.1.0/24

Step 2: システム部専用ゾーンに「許可サービス」を追加

Step 1 で作ったゾーンに、ssh, http, https の鍵(許可)を配置します。

  • システム部専用ゾーンに対して SSH を許可
sudo firewall-cmd --permanent --zone=illuso --add-service=ssh
  • システム部専用ゾーンに対して HTTP を許可
sudo firewall-cmd --permanent --zone=illuso --add-service=http
  • システム部専用ゾーンに対して HTTPS を許可
sudo firewall-cmd --permanent --zone=illuso --add-service=https

Step 3: 上位監視サーバー専用のゾーン作成と設定

社内のZabbixサーバー(192.168.12.6)だけが所属する man-in-the-mirror を作り、Zabbix Agent用の 10050 ポートを許可します。

  • 上位サーバー用のゾーンを新規作成
sudo firewall-cmd --permanent --new-zone=man-in-the-mirror
  • 再度リロードして、新しいゾーンを認識させる
sudo firewall-cmd --reload
  • そのゾーンに「上位ZabbixのIP」を紐付ける
sudo firewall-cmd --permanent --zone=man-in-the-mirror --add-source=192.168.12.6
  • そのゾーンに 10050 ポート(tcp)の許可を与える
sudo firewall-cmd --permanent --zone=man-in-the-mirror --add-port=10050/tcp

Step 4: 全員に開くポートの追加 と public の掃除

誰からでも受け付ける Zabbix Server(10051)や SMTP(25)を public に設定し、同時に、先ほど部内限定へお引越しさせた不要なサービス(ssh, http, https)を public から削除します。

  • 自身の Zabbix Server ポート(10051)を全員に開放
sudo firewall-cmd --permanent --zone=public --add-port=10051/tcp
  • SMTP(25)を全員に開放
sudo firewall-cmd --permanent --zone=public --add-service=smtp
  • 【とても重要】どこからでも SSH できる状態を public から削除
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
  • どこからでも HTTP できる状態を public から削除
sudo firewall-cmd --permanent --zone=public --remove-service=http
  • HTTPS も public から削除
sudo firewall-cmd --permanent --zone=public --remove-service=https

Step 5: 設定の最終反映と確認

ここまでの --permanent(次回起動用の設定)を、一気に本番環境へ反映(リロード)させます。

  • すべての設定を反映させます。
sudo firewall-cmd --reload

反映が完了したら、正しく設定できているか各ゾーンを覗いてみましょう。

  • 部内限定ゾーンの確認(sourcesにIP、servicesにssh http httpsがあること)
sudo firewall-cmd --zone=illuso --list-all
  • 上位Zabbix用ゾーンの確認(sourceにIP、portsに10050/tcpがあること)
sudo firewall-cmd --zone=man-in-the-mirror --list-all
  • 共通ゾーンの確認(servicesからssh, http, httpsが消え、zabbix-serverやsmtp、portsに10051/tcpがあること)
sudo firewall-cmd --zone=public --list-all

まとめ

以上、手順は多いものの

  • こいつは許可
  • こいつは拒否

を極めて柔軟に行えるのはRHEL系Linuxの持つ特権だと思いました。とはいえ、

この条件は「これから入ろうとする邪魔者」を拒否するには有効ですが、入ってしまったものを取り除くのは極めて厄介です。とくにウィルス(マルウェア)の除去は

『マン・イン・ザ・ミラー』オレだけが外に出る事を許可しろォォォォーーーーッ
うおおおががががが だが! ウイルスは許可しないィィィィィーーーッ
感染した部分は出る事は 許可しないィィィィィィィーーーッ!!

とはならないので注意が必要です。

(最後のこれが言いたいだけのエントリーを書き終えました)

Ubuntu24.04にfail2banの条件緩和。(ヘビーユースのWebサーバの問題点)

昨日設定したfailbanとの連携。ufw.aggressive。

結論から言うと「あまりにも閾値が低すぎて自分自身がロックアウトを喰らう」結果になりました。

何が起きたのか?

自分のIPからのアクセスが全てのサービスにつながらなくなった。

これはSSH接続はのみならずWeb閲覧でも弾かれるを意味します。

取り急ぎ、「確実にignoreipされている」場所からアクセスし、

sudo fail2ban-client status ufw

をしたところ、ものの見事にアクセスしていたIPアドレスが含まれています。

そのため、

sudo fail2ban-client set ufw unbaip IPアドレス

として条件を解除。

そんな中で見つけた挙動。Redminetneは比較的単純な通信が発生するため、

[ufw]
enabled = true
filter = ufw-aggressive
action = iptables-allports
logpath = /var/log/ufw.log
maxretry = 1
bantime = -1
# ignoreipには、自分自身のローカルホストと、巻き添えを防ぎたい大手検索エンジン(Googlebotなど)のIP帯を指定します

では、大量のretryが発生し、fail2banはご丁寧に「こいつは大量アクセスを繰り返している」としてban。更に、当然ながら、iptables-allportsが含まれているため、自分自身が締め出しを食らったという次第。

対処

「閾値の緩和」に尽きます。「人間の通常の作業は問題ないがbotが引っかかる」を目安に修正しました。この時の注意点は 対象サイトに過度にアクセスしないことに尽きます。iptablesと手を組んでいる以上、無効の怒りを買わないよう、接続はSSHのみにとどめます。

修正ファイル /etc/fail2ban/jail.local

[ufw]
enabled = true
filter = ufw-aggressive
action = iptables-allports
logpath = /var/log/ufw.log
# ↓ 1 から 30 〜 50 程度に大幅緩和(ボットは防げて人間は誤検知しない絶妙なライン)
maxretry = 50
# ↓ 10分〜15分(600〜900秒)の間に規定回数叩いたらアウトにする
findtime = 900
bantime = -1
# ignoreipは、自分が今アクセスしているIPを真っ先に入れます
ignoreip = 127.0.0.0/8 ::1 

設定後、

sudo fail2ban-client reload

を実行して設定完了。

設定終わって

  • Growi
  • Nextcloud

を操作して通常通りの作業が行えることを確認。

まとめ

今回、慌てずに作業できていたのが「どこからアクセスすれば安全か」を知っていたこと。

「頑固で融通が利かない門番」

を意図通りに動かすためには、門番が動く条件をきちっと動かす必要があるというお話でした。

Page 1 of 39

Powered by WordPress & Theme by Anders Norén