Webアプリエンジニアがドメイン取得し、DNSとCDNの設定をやってみた

## はじめに

普段はWebアプリケーションエンジニアとして、ほぼインフラはノータッチだった。ただ、Webの根幹であるネットワークの基礎の基礎（TCP/IP、DNSなど）を学ぶ中で、多少なりともネットワーク関連の簡単な仕組みや設定を理解できるようになると面白いのではと思った。

そこで今回はAWS EC2上に立てたWebサーバをオリジンサーバとして、以下のことをやってみようと思う。

1. 独自のドメインを取得しDNSの設定する
1. CDNを設定し、アクセスをCDN経由にする

## AWS EC2のWebサーバについて

EC2上に立てたApacheサーバを利用する。
EC2のインスタンスは、VPC内のパブリックサブネット（インターネットゲートウェイを設定）に所属し、EC2はパブリックIPを持つ（Elastic IPではないが今回のやりたいことには問題ない）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/e6132aa4-77a6-3bf3-af63-62f43fa0582e.png)

## 1. 独自のドメインを取得しDNSの設定する

今回は [お名前.com](https://www.onamae.com/) を利用して無料のドメインを取得する。

:::note info
whoisについて
`whois`でドメインの所有者の個人情報が見れるが、お名前.comであればこれを代行してくれるので個人のプライバシーの観点でも安心（[Whois情報公開代行](https://www.onamae.com/service/d-regist/option.html?btn_id=02_top_usedomainarea_whois_20201002)を参照）。
:::

:::note info
DNSレコードの設定前は何が表示されるか？
お名前.comを始めとしてドメイン登録サービスは、DNSのAレコードなどが未設定である場合、以下のようなデフォルトのページが開かれるようになっているみたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/28df171b-2928-b14d-1700-d4173fc4d0e8.png)
:::

ドメインの取得方法は別の記事等に譲るとして、ドメインの取得ができたら管理ページからネームサーバをお名前.comのネームサーバ（01.dnsv.jpなど）に設定を変える（これをしないとDNSレコードの設定ができない）。

続いて、Aレコードを以下のように設定する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/f621d64a-9f44-29a2-4cec-d68b53075277.png)

あとはnslookup等でIPアドレスがEC2ものになっているか、また実際にブラウザから意図したページが見れるか？などを確認してみればいい。
```console
[ec2-user@ip-10-0-1-10 ~]$ nslookup <登録したドメイン>.com
Server:         10.0.0.2
Address:        10.0.0.2#53

Non-authoritative answer:
Name:   <登録したドメイン>.com
Address: <EC2のIPアドレス>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7690cc2e-dd1d-0f69-5d98-6ed5f98565c5.png)

上記の通り、意図したページが見られるようになった。なお、SSLサーバ証明はまだ設定していないのでHTTPで通信している。

※ちなみに、今回はAレコードでIPアドレスを指定したが、CNAMEレコードでも同じように今回やりたいことは実現できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/25489bb5-af83-f139-d6fc-65365c4cb2c5.png)
```
$ nslookup www.<登録したドメイン>.com
Non-authoritative answer:
Server:  UnKnown
Address:  <自分のIP>

Name:    ec2-<IPアドレスのハイフン区切り>.ap-northeast-1.compute.amazonaws.com
Address:  <IPアドレス>
Aliases:  www.<登録したドメイン>.com
```

ただし、DNSの仕様ではCNAMEレコードをゾーンのAPEX（ルートドメイン、つまり example.com のようなドメインの最上位）に配置することはできないので、wwwなどのサブドメインに対してのみCNAMEレコードを設定することになる。そのため、www.<登録したドメイン>.comという形式になっている。
が、Cloudflareであれば[CNAME flattening](https://developers.cloudflare.com/dns/cname-flattening/)という仕組みがあり、以下のような設定が可能になるらしい。
```
<登録したドメイン>.com.　  IN   CNAME   ec2-<IPアドレスのハイフン区切り>.ap-northeast-1.compute.amazonaws.com
```

### 余談として

今回、お名前.comでドメインを取得し、DNSのAレコードを設定することで、ドメインの名前解決が行われてEC2のサーバにリクエストが届くようになったが、この作業の裏のDNS周りの設定がどういう風になっているか自身の備忘録として書き出してみたいと思う。

まず、ドメインの登録で何が起きるかだが、これはドメイン名前空間の`com`ドメインにサブドメインを追加することをしている。
サブドメインの追加は、

1. `com`ドメインに登録したいサブドメインのネームサーバを構築し、そのゾーン情報を作る
1. `com`ドメインのゾーンに、登録ドメインのネームサーバを指し示すNSレコードを登録する（権限委譲してもらう）

の2つを行う。
具体的に見ていくほうがわかりやすいので、`com`に`example`というサブドメインを追加するを例に、具体的に話を進める。

まずは`example`に対してオーソリティを持つ（ゾーンを管理する）ネームサーバを構築する。
ネームサーバもサーバでインターネット上でアクセスされるので、グローバルIPを持ち、さらにドメインを設定できる。今回は適当に以下とする。

- グローバルIP：10.20.30.40
- ドメイン：ns（つまり、ネームサーバは`ns.example.com.`というFQDNになる）

普通は`example.com`でアクセスしたサーバがあるのでAレコードなどのゾーン情報を作るが、一旦それはskipする。

続いて、`com`ドメインのゾーン情報にexampleネームサーバの情報を登録してもらう。
イメージとしては以下のようなデータになる（BINDフォーマット）。
```
…

