ApacheのVirtualHost設定で嵌った話し

先日、Apacheを使ってTLS・SSLに対応したバーチャルホストを構築していてど嵌まりしたことについて書いてみました。
そもそもの発端はApacheでバーチャルホストを構築する際の設定は何処に書くべきなのか？なのだけど、古い(例えばApache 2.2系列時代の)記事では`/etc/httpd/conf/httpd.conf`やTLS・SSL通信に対応するなら`/etc/httpd/conf.d/ssl.conf`を修正するなり、設定を追加するなりして自分のホスト環境に合わせての設定を行っている記事が多かったように思います。
しかし最近のApache 2.4系列のApacheでは、直接`/etc/httpd/conf/httpd.conf`や`/etc/httpd/conf.d/ssl.conf`を修正するするのではなく、`/etc/httpd/conf.d/`の下に新しく自分のホスト(WEBサイト)向けの設定ファイルを新たに作りここに設定を書き込むことで設定を行うことが主流になっているようです。
で、今回も新しいWEBサイトを設定するにあたり`/etc/httpd/conf/httpd.conf`の下に`[ドメイン名].conf`ファイルを作り、そこで以下のような設定を行いApacheを起動してWEBブラウザからサイトにアクセスしてみましたが、TLS・SSL接続を使わずに`http://[ドメイン名]`でアクセスするとちゃんと想定通りの内容が表示されるのだけど、何故か設定がTLS・SSL接続(`https://[ドメイン名]`)でアクセスするとデフォルトのApacheテストページが表示されてしまうし、TLS・SSL証明書も`/etc/httpd/conf.d/ssl.conf`で指定されている自動生成されるダミーの自己証明書が使われてしまうと言う謎の現象に遭遇しました。

`/etc/httpd/conf.d/`の下に追加して作った設定ファイルの内容
```config:webserver.example.com.conf
<VirtualHost *:80>
    ServerName webserver.example.com
    DocumentRoot /var/www/webserver.example.com/public_html
    ErrorLog /var/log/httpd/webserver.example.com-error.log
    CustomLog /var/log/httpd/webserver.example.com.log combined
</VirtualHost>

<VirtualHost *:443>
    ServerName webserver.example.com
    DocumentRoot /var/www/webserver.example.com/public_html
    ErrorLog /var/log/httpd/webserver.example.com-ssl_error.log
    CustomLog /var/log/httpd/webserver.example.com.ssl_log combined

    SSLCertificateFile /etc/letsencrypt/live/webserver.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/webserver.example.com/privkey.pem
    Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
```

「あれ？変だな〜」と思い、まず試してみたのは自分が運用している他のバーチャルホストをサポートする環境に同じ設定を作り、そちらにDNSの向き先を変えて動かしてみた。
でも特に同じような問題は発生せずに`http://webserver.example.com/`でもリアルなホスト名でもアクセスができる。
次にチェックしたのがmod_sslをインストールしたときに同時にインストールされた`/etc/httpd/conf.d/ssl.conf`の設定。
`/etc/httpd/conf.d/ssl.conf`の中には以下のようにすでにデフォルトのバーチャルホストの設定が書かれている。

`/etc/httpd/conf.d/ssl.conf`の40行目あたり
```config:/etc/httpd/conf.d/ssl.conf
##
## SSL Virtual Host Context
##

<VirtualHost _default_:443>

# General setup for the virtual host, inherited from global configuration
#DocumentRoot "/var/www/html"
#ServerName www.example.com:443
```
あ〜、そうか！ここでデフォルト(`_defallt_`)のバーチャルホストの設定がされているから、別の設定ファイルでバーチャルホストの設定をしても、そのホスト名(`ServerName`や`ServerAlias`)が実ホスト名にマッチしちゃうとこちらが優先されちゃうのか！
と気がついて、Apacheを稼働させているホストの名前を`webserver.example.com`から、`vhost.example.com`とか別の名前に変えてみた。
しかし、それでも結果は変わらない。
そこで思いついたのは、もしかしてTLS・SSL証明書のコモンネーム(`CN`)がマッチする証明書を使う設定があるとそれが優先されるのかな？と…

で、`openssl`コマンドで証明書の内容をチェックしてみた。
```bash
openssl x509 -text -in /etc/pki/tls/certs/localhost.crt
```

