[Django]サーバーの構築

VisualSdudioで開発していたDjango3.2のプロジェクトを、CentOSサーバーに公開する手順についてのメモ書き。

デプロイに関する3.2のドキュメントは以下。

https://docs.djangoproject.com/ja/3.2/howto/deployment/

今回は最終的にuWSGIでサーバーを構築する予定で、設定の流れは以下が参考になる。

https://uwsgi.readthedocs.io/en/latest/tutorials/Django_and_nginx.html

流れとしては、以下の順に順次確認して最終系にする。

  1. Django
  2. uWSGI –> Django
  3. nginx –> uWSGI –> Django
  4. WAF –> nginx –> uWSGI –> Django

まずはDjangoプロジェクトを設定して単体で確認する。

0.今回の条件

  • クライアント開発環境:Visual Studio Community 2022
  • Djangoバージョン:3.2
  • サーバー:CentOS Stream9
  • WAF:nginx+Naxsiサーバー
  • ソース管理:自前Gitサーバー

1.Djangoプロジェクト構築

1-1. Python

サーバーにインストールされているpythonのバージョンを確認。

python --version

今回のサーバーは3.9で、クライアント開発時と同じのためそのまま使うことにし、pyenvは使わないこととした

1-2. pip,git

dnf install pip
dnf install git

1-3. アプリ実行ユーザー作成

Django実行用のアプリユーザー(仮にappユーザー)を作成し、ユーザー変更する。

adduser app
su - app

1-4. Gitクライアント設定

gitサーバーは同じセグメント内の別サーバーにあり、鍵認証としている。

1)appユーザーで、鍵を作成する。

ssh-keygen -t rsa -b 4096

2)作成した公開鍵を、Gitサーバーに登録

作成された公開鍵(~/.ssh/id_rsa.pub)を、Gitサーバーのgitユーザーのauthorized_keysに登録する。

1-5. Djangoソース取得

Djangoプロジェクト用のディレクトリを作成(仮に~/django)して移動して、gitからソースを取得(クローン)する。

git clone ssh://git@10.10.10.1:9999/~/myapp.git

1-6. python実行環境

プロジェクトフォルダ配下に環境を構築し、必要なパッケージをインストール。

cd myapp
virtualenv env
cd env
source bin/activate
pip install -r ../requirements.txt

appユーザーの.bash_profileにvirtualenvのactivateを仕込んでおく。

1-7. Djangoの設定

設定をサーバー用に修正して、デプロイチェック。

https://docs.djangoproject.com/ja/3.2/howto/deployment/checklist/

まずはDEBUG=Flaseとする。SSL周りの設定については、WAFからの呼び出しのため後に設定。

ログファイルのディレクトリとしては以下を作成する。

mkdir /var/log/django
chown app:app /var/log/django

データベースについて、新規に作成した場合は、マイグレーションとスーパーユーザーを作成する。

https://docs.djangoproject.com/en/3.2/topics/migrations/

1-8. Django単体の確認

Django開発サーバーを立ち上げて、起動できることを確認する。

python manage.py runserver 0.0.0.0:8000

ブラウザで確認すると、DEBUG=Flaseとしているためstatic以下が取得できないが、この時点では無視して、後でnginx設定時に対応する。

2.uWSGI構築

2-1. uWSGIインストール

https://uwsgi.readthedocs.io/en/latest/Install.html

dnf groupinstall "Development Tools"
dnf install python-devel
pip install uwsgi

2-2. uWSGI設定

https://docs.djangoproject.com/ja/3.2/howto/deployment/wsgi/uwsgi/

起動用のuwsgi.iniファイルを/home/app/django/myapp/に作成する。設定例は以下。