; Delegation for example.com.
example.com.     IN  NS   ns.example.com.
ns.example.com.  IN  A    10.20.30.40
```

NSレコードで`example`のネームサーバを指定し、AレコードでネームサーバのIPを設定する（これが権限委譲で、comが持つオーソリティをexampleに委譲している）。
これにより、`example.com.`の名前解決をするときには、

- `com.`のネームサーバに問い合わせが来る
- example.comは`ns.example.com.`のネームサーバがオーソリティを持つからそのネームサーバに聞いて、という返答
- `ns.example.com.`に問い合わせが来て、Aレコードなどを応答

という流れになり、インターネット上で`example.com`のサーバの場所（＝IPアドレス）がわかるようになる。

そして、最後にAレコードを設定して、`exampl.com.`が名前解決されるようにするが、`example`にサブドメインを追加したり、ルート（`example.com.`）にIPアドレスを割り当てるなどの操作は、exampleがオーソリティを持つので自分自身で勝手にできる（DNSは分散型のシステムというのはこういうこと）。

操作としてはオーソリティを持つネームサーバの設定をいじるので、最終的には図示すると以下のような状態になる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/52e142be-a29a-636b-0fa0-1140970322c9.png)

## 2. CDNを設定し、アクセスをCDN経由にする

続いて、CDN経由でオリジンサーバにアクセスするようにCDNの設定をしていきたいと思う。今回はCloudflareを利用する（無料である程度のことができるので）。

Cloudflareの設定は公式の[Get started with Cloudflare](https://developers.cloudflare.com/learning-paths/get-started/)を参照（ここでは抜粋してやっていく）。

Cloudflareのダッシュボード上でお名前.comで取得したドメインを「サイトを追加」から登録する。途中でお名前.comの管理ページ上でネームサーバをCloudflareのものに更新する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/ef3e17e2-17e5-ea51-123e-efc5c8cdac49.png)

ネームサーバをCloudflareのものに更新してしばらく（2分くらいだった）すると、以下のようにアクティブになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/384b0760-ada8-0b7d-9f8d-733f1d42ac13.png)

アクティブになった後で`nslookup`をしてみると、以下のようにオリジンサーバーの情報ではなくCloudflareのCDNのIPアドレスが返答されるようになる。
```console
$ nslookup www.<登録したドメイン>.com
Non-authoritative answer:
Server:  UnKnown
Address:  <自分のIP>

Name:    www.<登録したドメイン>.com
Addresses:  172.67.201.235
          104.21.76.226
```

これでブラウザからのリクエストがCloudflareのCDNを経由しオリジンサーバーに到達するようになった。ブラウザからアクセスしてみても、Cloudflareの設定をする前と同じように閲覧できることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c8f260c2-1cc2-4b7a-5f6a-9d6098cc6d49.png)

※1点、上記の画像を見ればわかるが、Cloudflareを導入することでブラウザとCDNの間はCloudflareのエッジ証明書のおかげでSSL/TLSで暗号化されるのでHTTPSでの通信ができるようになる。詳細は「余談として」で触れる。

- 参考文献
  - [How Cloudflare works](https://developers.cloudflare.com/learning-paths/get-started/concepts/how-cloudflare-works/)

### 余談として

#### CloudflareのProxy statusについて

[Proxy status](https://developers.cloudflare.com/dns/manage-dns-records/reference/proxied-dns-records/)に詳細が書かれているが、以下の画像のようにプロキシステータスが「プロキシ済み」になっていないと、Cloudflare経由でのオリジンサーバーにアクセスする設定にならない。「DNS のみ」にすると、単に名前解決されるだけなのでオリジンサーバーの情報がばれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9681e05c-d15f-9149-ef71-c5b5a31a3335.png)

#### CloudflareのSSL/TLSについて

Cloudflareを利用すると、CDNまでの経路はデフォルトでSSL/TLSで暗号化されるように設定される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/6849a29f-437d-4392-9ccd-ed99a52d3ffa.png)

ただ、Cloudflareとオリジンサーバー間はHTTPで通信できる状態になっている。これをCloudflareのオリジン証明書（サーバ証明書）を使用してCloudflareとオリジンサーバー間も暗号化されるようにしてみようと思う。

Cloudflareではオリジン証明書は以下から作成できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9940d5e9-a3f5-f97c-ac2e-fdd182b75b81.png)

作成した後は、以下のようなコマンドでオリジン証明書・プライベートキーを設定する。
```console
$ sudo dnf install -y mod_ssl
$ sudo mkdir -p /etc/ssl/certs /etc/ssl/private
$ sudo vim /etc/ssl/certs/selfsigned.crt
$ sudo vim /etc/ssl/private/selfsigned.key
$ sudo vim /etc/httpd/conf.d/ssl.conf
```

`sudo vim /etc/httpd/conf.d/ssl.conf`では、`SSLCertificateFile`と`SSLCertificateKeyFile`のパスを作成したファイルのパスに上書きする。
それが完了したら、`sudo systemctl restart httpd.service`でApacheサーバを再起動する。
httpd.serviceのステータスを確認し、443でlistenしていることを確認できればOK（`sudo lsof -i -n -P`でもいいかも）。

```console
$ sudo systemctl status httpd.service
● httpd.service - The Apache HTTP Server
     ...

