Amazon EC2 を用いてファイル受け渡し用のWebサーバをとりあえず立てる

# この記事の目的

組織から、ファイルをSMSで送るように言われた！
SMSに直接ファイルを添付することはできないし、base64エンコードしても1通で数十文字くらいしか送れないから何通送る羽目になることやら…
外部にアップロードしてURLを送ることで許してくれるかな…
とはいえ、適当なアップローダを使うと広告やらなんやらで相手に迷惑をかけるかも…
そうだ、Webサーバを立ててそこにファイルを置こう！

…という感じの状況が発生したときなどに、なるべく簡単な設定で Amazon EC2 を用いてWebサーバを立て、ファイルを配信します。

# 前提条件

* 基本的なLinuxのコマンドラインでの操作ができる
* 利用可能なAWSのアカウントがあり、一般的なEC2インスタンスの立て方がわかる

# 免責

この記事では設定の簡単さを重視するため、パフォーマンスやセキュリティなどでまずい点がある可能性があります。
重要な案件では、専門家に相談することをおすすめします。
この記事を読んだ後行った操作 (誤操作を含む) や行わなかった操作により生じたファイルの漏洩や不着、想定外のAWS利用料金の発生、その他一切の損害について、筆者は責任を負いません。操作の判断は自己責任で行ってください。
設定方法・値・項目名などは執筆時点のものであり、今後のアップデートなどにより変わる可能性があります。

# とりあえずファイルを配信する

## インスタンスを起動する

今回は、以下の設定で Amazon EC2 のインスタンスを起動します。
これ以外の設定はデフォルトとします。

* AMI: Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
* AMI ID: ami-008fe2fc65df48dac
* インスタンスタイプ： t2.micro
* キーペア: 適宜設定する
* ネットワーク設定：セキュリティグループを作成
  * インターネットからのSSHアクセス、HTTPSアクセス、HTTPアクセスを許可する

:::note
この「とりあえずファイルを公開する」のパートではHTTPSアクセスの許可は不要ですが、後で使います。
:::

## インスタンスにSSH接続する

インスタンスを起動したら、インスタンス一覧の画面に移動し、インスタンスの状態が「実行中」になるまで待ちます。
そして、以下の設定でインスタンスにSSH接続します。

* ホスト: インスタンス概要にある「パブリック IPv4 アドレス」
* ポート: 22
* ユーザー名: `ubuntu`
* 鍵: インスタンスの起動時に設定したキーペアに対応する鍵

以下、SSH接続したインスタンス内で作業を行います。

## アプリケーションを最新にする

念のため、アプリケーションを最新にしておきます。

```
sudo apt-get update
sudo apt-get upgrade -y
```

以下のような画面が出たら、起動したばかりで再起動して困るものは無いと思う上、ここで拒否してもアップグレードのたびに聞いてくるはずなので、全部のサービスを再起動しておきます。

![apt-upgrade-20240211.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/6fbf6974-04d6-2f66-8044-4f2356c5bfcf.png)

上下矢印キーで選択中のボックスを移動し、スペースキーでチェック (`*`) の有無を切り替えます。
項目は表示されているだけでなく、下にスクロールする可能性があるので、確認を忘れないようにしましょう。
全てのボックスにチェックを入れたら、Tabキーで「Ok」に選択を移し、Enterキーで決定できます。

## HTTP(S)サーバをインストールする