[uwsgi]
chdir=/home/app/django/myapp/
module=myapp.wsgi:application
master=True
pidfile=/tmp/myapp-master.pid
vacuum=True
max-requests=5000
daemonize=/var/log/uwsgi/daemon-@(exec://date +%%Y-%%m-%%d).log 
http-socket = :8000
#socket=/usr/share/nginx/tmp/uwsgi.sock ※nginxとunixソケットでつなぐ場合

/var/log/uwsgiは以下で作成

mkdir /var/log/uwsgi
chown app:app /var/log/uwsgi

パラメータに関するドキュメントは以下。

https://uwsgi.readthedocs.io/en/latest/Options.html

2-3. 起動コマンド

uWSGIの起動コマンドstart_uwsgi.shを、/home/app/django/myapp/binに作成する。

#!/bin/sh
cd /home/app/django/myapp/
source env/bin/activate
env/bin/uwsgi --ini uwsgi.ini

起動コマンドに実行権を付与して実行し、正常に動作することを確認する。

2-4. uwsgiサービス登録

/usr/lib/systemd/systemに、サービス定義ファイルuwsgi.serviceを作成する。

[Unit]
Description=UWSGI
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
ExecStart=/home/app/django/myapp/bin/start_uwsgi.sh
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=mixed
User=app
Group=app

[Install]
WantedBy=multi-user.target

定義ファイルを作成したら、サービスの起動設定を行う。

systemctl enable uwsgi.service
systemctl start uwsgi.service

3.nginx構築

3.1 nginxインストール

dnf install nginx

3.2 static設定

https://docs.djangoproject.com/en/3.2/howto/static-files/#deployment

1)ディレクトリ作成

staticファイル配置用のディレクトリを作成する。nginxから参照できる場所として以下とし、appユーザーからもアクセスできるようにする。

mkdir /usr/share/nginx/static
chmod 777 /usr/share/nginx/static

2)Django設定

Djangoプロジェクトのsetting.pyのSTATIC_ROOTに、上記ディレクトリを設定

STATIC_ROOT = '/usr/share/nginx/static/'

3)ファイル配置

appユーザーにて以下を実行する。

python manage.py collectstatic

3.3 設定

https://uwsgi.readthedocs.io/en/latest/Nginx.html

nginxの設定ファイルに以下を追加。

location / {
    include    uwsgi_params;
    uwsgi_pass 127.0.0.1:8000;
    #uwsgi_pass unix:/usr/share/nginx/tmp/uwsgi.sock; ※unixソケットの場合
}

location /static {
    alias /usr/share/nginx/static;
}

※unixソケットを使う場合はこちらを有効にして、uWSGI側の設定もこれに合わせる。

3.4 起動設定

設定ファイルが問題ないことを確認して、nginxサービスを起動する。

nginx -t
systemctl start nginx

httpでアクセスして正常に表示できることを確認する。

4.WAF設定

4.1 WAFサーバー設定

WAFサーバー経由での公開設定を行い、正式なSSL証明書を取得する。

4.2 DjangoのSSL設定

Djangoのデプロイチェックで保留していたSSL周りの設定を変更する。SECURE_PROXY_SSL_HEADERを設定する場合は、nginx側で、X-Forwarded-Proto ヘッダーを付与するように設定する。

https://docs.djangoproject.com/ja/3.2/ref/settings/#std:setting-SECURE_PROXY_SSL_HEADER

具体的には以下を追加。

proxy_set_header    X-Forwarded-Proto       $scheme;

4.3 Naxsi設定

最初はNAXSIを学習モードにして、ルール調整して、最終的にfail2banを有効にする。

Naxsiの設定

WAFとしてNaxsiを使う場合の設定について。

Naxsiのインストールについては、以下。

ここでは、具体的にNaxsiをどう設定するかについて、ドキュメントに従って具体的に作業した内容を記す。

(ドキュメント)https://github.com/nbs-system/naxsi/wiki

1.ホワイトリスト設定

基本ルールでブロックされるページについて、表示できるようにホワイトリスト設定をする。

(ホワイトリスト)https://github.com/nbs-system/naxsi/wiki/whitelists-bnf

(MatchZones)https://github.com/nbs-system/naxsi/wiki/matchzones-bnf

ログからブロックされたルール条件を確認して、それに対するホワイトリストを設定すればいい。

(Naxsiログ)https://github.com/nbs-system/naxsi/wiki/naxsilogs