May 28 06:06:34 ip-10-0-1-10.ap-northeast-1.compute.internal systemd[1]: Starting httpd.service - The Apache HTTP Server...
May 28 06:06:34 ip-10-0-1-10.ap-northeast-1.compute.internal systemd[1]: Started httpd.service - The Apache HTTP Server.
May 28 06:06:34 ip-10-0-1-10.ap-northeast-1.compute.internal httpd[138473]: Server configured, listening on: port 443, port 80

$ sudo lsof -i -n -P
COMMAND      PID            USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
…
httpd     138473            root    4u  IPv6 575628      0t0  TCP *:80 (LISTEN)
httpd     138473            root    6u  IPv6 575638      0t0  TCP *:443 (LISTEN)
httpd     138476          apache    4u  IPv6 575628      0t0  TCP *:80 (LISTEN)
httpd     138476          apache    6u  IPv6 575638      0t0  TCP *:443 (LISTEN)
httpd     138477          apache    4u  IPv6 575628      0t0  TCP *:80 (LISTEN)
httpd     138477          apache    6u  IPv6 575638      0t0  TCP *:443 (LISTEN)
httpd     138478          apache    4u  IPv6 575628      0t0  TCP *:80 (LISTEN)
httpd     138478          apache    6u  IPv6 575638      0t0  TCP *:443 (LISTEN)
```

続いてVPCのセキュリティグループの設定で、インバウンドにHTTPS（443）を許可するように設定を更新する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c495d2a9-784d-fa95-8850-c5ed77a5b372.png)

ここまで設定できると、ブラウザからアクセスできるようになる（`curl`などのコマンドでも同じ）。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c8f260c2-1cc2-4b7a-5f6a-9d6098cc6d49.png)

ちなみに、証明書を設定しないで「フル」モードにすると以下のように522エラーになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/3c18a174-8fc4-81f4-d1c4-4e784826262f.png)

:::note info
Cloudflareのオリジン証明書について
通常、SSLサーバ証明書はPKI（公開鍵基盤）のCA（認証局）に発行してもらい、そのレベルによってDV、OV、EVなどの区分けがされる。
が、Cloudflareの場合はCloudflareのCAというものがあり、これを利用することでCloudflareのCDNとオリジンサーバの間のSSL/TLS暗号化ができる。ちなみに、[Full (strict) - SSL/TLS encryption modes](https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/full-strict/)に以下の通り書かれているので、厳密のモードも利用可能。

> Issued by a publicly trusted certificate authority or Cloudflare’s Origin CA.

- 参考
  - [Origin CA certificates](https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/)
:::

#### CloudflareのAuthenticated Origin Pulls (mTLS)について

クライアントとサーバの間にCDNを挟むことで、クライアントからのリクエストがオリジンサーバに届かなくなるのでセキュリティを高めることができるが、悪意のあるユーザが何らかの方法でオリジンサーバのドメイン（サブドメイン）を知った場合、CDNを経由しないで直接リクエストが送られてきてしまう。

それでは意味がないので、CDNからのリクエストであることを識別できるようにして、オリジンサーバはそれ以外を受け付けないという設定をするべき。

そのためのオプションが[Authenticated Origin Pulls (mTLS)](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/)。これは、TLSハンドシェイク時に`Client Certificate request`をオリジンサーバが送ることでクライアント証明書を要求しその証明書を確認してセッションを確立する、という仕組みを利用している。つまり、オリジンサーバに届くリクエストの内、TLSハンドシェイク時にCloudflareのクライアント証明書がないものは偽物なのではじくということができるようになる。

実際に設定してみる（今回はCloudflareが用意しているクライアント証明書を利用する）。手順の詳細は[Zone-level authenticated origin pulls](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/set-up/zone-level/) を参照。

まずは、Cloudflareの証明書をダウンロードする（pemファイル）。ダウンロードしたらEC2の`/etc/ssl/certs/authenticated_origin_pull_ca.pem`に配置する。
続いてApacheなので、以下のように設定を更新する。
```console
$ sudo vim /etc/httpd/conf.d/ssl.conf
$ sudo systemctl restart httpd.service
```
```
SSLVerifyDepth 1
SSLCACertificateFile /etc/ssl/certs/authenticated_origin_pull_ca.pem
SSLVerifyClient require
```

続いてダッシュボード上で以下のようにトグルをオンにする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/1dcc1c9d-5bcb-7e46-93c8-68f8f38f770f.png)

これで設定は完了になる。実際にcurlでクライアント証明書を持っていない状態で直接オリジンサーバにリクエストを送ってみると以下の通りクライアント証明書の検証で失敗していることがわかる（`curl: (56) OpenSSL SSL_read: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0`の部分）。
```console
$ curl https://ec2-<EC2のIPアドレスのハイフン区切り>.ap-northeast-1.compute.amazonaws.com/ -v -k
*   Trying <EC2のIPアドレス>:443...
* Connected to … (#0)
* …
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* …
* Server certificate:
*  subject: O=CloudFlare, Inc.; OU=CloudFlare Origin CA; CN=CloudFlare Origin Certificate
*  start date: May 28 02:17:00 2024 GMT
*  expire date: May 25 02:17:00 2039 GMT
*  issuer: C=US; O=CloudFlare, Inc.; OU=CloudFlare Origin SSL Certificate Authority; L=San Francisco; ST=California
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* …
* TLSv1.3 (IN), TLS alert, unknown (628):
* OpenSSL SSL_read: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0
* Closing connection 0
curl: (56) OpenSSL SSL_read: error:0A00045C:SSL routines::tlsv13 alert certificate required, errno 0
```

ちなみに、余談だがサーバ証明書の内容も表示されており、`CloudFlare Origin CA`であることがわかる。

:::note info
オリジンサーバ側でのCDNからのリクエストをチェックしてはじく方法について
Cloudflareの場合はTLSのクライアント証明書を利用して、CDNからのリクエストを識別していたが、他のCDNサービスでも同じことができるかというと不明。
[Web配信の技術 ―HTTPキャッシュ・リバースプロキシ・CDNを活用する](https://gihyo.jp/book/2021/978-4-297-11925-6)という本には、CDNからのリクエストのヘッダに何かしらの値を入れておいてそれがなければオリジンサーバ側ではじく、という方法が紹介されていた。それなら色々なCDNで対応できるみたい。
また、同本に書かれれていたが、CloudflareのAuthenticated Origin Pulls (mTLS)にしろヘッダに何かしらの値をいれるにしろ、結局オリジンサーバのサブドメインがばれるとリクエスト自体はできるので、最低限推測しにくいサブドメインにするなどの対策は必要と書かれていた（←サブドメインを推測しにくいものにすることは、「セキュリティのための曖昧化」（security through obscurity）と言うらしい）。
:::

#### なぜCDN経由にするのか？

CDNを利用すると、オリジンサーバの存在を隠すことができ、かつCDNに備わっているセキュリティ機能の利用ができるため（他にもキャッシュの話もあるが今回はそれは考えない）。

具体的にどういうことかというと、まず、CDNがない場合はブラウザからアクセスしてくるユーザー目線で考えると、以下のように直接EC2のサーバにリクエストを送っている状態になる。ということは悪意のあるユーザーであればサーバにDDoS攻撃を仕掛けたりができてしまう。

一方、CDN経由であればEC2のサーバ（オリジンサーバ）のIPアドレスを知ることはできず、またCDNには大抵WAFやDDoS保護などの機能があるのでそれを利用してセキュリティを強化できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/ecec0bcf-ac70-20a4-99a3-ca79321c64d5.png)

## まとめとして

今回は、ドメインをお名前.comで取得しお名前.comのネームサーバの設定を変更してEC2のWebサーバが名前解決されるようするという事と、CloudflareのCDNを経由してオリジンサーバ（EC2）にアクセスさせるという事の2つをやってみた。

普段Webアプリケーションエンジニアではやらないところだと思うので、基本的な部分だけでも手を動かしてやってみることで理解を深められたような気がした。また、インフラエンジニアの方との会話で理解できることが増えそうな気がした（笑）。