```console
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 475065158036033417 (0x697c53e85e70f89)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Unspecified, OU=ca-2753923304029444439, CN=webserver.example.com, emailAddress=root@webserver.example.com
        Validity
            Not Before: Feb 21 06:28:31 2025 GMT
            Not After : Feb 21 06:28:31 2026 GMT
        Subject: C=US, O=Unspecified, CN=webserver.example.com, emailAddress=root@webserver.example.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:bf:28:8b:f3:c1:eb:02:38:4f:1c:05:7f:a0:4f:
```
あれ？証明書のコモンネーム(`CN`)hが変わっていない？
ちょっと調べてみるとこのダミーの証明書はApache起動時に`/usr/libexec/httpd-ssl-gencerts`スクリプトで自動生成され、ダミーの証明書がすでに生成済みな場合には再生成はされずに、過去に生成されたものをそのまま使うらしいので、まずは過去に生成されたダミーの証明書を削除してApacheを再起動してみた。

ダミーのTLS・SSL証明書の削除と再生成
```bash
sudo rm /etc/pki/tls/certs/localhost.crt
sudo rm /etc/pki/tls/private/localhost.key
sudo systemctl restart httpd
```

もう一度`openssl`コマンドで証明書の内容をチェックしてみた。
ダミーの証明書のコモンネーム(`CN`)は設定しなおしたホスト名('vhost.example.com')に変わっていたので、WEBブラウザで`https://webserver.example.com/`へとアクセスしてみる。

やっぱり表示されるのは変わらずにApacheのテストページ

opensslでリモートから`https://webserver.example.com/`へとアクセスして証明書をチェックしてみるとやはりダミーの証明書(コモンネームは'vhost.example.com'に変わっている)が使われているので、Apacheの設定ファイルとしては`/etc/httpd/conf.d/ssl.conf`が使われているらしい。

```bash
openssl s_client -connect webserver.example.com:443 -servername webserver.example.com
```

```
Connecting to nnn.nnn.nnn.nnn
CONNECTED(00000005)
depth=1 C=US, O=Unspecified, OU=ca-5015121004957820975, CN=vhost.example.com, emailAddress=root@vhost.example.com
verify error:num=19:self-signed certificate in certificate chain
verify return:1
depth=1 C=US, O=Unspecified, OU=ca-5015121004957820975, CN=vhost.example.com, emailAddress=root@vhost.example.com
verify return:1
depth=0 C=US, O=Unspecified, CN=vhost.example.com, emailAddress=root@vhost.example.com
verify return:1
---
Certificate chain
 0 s:C=US, O=Unspecified, CN=vhost.example.com, emailAddress=root@vhost.example.com
   i:C=US, O=Unspecified, OU=ca-5015121004957820975, CN=vhost.example.com, emailAddress=root@vhost.example.com
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Feb 25 05:47:07 2025 GMT; NotAfter: Feb 25 05:47:07 2026 GMT
 1 s:C=US, O=Unspecified, OU=ca-5015121004957820975, CN=vhost.example.com, emailAddress=root@vhost.example.com
   i:C=US, O=Unspecified, OU=ca-5015121004957820975, CN=vhost.example.com, emailAddress=root@vhost.example.com
   a:PKEY: rsaEncryption, 4096 (bit); sigalg: RSA-SHA256
   v:NotBefore: Feb 25 05:47:07 2025 GMT; NotAfter: Feb 25 05:47:07 2026 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIIPZ3D9IPUdvQwDQYJKoZIhvcNAQELBQAwgYkxCzAJBgNV
BAYTAlVTMRQwEgYDVQQKDAtVbnNwZWNpZmllZDEfMB0GA1UECwwWY2EtNTAxNTEy
```

クライアントからアクセスするのに使ったホスト名(`-servername`)は`webserver.example.com`で、証明書のコモンネーム(`CN`)は`vhost.example.com`で、ましてや自己証明書なので、当然検証の結果はエラー(`Verify return code: 19 (self-signed certificate in certificate chain)`)
```
    00d0 - 88 55 85 af 9a 0e 37 4f-43 9e f4 3b 6f 33 82 39   .U....7OC..;o3.9
    00e0 - ef db 42 4e 9b 55 00 b1-90 8f 1b bc 9a 2b 95 5e   ..BN.U.......+.^

    Start Time: 1740462620
    Timeout   : 7200 (sec)
    Verify return code: 19 (self-signed certificate in certificate chain)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
closed
```