ツールで自動にも作成できるが、それほどないのであれば、サンプルを見ながら簡単にできる。

https://github.com/nbs-system/naxsi/wiki/whitelists-examples

具体的な対応方法は、以下のように行う。

1)ログのエラー確認

エラーログを見ると、例えば、Chromeからのアクセスの場合にfavicon.icoの取得でエラーが出ていることがわかる。

2022/02/23 14:46:11 [error] 1654#1654: *181 NAXSI_FMT: ip=x.x.x.x&server=xxx.net&uri=/favicon.ico&vers=1.3&total_processed=144&total_blocked=92&config=block&cscore0=$LIBINJECTION_SQL&score0=8&cscore1=$SQL&score1=16&zone0=HEADERS&id0=17&var_name0=sec-ch-ua&zone1=HEADERS&id1=1005&var_name1=cookie, client: x.x.x.x, server: xxx.net, request: "GET /favicon.ico HTTP/1.1", host: "xxx.net", referrer: "https://xxx.net/favicon.ico"

上記メッセージは、以下の2つのルールでブロックされていることを示している。

【ルールID:17、変数:sec-ch-ua】

zone0=HEADERS&id0=17&var_name0=sec-ch-ua

【ルールID:1005、変数:cookie】

zone1=HEADERS&id1=1005&var_name1=cookie

2)ホワイトリスト追加

上記2ルールに対して、以下の設定を追加する。

BasicRule wl:17 "mz:$URL:/favicon.ico|$HEADERS_VAR:sec-ch-ua";
BasicRule wl:1005 "mz:$URL:/favicon.ico|$HEADERS_VAR:cookie";

以下サンプルも参考に。

https://github.com/nbs-system/naxsi/wiki/whitelists-examples

3)動作確認

設定を確認して、以下で再読み込みを行う。

nginx -t
systemctl reload nginx 

これで再度表示できるか確認する。別のエラーになったら、正常に表示できるまで1)から3)の手順を繰り返す。

favicon.ico以外の読み込みも同じ問題があるので、URLの条件を外した。また他のルール1010,1011への対応も必要となったので、以下のような設定となった。

BasicRule wl:17 "mz:$HEADERS_VAR:sec-ch-ua";
BasicRule wl:1005,1010,1011 "mz:$HEADERS_VAR:cookie";

2.Fail2ban連携

Fail2banと連携することで、Naxsiで連続でエラーになったIPを除外できる。

https://github.com/nbs-system/naxsi/wiki/integration-fail2ban

設定は簡単に(2,3分で)できる。

2.1 設定

1)/etc/fail2ban/filter.d/nginx-naxsi.confを追加

以下の内容で、/etc/fail2ban/filter.d/nginx-naxsi.confを作成

[INCLUDES]
before = common.conf
[Definition]
failregex = NAXSI_FMT: ip=<HOST>&server=.*&uri=.*&learning=0
            NAXSI_FMT: ip=<HOST>.*&config=block
ignoreregex = NAXSI_FMT: ip=<HOST>.*&config=learning

2)/etc/fail2ban/jail.localに定義を追加

ドキュメントではjail.confを修正となっているが、jail.localに定義する方が良いので、こちらに定義を追加。