HTTP(S)サーバとして有名な [NGINX(エンジンエックス)](https://www.nginx.co.jp/) をインストールします。

```
sudo apt-get install nginx -y
```

デフォルトで入っている設定ファイルの内容を確認します。

```
cat /etc/nginx/sites-enabled/default
```

おそらく、出力の中にこのような行があります。

```nginx
        root /var/www/html;
```

これは、配信するファイルが `/var/www/html` ディレクトリに格納されていることを表します。

## 配布するファイルを配置する

先ほど確認した配信するファイルがあるディレクトリに移動し、内容を確認します。

```
cd /var/www/html
ls -al
```

たとえば、以下のような結果が得られます。

```
total 12
drwxr-xr-x 2 root root 4096 Feb 10 15:57 .
drwxr-xr-x 3 root root 4096 Feb 10 15:57 ..
-rw-r--r-- 1 root root  612 Feb 10 15:57 index.nginx-debian.html
```

このディレクトリは `root` が所有者になっており、所有者以外は書き込みができない設定になっていることがわかります。
今回は、簡単のため、ディレクトリのパーミッションを変更して誰でも書き込みができるようにします。

```
sudo chmod 777 /var/www/html
```

これで書き込みができるはずなので、配布したいファイルをSCPなどを用いてこのディレクトリにコピーします。
さらに、ファイルへのリンクを張ったインデックスページを用意します。
たとえば、以下のように記述し、`index.html` としてこのディレクトリに保存します。
(配布するファイルを `file.zip` とします)

```html:index.html
<html>
<body>
<a href="file.zip">file.zip</a>
</body>
</html>
```

たとえば `cat` コマンドを用いてファイルを作成できます。
以下をコピペした後、Ctrl+D を押して入力を終了します。

```
cat > index.html
<html>
<body>
<a href="file.zip">file.zip</a>
</body>
</html>
```

## 完成

これで最低限の設定は完了です！
(インスタンス内ではなく、手元の) Webブラウザで `http://` に続いてインスタンスに割り当てられたパブリックIPv4アドレスを書いたURLにアクセスすると、ファイルへのリンク (作成したインデックスページ) が表示されるはずです。
(たとえば、パブリック IPv4 アドレスが `203.0.113.224` であれば `http://203.0.113.224`)
このURLをファイルを渡したい相手に伝えましょう。

:::note warn
このアドレスは、インスタンスの休止や停止をすると変わる可能性があります。
ファイルの受け渡しが完了するまで、インスタンスの終了はもちろん、休止や停止もしないようにしましょう。
:::

:::note
Elastic IP アドレスを用いればアドレスを固定できるはずですが、設定の手間が増えるのでこの記事では扱いません。
:::

# Basic認証を設定する

ここまでの設定でファイルを配信することはできましたが、このままではファイルを全世界に公開することになってしまいます。
サーバーのIPアドレスが分からなければアクセスはできないはずであるとはいえ、大事なファイルを余計な人に見られてしまうリスクが発生し、不安に思えます。
そこで、サーバにBasic認証を設定し、IPアドレスだけがわかってもファイルにアクセスできないようにします。

:::note warn
NGINX をインストールした後、ファイルをインスタンスにコピーする**前に**この章の設定を行います。
ファイルをコピーした後に設定をしても、設定をする前はファイルを守れません。
:::

## 認証に用いるユーザー名とパスワードを決める

まず、認証に用いるユーザー名とパスワードを決めます。
ユーザー名は、適当に `user` などでいいでしょう。
パスワードは、たとえばWebツール [パスワード生成](https://www.graviness.com/app/pwg/) などで生成できます。

今回は、以下の設定を用います。

* ユーザー名: `user`
* パスワード: `4nnNYeHt7yY7LJFt`

## 認証情報を格納したファイルを設置する

認証情報をファイルに格納するツールをインストールします。

```
sudo apt-get install apache2-utils -y
```

このツールを用い、ファイルに認証情報を格納します。

```
sudo htpasswd -Bbc /etc/nginx/.htpasswd user 4nnNYeHt7yY7LJFt
```

ここで使用したオプションは以下の意味です。

* `B` : 強いアルゴリズム (bcrypt) を用いる
* `b` : パスワードをコマンドラインで指定する
* `c` : 認証情報のファイルを新規作成する

たとえば、以下のような認証情報が格納されるはずです。
ユーザー名とパスワード情報が `:` 区切りで書かれています。

```
user:$2y$05$10i3F/Oi4tWcqwmOPyBzxeroBAN5R/opNJTr/PMvkf/9YVdxc8/3y
```

## NGINX の設定を変更する

NGINX の設定ファイルに、Basic認証の設定を追加します。
ついでに、デフォルトの設定ファイルには余計なコメントが多いので、これを用いるのはやめ、新しく設定ファイルを作成します。

```nginx
server {
	# 80番ポートでアクセスを待ち受ける
	listen 80 default_server;
	listen [::]:80 default_server;

	# 配布するファイルが有るディレクトリを設定する
	root /var/www/html;

	# ディレクトリが指定された際に返すファイル名を設定する
	index index.html;

	location / {
		# Basic認証を有効化し、realmを設定する
		auth_basic "Please enter ID and password.";
		# Basic認証用の認証情報ファイルの位置を設定する
		auth_basic_user_file /etc/nginx/.htpasswd;
	}
}
```

デフォルトの設定ファイル (のリンク) を削除し、この設定ファイルを配置します。

```
cd /etc/nginx/sites-enabled
sudo rm default
sudo vim dist
```

これらのコマンドを実行後、(Vim で) 以下の操作を行います。

1. `:set paste` と入力し、Enterキーを押す (貼り付けモードに設定する)
2. `i` と入力する (編集モードに設定する)
3. 上記設定ファイルをペーストする
4. Escキーを押す (編集モードを解除する)
5. `:wq` と入力し、Enterキーを押す (保存してVimを閉じる)

その後、NGINX を再起動します。

```
sudo service nginx restart
```

:::note warn
NGINX を再起動しないと、書き換えた設定が反映されず、Basic認証が有効になりません。
再起動を忘れないようにしましょう。
:::

## 完成

Webブラウザからサーバにアクセスし、ユーザー名とパスワードの入力が要求されることを確認します。

# HTTPSでアクセスできるようにする

Basic認証を行っても、今のままでは認証情報 (入力したユーザー名とパスワード) やファイルが暗号化せずにやりとりされます。
これでは、大事なファイルが余計な人に見られるリスクが高そうに思えます。
そこで、暗号化ありのHTTPSでアクセスできるようにします。

:::note
TLS/SSL用の通常の証明書の入手は難しそうなので、ここでは**自己署名証明書**を用います。

たとえば、[Let's Encrypt](https://letsencrypt.org/ja/) では、IPアドレスに対する証明書の発行は拒否されるようです。
また、「証明書発行対象のドメインは自分が登録者でなければならない」的な条件もあるため、「パブリック IPv4 DNS」に対する証明書の発行は規約違反になりそうです。
自分のドメインを用いる方法は、この記事では扱いません。
:::

## 秘密鍵と証明書を作成する

サーバ関係の設定ファイルがある場所に移動します。

```
cd /etc/nginx
```

そこに秘密鍵とTLS/SSL用の証明書を作成します。
1行目が秘密鍵を作成するための設定、2行目が証明書を作成するための設定です。

```
sudo openssl req -newkey rsa:2048 -noenc -keyout server.key \
    -new -x509 -days 3650 -sha512 -subj "/CN=*" -out server.crt
```


さらに、これだけだと秘密鍵が root からしか参照できず、NGINX が使えないので、パーミッションを設定します。

```
sudo chmod 644 server.key
```

## NGINX で HTTPS を有効化する

NGINX と OpenSSL のバージョンを確認します。

```
nginx -v
openssl version
```

今回は、以下の出力が得られました。

```
nginx version: nginx/1.18.0 (Ubuntu)
OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
```

[Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/)
を開き、HTTPS 用の設定ファイルを生成します。

* Server Software が nginx になっていることを確認する
* Mozilla Configuration を適宜設定する (Intermediate でいいだろう)
* Environment を確認したバージョンに設定する
  * 今回の場合、以下の設定となる
  * Server Version: `1.18.0`
  * OpenSSL Version: `3.0.2`
* Miscellaneous を適宜設定する
  * HTTP Strict Transport Security (WebブラウザにHTTPSを優先使用させる): オン
  * OCSP Stapling (サーバ証明書の失効チェックの効率を上げる): オフ

:::note
今回は自己署名証明書を用いており、発行元の認証局が存在しないため、OCSPによる失効チェックが効きません。
そのため、OCSP Stapling は意味がなさそうなので無効化します。
:::

[今回](https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=3.0.2&ocsp=false&guideline=5.7)は、以下の設定が生成されました。

<details><summary>生成されたままの設定ファイル</summary>

```nginx
# generated 2024-02-10, Mozilla Guideline v5.7, nginx 1.18.0, OpenSSL 3.0.2, intermediate configuration, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=3.0.2&ocsp=false&guideline=5.7
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /path/to/signed_cert_plus_intermediates;
    ssl_certificate_key /path/to/private_key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
    ssl_dhparam /path/to/dhparam;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000" always;
}
```

</details>

これに以下の変更を加えます。

* `ssl_certificate` を証明書のファイルパスに設定する
* `ssl_certificate_key` を秘密鍵のファイルパスに設定する
* `ssl_dhparam` を後でデータファイルを置くファイルパスに設定する
* Basic認証を設定した際の設定を書き加える

すると、以下の設定ファイルが完成しました。

```nginx
# generated 2024-02-10, Mozilla Guideline v5.7, nginx 1.18.0, OpenSSL 3.0.2, intermediate configuration, no OCSP
# https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=3.0.2&ocsp=false&guideline=5.7
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /etc/nginx/server.crt;
    ssl_certificate_key /etc/nginx/server.key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
    ssl_dhparam /etc/nginx/ffdhe2048.txt;

    # intermediate configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # 配布するファイルが有るディレクトリを設定する
    root /var/www/html;

    # ディレクトリが指定された際に返すファイル名を設定する
    index index.html;

    location / {
        # Basic認証を有効化し、realmを設定する
        auth_basic "Please enter ID and password.";
        # Basic認証用の認証情報ファイルの位置を設定する
        auth_basic_user_file /etc/nginx/.htpasswd;
    }
}
```

[先ほど](#nginx-の設定を変更する)と同様に、NGINX の設定ファイルをこれに差し替えます。

さらに、設定に合うようにデータファイルを配置します。

```
cd /etc/nginx
sudo curl -O https://ssl-config.mozilla.org/ffdhe2048.txt
```

最後に、NGINXを再起動します。

```
sudo service nginx restart
```

:::note warn
NGINX の再起動を忘れないようにしましょう。
:::

## 完成

これまでの `http://203.0.113.224` のようなURLにアクセスすると、`https://203.0.113.224` のような HTTPS の URL にリダイレクトされるはずです。
自己署名証明書を用いているので、Webブラウザは警告を出すかもしれません。
「危険性を承知で続行」のような項目を選択すると、前の章と同様のBasic認証のプロンプトが出るはずです。
設定したユーザー名とパスワードを入力して決定し、アクセスできることを確認しましょう。

# CloudFront を用いて HTTPS を有効化する

上の設定を行うと、自己署名証明書を用いたTLSアクセスが強制されるため、閲覧者にブラウザの警告により不便や不安を感じさせてしまう可能性があります。
そこで、[Amazon CloudFront](https://aws.amazon.com/jp/cloudfront/?nc=sn&loc=0) を経由してアクセスさせることで、閲覧者が警告なしでHTTPSアクセスできるようにします。

:::note warn
今回の設定では、閲覧者とCloudFrontの通信は暗号化されますが、CloudFrontとEC2インスタンスの通信は暗号化されません。
CloudFrontとEC2インスタンスの通信はAWS内部で行われるはずであり、管理されていないインターネット全体で平文で通信を行うよりはマシだと信じたいですが、余計な人に認証情報やファイルを見られる心配は残ります。
まあ、深く考えない閲覧者にHTTPS通信によるBasic認証を提示し、(安全ではなくても) **安心**してパスワードを入力させるには十分であることを期待しましょう。
:::

:::note
[CloudFront は、ひと月に 1TB 以内かつ 1千万リクエスト以内なら無料](https://aws.amazon.com/jp/cloudfront/pricing/?nc=sn&loc=3)で利用できるようです。
さらに、[EC2 から CloudFront への転送も無料](https://aws.amazon.com/jp/ec2/pricing/on-demand/)のようです。
よって、特定少数への大きくないファイルの受け渡しであれば、CloudFront は事実上無料 (追加料金なし、EBS を含む EC2 インスタンスの料金のみ) で利用できると考えられるでしょう。
:::

## NGINX 側の HTTPS 設定を解除する

上記「HTTPSでアクセスできるようにする」の設定を行った場合は、「Basic認証を設定する」の設定に戻し、HTTP でサーバにアクセスできるようにします。

:::note
CloudFront は自己署名証明書によるTLS通信を拒否するらしいので、TLS/SSL を用いない設定に戻して CloudFront に拒否されないようにします。
:::

:::note warn
NGINX の再起動を忘れないようにしましょう。
:::

## CloudFront の設定をする

CloudFront では、閲覧者からリクエストを受け取り、それに基づいてデータ配信元のサーバーとやり取りを行い、閲覧者にリソースを返すための設定のことを「ディストリビューション」というようです。
この「ディストリビューション」を作成し、Basic認証が設定されたサーバのリソースを閲覧者に提供できるようにします。

AWS コンソール左上の検索欄に「CloudFront」と入力するなどし、CloudFront の画面を開きます。
「CloudFront ディストリビューションを作成」ボタンを押します。
(または、「ディストリビューション」画面の「ディストリビューションを作成」ボタンを押します。

以下の設定を行います。

* オリジン
  * 「Origin domain」をインスタンスの「パブリック IPv4 DNS」にする
  　(たとえば `ec2-203-0-113-224.us-west-2.compute.amazonaws.com`)
  * 「プロトコル」を「HTTP のみ」にする
* デフォルトのキャッシュビヘイビア
  * 「ビューワープロトコルポリシー」を「Redirect HTTP to HTTPS」にする
  * 「キャッシュポリシー」を「CachingDisabled」にする
  * 「オリジンリクエストポリシー - オプション」を「AllViewerExceptHostHeader」にする
* ウェブアプリケーションファイアウォール (WAF)
  * 「セキュリティ保護を有効にしないでください」にする

:::note
以下の理由で、このような設定にします。

* Origin domain に IP アドレスを設定しようとすると拒否されるようである
* キャッシュを有効にしてしまうと、一旦Basic認証を抜けてファイルを入手した後は、ファイルがキャッシュされてBasic認証なしで入手できてしまうようである
* オリジンリクエストポリシーを設定しないと、Basic認証用のヘッダがNGINXに届かず、認証ができなくなってしまうようである
* WAFによるセキュリティ保護を有効にすると、料金が発生しそうである
:::

設定後、「ディストリビューションを作成」ボタンを押します。

## 完成

ディストリビューションの情報の「一般」の「詳細」にある「ディストリビューションドメイン名」のコピーボタンでコピーできる `https://(英数字列).cloudfront.net` のようなURLで、サーバにアクセスできるようになります。
ディストリビューションを作成した直後は名前解決ができないようなので、名前解決ができるようになるまで数分待ってからアクセスします。
うまくいけば、Webブラウザは証明書関係の警告を出さず、Basic認証のプロンプトを出すはずです。

## 後始末

ファイルの受け渡しが完了し、EC2インスタンスを終了(削除)する際、CloudFront のディストリビューションも削除したくなると思います。
ここでは、ディストリビューションの削除の仕方を確認します。

まず、ディストリビューション一覧の画面を開きます。
削除するディストリビューションの左側のチェックボックスにチェックを入れます。
ディストリビューションが有効になっている場合は、まず「無効」ボタンを押して無効化し、「最終変更日」が「デプロイ」から日付になるまで数分程度待ちます。
無効になっているディストリビューションは、チェックボックスにチェックを入れた状態で「削除」ボタンを押すと、確認ダイアログを経て削除できます。

# 参考サイト

* [特殊なIPアドレス：テストネットワーク用アドレスについて | IT情報メディア「LIVRA」](https://livra.geolocation.co.jp/iplearning/227/)
* [Amazon EC2 インスタンスの IP アドレス指定 - Amazon Elastic Compute Cloud](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-instance-addressing.html)
* [nginxの公開ディレクトリの設定方法 – 日々、コレ勉強](https://www.khstasaba.com/?p=953)
* [Nginx で Basic 認証 #Ubuntu - Qiita](https://qiita.com/kotarella1110/items/be76b17cdbe61ff7b5ca)
* [Module ngx_http_auth_basic_module](http://nginx.org/en/docs/http/ngx_http_auth_basic_module.html)
* [自己署名証明書の作成 #Security - Qiita](https://qiita.com/sanyamarseille/items/46fc6ff5a0aca12e1946)
* [HSTS（HTTP Strict Transport Security）についてまとめる #Security - Qiita](https://qiita.com/rocinante-ein/items/8250ec6a6712a9dcb96a)
* [OCSP Stapling](https://rms.ne.jp/sslserver/basis/enabling-ocsp-stapling/)
* [How to Accelerate Your WordPress Site with Amazon CloudFront | AWS Startups Blog](https://aws.amazon.com/jp/blogs/startups/how-to-accelerate-your-wordpress-site-with-amazon-cloudfront/)
* [ディストリビューションを削除する - Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/HowToDeleteDistribution.html)
* [amazon web services - CloudFront wasn't able to connect to the origin - Stack Overflow](https://stackoverflow.com/questions/38839693/cloudfront-wasnt-able-to-connect-to-the-origin)