ちなみに、`hostname`コマンドで問い合わせても、念の為に`/etc/hostname`ファイルを確認してみてもApacheを動作させているホストのホスト名は`vhost.example.com`となっているし、ダミーのTLS・SSL証明書のコモンネーム(`CN`)は`vhost.example.com`だし、ではApacheは何処で`_default_`にマッチするホスト名として`webserver.example.com`を設定してしまったのだろう？ということで、次の手段として`httpd`を`-S`オプションを付けて起動して、設定ファイルの読み込みと設定の経過を観察してみた。

```bash
/usr/sbin/httpd -S
```
```console
VirtualHost configuration:
*:80                   webserver.example.com (/etc/httpd/conf.d/webserver.example.com.conf:1)
*:443                  is a NameVirtualHost
         default server webserver.example.com (/etc/httpd/conf.d/ssl.conf:40)
         port 443 namevhost webserver.example.com (/etc/httpd/conf.d/ssl.conf:40)
         port 443 namevhost webserver.example.com (/etc/httpd/conf.d/webserver.example.com.conf:8)
ServerRoot: "/etc/httpd"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/etc/httpd/logs/error_log"
```
すると何故か？`/etc/httpd/conf.d/ssl.conf`がデフォルトのホスト名(`_default_`)として認識しているホスホネームが`webserver.example.com`となっている。
`grep`を使い`grep -lr "webserver.example.com" /etc`とかやって文字列`webserver.example.com`を探しても何処にも無いし、このホスト名は一体どこから持ってきた？と思ったら、思わぬところに伏兵が！
今回のWEBサーバー構築にはさくらインターネットの「さくらのVPS」を使ったので、「さくらのVPS」ではちゃんとVPSホストに割り当てられたIPアドレスに対してDNSの逆引きのホスト名を設定できるので、これを`webserver.example.com`に設定していたのだが、どうやらApacheは設定ファイルを読み込んでのデフォルトのバーチャルホストのホスト名(`_default_`)には、ホストのIPアドレスから逆引きして得られたホスト名を使うらしい。

と言うことは！
もしかして、DNSの逆引きホスト名を`webserver.example.com`じゃないホスト名に変えてしまうか、さくらインターネットが決めた元のホスト名に戻してしまえば上手く動くんじゃないの？
と思ってやってみました。

```
dig -x nnn.nnn.nnn.nnn
```
```
; <<>> DiG 9.16.23-RH <<>> -x nnn.nnn.nnn.nnn
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46072
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;nnn.nnn.nnn.nnn.in-addr.arpa.	IN	PTR

;; ANSWER SECTION:
nnn.nnn.nnn.nnn.in-addr.arpa. 3600 IN	PTR	wwwXXXXue.sakura.ne.jp.

;; Query time: 511 msec
;; SERVER: 210.224.163.3#53(210.224.163.3)
;; WHEN: Wed Feb 26 16:53:40 JST 2025
;; MSG SIZE  rcvd: 91
```

案の定！、DNSの逆引きホスト名を元に戻したら`httpd -S`で確認したデフォルトサーバー(`default server`)のホスト名がさくらインターネットで定義されたホスト名に変わっています。
※この時、WEBサーバーのホストの`hostname`コマンドや`/etc/hostname`の設定は`webserver.example.com`のままです。

```bash
/usr/sbin/httpd -S
```
```console
VirtualHost configuration:
*:80                   webserver.example.com (/etc/httpd/conf.d/webserver.example.com.conf:1)
*:443                  is a NameVirtualHost
         default server wwwXXXXue.sakura.ne.jp (/etc/httpd/conf.d/ssl.conf:40)
         port 443 namevhost wwwXXXXue.sakura.ne.jp (/etc/httpd/conf.d/ssl.conf:40)
         port 443 namevhost webserver.example.com (/etc/httpd/conf.d/webserver.example.com.conf:8)
ServerRoot: "/etc/httpd"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/etc/httpd/logs/error_log"
```

この状態でWEBブラウザなどのクライアントアプリから`https://webserver.example.com/`へとアクセスすれば期待通りのバーチャルホスト設定`/etc/httpd/conf.d/webserver.example.com.conf`が使われるはずです。