[nginx-naxsi]
enabled = true
port = http,https
filter = nginx-naxsi
logpath = /var/log/nginx/*error.log   # ※Naxsiのエラーログを指定
maxretry = 6

3)定義の読み込み

systemctl reload fail2ban

2.2 動作確認

連続でエラーを起こして、BANされることを確認する。

/var/log/fail2ban.logで確認できる。また、以下コマンドでも確認できる。

fail2ban-client status nginx-naxsi

Banned IP listに、自分のIPアドレスが追加されていることが確認できる。

実際のIP制限は、banactionのデフォルトではiptablesになってるため、CentOS Stream9では効かないので次の手順で設定する。

2.3 IP制限の種類変更

CentOS Stream9では、iptablesではなくてfirewalldを利用しているので、実際にBANを有効にするには、/etc/fail2ban/jail.localに以下定義を追加して、firewalldでIP制限をかけるように変更する。

[DEFAULT]
banaction = firewallcmd-ipset
banaction_allports = firewallcmd-allports

:

BANされた場合に、この設定が効いているのは、以下コマンドで確認できる。

# firewall-cmd --direct --get-all-rules
ipv4 filter INPUT_direct 0 -p tcp -m multiport --dports http,https -m set --match-set f2b-nginx-naxsi src -j REJECT --reject-with icmp-port-unreachable

f2b-nginx-naxsiというIPセットに問題のIPアドレスが設定される。

# ipset list f2b-nginx-naxsi
Name: f2b-nginx-naxsi
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 timeout 0 bucketsize 12 initval 0x29e6ce30
Size in memory: 280
References: 1
Number of entries: 1
Members:
X.X.X.X timeout 0   ※問題のIPアドレス

ブロックIPを解除するには以下。

fail2ban-client set nginx-naxsi unbanip X.X.X.X

2.4 メール通知

BANした場合に、メール通知する設定は以下。

destemail = jibun@xxxx.com    # メールの通知先(自分)
sender = fail2ban@xxxx.com    # メールの送信元
action = %(action_mwl)s       # BANが発動した時のアクション

Whois情報もつけてくれるので、whoisをインストール。

dnf install whois

Nginx+NaxsiでWAF

CentOS Strem9サーバーに、Nginx+NaxsiでWAFを構築する。

(Naxsi) https://github.com/nbs-system/naxsi

1.Nginxインストール

Naxsiモジュールをコンパイルするので、Nginxもソースからインストールしたものを使うほうがいいかもしれないが、保守を考えてdnfにてインストール。

dnf install nginx

今回入れたのは、バージョン1.20.1。

2.Naxsiモジュール作成

Naxsiインストールについては、GitHubの以下にある。

https://github.com/nbs-system/naxsi/wiki/naxsi-compile#build-naxsi-as-a-dynamic-extension-for-nginx-from-your-distribution-package-ie-ubuntu

この手順に従って行えばいい。

2.1 コンパイル環境

とりあえず開発用一式を入れる。

dnf groupinstall 'Development Tools'

追加でインストールしたコマンドは以下。

dnf install gd-devel
dnf install libxslt-devel
dnf install perl-ExtUtils-Embed

2.2 Naxsiソース取得

最新のバージョンを確認して、環境変数を設定。現時点では1.3。

以下のようにして、ソースを取得して展開。

export NAXSI_VER=1.3
wget https://github.com/nbs-system/naxsi/archive/$NAXSI_VER.tar.gz -O naxsi_$NAXSI_VER.tar.gz
tar vxf naxsi_$NAXSI_VER.tar.gz

2.3 Nginxソース取得

以下のようにして、1.20.1のソースを取得して展開。

export NGINX_VER=1.20.1
wget https://nginx.org/download/nginx-$NGINX_VER.tar.gz
tar vxf nginx-$NGINX_VER.tar.gz 

2.4 Naxsi動的ライブラリの作成

dnfでインストールしたNginxと同じ設定でコンパイルする必要があるので、まずNginxの情報を取得する。

nginx -V

このコマンドの出力結果の中のconfigure argumentsを指定してconfigureを行い、モジュールのコンパイルを実行。

cd nginx-$NGINX_VER  

./configure (※ここにconfigure arguments) --add-dynamic-module=../naxsi-$NAXSI_VER/naxsi_src/

make modules

今回の場合のconfigureコマンドは、具体的には以下のようになる。

./configure --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nginx --group=nginx --with-compat --with-debug --with-file-aio --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_degradation_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_image_filter_module=dynamic --with-http_mp4_module --with-http_perl_module=dynamic --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-http_xslt_module=dynamic --with-mail=dynamic --with-mail_ssl_module --with-pcre --with-pcre-jit --with-stream=dynamic --with-stream_ssl_module --with-stream_ssl_preread_module --with-threads --with-cc-opt='-O2 -flto=auto -ffat-lto-objects -fexceptions -g -grecord-gcc-switches -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -fstack-protector-strong -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -march=x86-64-v2 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' --with-ld-opt='-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -Wl,-E' --add-dynamic-module=../naxsi-$NAXSI_VER/naxsi_src/

まあNaxsiのライブラリを作りたいだけなので全部は必要ないかもしれないが、元ドキュメントに”This will ensure module will be compatible with your nginx and will load properly.”とあるように、実運用するシステムであれば、このように同じ条件でNaxsi動的ライブラリを作成するほうが安全で安心かと思う。

3.基本設定

3.1 モジュールの配置

動的ライブラリを適当な場所(/etc/nginx/moduls等)に配置する。

mkdir /etc/nginx/modules
cp objs/ngx_http_naxsi_module.so /etc/nginx/modules/

基本的なルールも、そのまま使うのでコピーする。

cp (naxsiソースディレクトリ)/naxsi_config/naxsi_core.rules /etc/nginx/

3.2 Nginxの設定

/etc/nginx/nginx.confに、モジュールとルールの読み込みと、Naxsiの設定を以下の例を参考に追加していく。

...
load_module /etc/nginx/modules/ngx_http_naxsi_module.so; # naxsiのロード

http {
    include /etc/nginx/naxsi_core.rules; # naxsi基本ルールのロード
    ...
}
...
server {
...

    location / { # naxsi is enabled, and in learning mode

        SecRulesEnabled; # naxsiを有効に
        LearningMode; #学習モードを有効に  ※エラーページにしたい場合はこれを外す
        LibInjectionSql; #SQLインジェクションへのサポートを有効に
        LibInjectionXss; #XSSインジェクションへのサポートを有効に

        DeniedUrl "/RequestDenied"; #naxsiがブロックした場合のリダイレクト先
        CheckRule "$SQL >= 8" BLOCK; #$SQLにスコアが8以上になった場合のアクションがBLOCK
        CheckRule "$RFI >= 8" BLOCK;
        CheckRule "$TRAVERSAL >= 5" BLOCK;
        CheckRule "$UPLOAD >= 5" BLOCK;
        CheckRule "$XSS >= 8" BLOCK;


        proxy_pass http://10.10.10.1; # WAFを設定したいWebサーバーのアドレス  
        ....
    }

    location /admin { # naxsi is disabled ※/admin以下はnaxsiは無効

        SecRulesDisabled; # オプション、デフォルトではnaxsiは無効
        
        allow 1.2.3.4;
        deny all;
        proxy_pass http://127.0.0.1;
        ....
    }

    location /vuln_page.php { # naxsi is enabled, and is *not* in learning mode
                 # ※この/vuln_page.phpはnaxsi有効で、学習モードでない(それ以外のページは上で指定されている学習モードになる)

        SecRulesEnabled;
        proxy_pass http://127.0.0.1;
    }
    
    location /RequestDenied {  # ※上で指定されたnaxsiがブロックした場合のlocation
        internal;
        return 403;
    }
...

}

4.動作確認

4.1 HTTPポート公開

ファイアウォールの設定で、httpのポートを開放する。

firewall-cmd --permanent --add-service=http
firewall-cmd --reload

4.2 Nginx起動

設定したnginx.confに間違いが無いかチェックする。

nginx -t

問題が無ければnginxを起動する。

systemctl start nginx

4.3 Naxsi動作確認

ブラウザからWAFを叩いてテストする。

不正なパラメータ(<scrript>)を付けてURLを叩くと、ブロックが動作しているか確認できる。LearningModeの場合は、エラーログにメッセージが残るだけである。LearningModeでない場合は、エラーページが表示される。

通常ページがブロックされないか念入りに確認する。ブロックされた場合は、ホワイトリストを設定するか、そのページをNaxsiから除外するか、ルールを見直す等の対応をする。

5.公開

問題がなくなれば、正式にhttps化して公開する。

httpsポートの開放は以下。

firewall-cmd --permanent --add-service=https
firewall-cmd --reload

Nginxを有効に。

systemctl enable nginx