10分でできるセキュアなPythonのWebアプリ環境（ubuntu+nginx+http2+letsencrypt）

# PythonでWebアプリ
最近、Pythonじゃないと不便なことが少しずつ増えてきて、PythonコードでさらっとWeb応答返したいときがあります。なるべく手間をかけずに構築してみました。

# 環境
Ubuntu 18.04.1 LTS
Nginx 1.14.0
uWSGI 2.0.15
Let's Encryptの証明書

# 何はともあれインストール
UbuntuはAWSやAzureでポチっとすれば、すぐに用意できますね。
ひとまず必要なものをインストールしましょう。

```shell-session
# apt install nginx-light certbot python3-certbot-nginx uwsgi-plugin-python3
```

# 証明書のインストール
[Let's Encrypt](https://letsencrypt.org/)のおかげで本当に楽になりました。Azure環境で実行した内容を貼り付けておきます。
実行すると、`/etc/letsencrypt`以下にサーバー証明書を自動的に取得してくれる上に、`/etc/nginx/sites-enabled/default`に設定してくれます。xxxx.yyyy.cloudapp.azure.comは、実際のサーバーのURLに合わせましょう。

下の引用ではわかりにくいのですが、コマンドを実行後、対話型で4回入力をしています。

1. 自分のメールアドレス
2. 利用規約に合意するかどうか (A)
3. メールアドレスをElectronic Frontier（Let's Encryptの支援企業）に開示して良いか (Y/N)
4. httpを全部httpsにリダイレクトするか (1/2)

```shell-session
# certbot -d xxxx.yyyy.cloudapp.azure.com --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): (自分のメールアドレス)

-------------------------------------------------------------------------------
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v01.api.letsencrypt.org/directory
-------------------------------------------------------------------------------
(A)gree/(C)ancel: A

-------------------------------------------------------------------------------
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about EFF and
our work to encrypt the web, protect its users and defend digital rights.
-------------------------------------------------------------------------------
(Y)es/(N)o: Y
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for xxxx.yyyy.cloudapp.azure.com
Waiting for verification...
Cleaning up challenges
Deploying Certificate to VirtualHost /etc/nginx/sites-enabled/default

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
-------------------------------------------------------------------------------
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Redirecting all traffic on port 80 to ssl in /etc/nginx/sites-enabled/default

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled
https://xxxx.yyyy.cloudapp.azure.com

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=xxxx.yyyy.cloudapp.azure.com
-------------------------------------------------------------------------------

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/xxxx.yyyy.cloudapp.azure.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/xxxx.yyyy.cloudapp.azure.com/privkey.pem
   Your cert will expire on 2018-11-07. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot again
   with the "certonly" option. To non-interactively renew *all* of
   your certificates, run "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
```

# http2 on nginx
せっかくnginxだし、http2にしておきましょう。`/etc/nginx/sites-enabled/default`の以下の部分を書き換えます。

```conf
    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
```

sslの後にhttp2を入れるだけです。ついでにuwsgiの設定も入れておきましょう。ここではUnix Socketにしていますが、もちろんTCP/IPでも問題ありません。

```conf
    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    location /uwsgi/ {
        include uwsgi_params;
        uwsgi_pass unix:/tmp/uwsgi.sock;
    }
```

設定が終わったら、nginxの再読み込み。

```shell-session
# service nginx reload
```

# uWSGI
nginxでは、通常、起動効率が悪いCGIは使わない（使えない）のですが、代わりの流儀としてFastCGI, SGI, WSGIがあります。ここは多機能さでもWSGIでしょう。

もう最初の`apt`でインストールしてあるので、単体で動くことを確認しましょう。

```shell-session
# uwsgi --plugin python3 --http-socket :8000 --wsgi-file test.py
```

```python
#!env python3
# coding: utf-8

def application(env, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    yield '<html><head><title>テストページ</title></head><body>はーい!</body></html>'.encode('utf-8')
```

これ（[http://localhost:8000/](http://localhost:8000/)）が問題なく動いていれば、nginxとの連携はuWSGIをデーモン起動させておくだけです。
パラメータはまとめてiniファイル化しておくのが良さそう。

```ini:uwsgi.ini
[uwsgi]
socket = /tmp/uwsgi.sock
chdir = /home/myhome/app
plugin = python3
wsgi-file = /home/myhome/app/test.py
```
uWSGIを起動しておきます。一般ユーザーが良いでしょう。

```shell-session
$ uwsgi --ini uwsgi.ini -d uwsgi.log
```

Unix Socketを使ってしまったので、`/tmp/uwsgi.sock`のパーミッションがnginxから書き込み可能じゃないとエラーになります。デフォルトのnginxの実行ユーザはwww-dataなので、デフォルトのumask 0002の場合には実行ユーザを`usermod -G www-data -a xxxx`としてwww-dataグループに入れてから、実行時に`newgrp www-data`として実行するか、もしくは`umask 0`にします。

# Python3のWeb処理コード

application関数の第1引数にリクエストパラメーターが渡ってきます。ライトな処理ならそのままゴリゴリ対応しても良いし、しっかり作るなら[Flask](http://flask.pocoo.org/)や[Django](http://djangoproject.jp/)を使うのも良いでしょう。

参考のため、生のリクエストパラメーターの中身はこんな感じです。（一部ホスト名やIPアドレスは伏字化してあります）

```python
{
    'QUERY_STRING': '',
    'REQUEST_METHOD': 'GET',
    'CONTENT_TYPE': '',
    'CONTENT_LENGTH': '',
    'REQUEST_URI': '/uwsgi/script',
    'PATH_INFO': '/uwsgi/script',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_PROTOCOL': 'HTTP/2.0',
    'REQUEST_SCHEME': 'https',
    'HTTPS': 'on',
    'REMOTE_ADDR': '***.***.***.***',
    'REMOTE_PORT': '****',
    'SERVER_PORT': '443',
    'SERVER_NAME': 'xxxx.yyyy.cloudapp.azure.com',
    'HTTP_HOST': 'xxxx.yyyy.cloudapp.azure.com',
    'HTTP_CACHE_CONTROL': 'max-age=0',
    'HTTP_UPGRADE_INSECURE_REQUESTS': '1',
    'HTTP_USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.32 Safari/537.36',
    'HTTP_SEC_METADATA': 'cause="forced", destination="document", target="top-level", site="cross-site"',
    'HTTP_DNT': '1',
    'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8','HTTP_ACCEPT_ENCODING': 'gzip,deflate,br',
    'HTTP_ACCEPT_LANGUAGE': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
    'wsgi.input': <uwsgi._Input object at 0x7f5a991928d0>,
    'wsgi.file_wrapper': <built-in function uwsgi_sendfile>,
    'wsgi.version': (1,0),
    'wsgi.errors': <_io.TextIOWrapper name=2 mode='w' encoding='UTF-8'>,
    'wsgi.run_once': False,
    'wsgi.multithread': False,
    'wsgi.multiprocess': False,
    'wsgi.url_scheme': 'https',
    'uwsgi.version': b'2.0.15-debian',
    'uwsgi.node': b'xxxx'
}
```