試しに`openssl`を使ってWEBサーバーへと接続して想定どおりの証明書が使われているかを確認してみます。
```bash
openssl s_client -connect webserver.example.com443 -servername webserver.example.com
```
```console
Connecting to nnn.nnn.nnn.nnn
CONNECTED(00000005)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R11
verify return:1
depth=0 CN=example.com
verify return:1
---
Certificate chain
 0 s:CN=example.com
   i:C=US, O=Let's Encrypt, CN=R11
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Feb 15 11:46:07 2025 GMT; NotAfter: May 16 11:46:06 2025 GMT
 1 s:C=US, O=Let's Encrypt, CN=R11
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFMzCCBBugAwIBAgISA5P3+oblpLleVpcbszj2CTdmMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
```
この証明書では`CN=`が`example.com`となっていますが、これはここで使った証明書がワイルドカード証明書で、マッチするドメイン名は証明書のSAN(Subject Alternative Name)で指定されています。
例えば、この証明書が置かれたホスト上で以下のように証明書の内容を確認することで、SANでどのようなホスト名をマッチさせているかを確認することができます。
```bash
openssl x509 -text -in /etc/letsencrypt/live/example.com/cert.pem
```
```console
            X509v3 Subject Alternative Name:
                DNS:*.example.com, DNS:example.com
```

ちゃんと最後のベリファイ結果もOKとなっているようです。
```console
    00d0 - 79 f3 2a bf 8f 9f 06 2c-23 a8 9f 92 b2 67 ee be   y.*....,#....g..
    00e0 - 6a fd 60 ab 2b 90 ab b2-b5 52 33 4f 1f e8 61 a7   j.`.+....R3O..a.

    Start Time: 1740557291
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
closed
```

ここまで散々と弄って何なのだけど、さらに色々と試しているうちにもっと単純なルールを発見！
実は！(も何も、当たり前と言われれば当たり前なのだけど…)諸々の設定は早い者勝ち的に先に定義されたものが有効になるようなので、`/etc/httpd/conf.d/`の中から読み込まれる設定ファイルの順番をファイル名を工夫することで調整してssl.confよりも前に追加したバーチャルホストの設定ファイル(今回の例では`/etc/httpd/conf.d/webserver.example.com.conf`)が読み込まれるようにすればこれが有効になるので全て解決です。

この`/etc/httpd/conf.d/`ディレクトリの中から設定ファイルが読み込まれる順番はディレクトリを`ls`コマンドでリストした際の表示順(アルファベット順)で読み込まれます。

例えば今回の例では`/etc/httpd/conf.d/`ディレクトリを`ls`コマンドでリストすると
(`-1`オプションはファイル名を1行に1件表示するオプションです)
```bash
ls -1 /etc/httpd/conf.d/*.conf
```
```console
/etc/httpd/conf.d/autoindex.conf
/etc/httpd/conf.d/ssl.conf
/etc/httpd/conf.d/userdir.conf
/etc/httpd/conf.d/webserver.example.com.conf
/etc/httpd/conf.d/welcome.conf
```
のように`ssl.conf`が`webserver.example.com.conf`より先に読み込まれることが判ります。

これを例えば以下のようにファイル`webserver.example.com.conf`の前に文字列`00_`を付けることで`00_webserver.example.com.conf`を`ssl.conf`より先に読み込ませることができます。
```bash
ls -1 /etc/httpd/conf.d/*.conf
```
```console
/etc/httpd/conf.d/00_webserver.example.com.conf
/etc/httpd/conf.d/autoindex.conf
/etc/httpd/conf.d/ssl.conf
/etc/httpd/conf.d/userdir.conf
/etc/httpd/conf.d/welcome.conf
```

その様子を`httpd -S`で確認しても先にバーチャルホスト`webserver.example.com`の設定として`/etc/httpd/conf.d/00_webserver.example.com.conf`が使われているのが判ります。
```bash
/usr/sbin/httpd -S
```
```console
VirtualHost configuration:
*:80                   webserver.example.com (/etc/httpd/conf.d/00_webserver.example.com.conf:1)
*:443                  is a NameVirtualHost
         default server webserver.example.com (/etc/httpd/conf.d/00_webserver.example.com.conf:8)
         port 443 namevhost webserver.example.com (/etc/httpd/conf.d/00_webserver.example.com.conf:8)
         port 443 namevhost webserver.example.com (/etc/httpd/conf.d/ssl.conf:40)
ServerRoot: "/etc/httpd"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/etc/httpd/logs/error_log"
```

でも、このディレクトリの中の設定ファイルの読み込み順で動作が変わっちゃうやつに関しては、大抵の場合はここでの対処のように最初から(パッケージ化する時点で！)`nn_ファイル名`みたいになっていて、それを見てみんな「ああそうか！そんな感じでファイル名には注意が必要なのか！」と気がつくんだと思うんで「先にそれを言ってよ〜」な感じはする…