WordPress 5.9

余り使いこなしていないので、イマイチ何が変わったか解らないけど、アナウンスがあったので兎に角アップグレードを行った。サイト改造によりDocker環境での運用のためDockerイメージとしてのwordpressを使用しているが、運用中でもあることなので、普通にダッシュボード画面にてアップグレードで難なく完了。

しかし、ダッシュボードのサイトヘルスステータスを見ると新たなエラーがあった。調べても良く分からないが何やら「intlなるライブラリが足りない」旨のメッセージのようだ。これはwordpress本体の方ではなくサーバー機能の問題ということで、DockerHubへ調査に。wordpress5.9用のfpmイメージがあったのでDockerfileを見てみるとphpのライブラリとしてintlが追加されている。

ということで、Dockerイメージの入れ替え

$ docker-compose down --rmi all

コンテナを停止してイメージ削除、そしてdocker-compose.ymlを編集してをwordpress用phpイメージを5.9に変更

$ docker-compose up -d

にて再起動完了。無事動作することを確認しました。

以前これをやってwordpressのサイトデータがおかしくなった記憶があるので、念のためディレクトリごとバックアップをとっておいたのですが、問題なかったため出番は無し。

mariadbのデータベースも毎日バックアップを取っているので事故があっても面倒なだけで大丈夫な環境にはなっています。

dockerイメージのアップデートは頻繁にあるものもあるので、ときどきはチェックしないといけないと反省。しかもDockerfileやdocker-compose.ymlでのイメージ指定がlatestになっているのが多く、現在のバージョンを調べるのが面倒。しっかりとバージョンを記載したイメージ指定にしないといけないな。。

docker対応ufw(iptabels)設定の件

ここでdockerについての解説をするのはおこがましいので、下記に覚え書きリンクを貼付ける

ローカルLAN環境でWEBサーバーを運用するだけなら良いが、当サイトにdockerを導入するに当たって一番の懸念事項がファイアウォールとの相性問題があった。

当サイトと同じUbuntu20.04LTSで下記のようなファイアウォール設定であったとして

xxxx@user$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

dockerコンテナで8080ポートでwebサイトを立ち上げると、ファイアウォールをするりと抜けて外部に公開できてしまう(下記テキトウdocker-compose.yml)

web:
  image: nginx
  volumes:
   - ./templates:/etc/nginx/templates
  ports:
   - "8080:80"
  environment:
   - NGINX_HOST=foobar.com
   - NGINX_PORT=80

当サイトではufwファイアウォールの運用で特定IPアドレスからのアクセスを遮断している(メールサーバへの不正アクセスやwordpressへの不正ログイン試行を手動・自動で監視して遮断対象アドレスをリストアップ)。

ufwコマンドで設定するポートアクセス許可よりも前で設定するために、/etc/ufw/before.rules に記載のあるチェインufw-before-inputにログ出力と遮断コマンドを追加している。

# blacklist-ipset-S ここにブロック対象のアドレスを追加
-A ufw-before-input -s xxx.xxx.xxx.xxx -j auto-blocklist
# blacklist-ipset-S

# タグをつけたログを残してドロップさせるチェイン
# ファイルの先頭の*filter節に追加する必要あり?
-A auto-blocklist -j LOG --log-prefix "[BLOCKLIST]"
-A auto-blocklist -j DROP

こうすることでufw起動時にiptableの遮断リストが設定される。

しかし、上記のdockerコンテナはこの設定も効かず、遮断対象アドレスも通過してしまう。

そこで、まずdockerコンテナが何故ufwの設定を無視できるのかを調べてみる。(iptableコマンド)

user@server:~$ sudo iptables -nvL
Chain INPUT (policy DROP 2979 packets, 152K bytes)
 pkts bytes target     prot opt in     out     source               destination
3687K 3750M ufw-before-logging-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3687K 3750M ufw-before-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
83453 4149K ufw-after-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-after-logging-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-reject-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-track-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
4406K 5775M DOCKER-USER  all  --  *      *       0.0.0.0/0            0.0.0.0/0
4406K 5775M DOCKER-ISOLATION-STAGE-1  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3634K 4639M ACCEPT     all  --  *      br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
80854 4814K DOCKER     all  --  *      br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
 675K 1102M ACCEPT     all  --  br-a52db8fb2804 !br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
70296 4218K ACCEPT     all  --  br-a52db8fb2804 br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
71973  318M ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
54110 3031K ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ufw-before-logging-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-before-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-after-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-after-logging-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-reject-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-track-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 4 packets, 196 bytes)
 pkts bytes target     prot opt in     out     source               destination
3574K 2258M ufw-before-logging-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3574K 2258M ufw-before-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-after-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-after-logging-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-reject-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-track-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0

この辺りもこのために改めて勉強したため、理解不足かも知れないが、、、

普通にufwで設定するファイアウォールはINPUTチェインが主で、5行目のufw-before-inputのチェインから繋がっている(当サイトのアクセス遮断リストもここに繋げている)。それに対してdockerのファイアウォールはFORWARDチェインであり、16,20行目のDOCKERチェインでコンテナで設定したポートの待ち受けを実現している。それはufwのFORWARD設定よりも優先しているので、アクセス制限はそれよりも前で行う必要がある。ここでアレ?と思うのがDOCKERチェインよりも前にあるDOCKER-USERチェイン。このチェインの中身は空白で何もせずに帰ってくる仕組み。

ならば、ここに遮断リストを追加してやれば良いんじゃね?という感じで、

$ sudo iptables -A DOCKER-USER -s xxx.xxx.xxx.xxx -j DROP

としてみたら、無事遮断していることを確認(とりあえずスマホのIPアドレスを遮断してみて確認)。

実は英文の方のdocker docsにはそれっぽいことが書いてあった。。

次に問題はこのリストを起動時に読み込ませるにはだけど、これは何のことはなくufwの助けが借りられることが分かった。先の遮断リストをログ出力しつつDROPするチェインを追加した要領でDOCKER-USERチェインも/etc/ufw/before.rulesに記載してしまおうという思いつきで実験、、上手く動作したので下記に示します。

$ sudo cat /etc/ufw/before.rules
# Don't delete these required lines, otherwise there will be errors
# added for docker access limits. 21/08/17 by mamiyan
*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]
:DOCKER-USER - [0:0]           # <---- これ追加
:auto-blocklist - [0:0]        # <---- ログ出力&DROP処理用チェイン
:docker-blocklist - [0:0]      # <---- 同上
# End required lines

・・・・・
# for blocklist custon chain 追加チェイン
-A auto-blocklist -j LOG --log-prefix "[DROPLIST]"
-A auto-blocklist -j DROP

# for docker custom chain 追加チェイン
-A docker-blocklist -j LOG --log-prefix "[DROPDOCKER]"
-A docker-blocklist -j DROP

# blacklist-ipset-S ここから
-A ufw-before-input -s xxx.xxx.xxx.xxx -j auto-blocklist
-A DOCKER-USER -s xxx.xxx.xxx.xxx -j docker-blocklist
# blacklist-ipset-E ここまでにリスト追記

24行25行のように二つのチェイン向けに遮断アドレスリストを追記して、$ sudo ufw reload でdocker向けのファイアウォールが設定されます。その他のアクセス制限もこの/etc/ufw/before.rules の DOCKER-USERチェインに記載しておけば良いと思います。

この辺りは、ufwを少し触れる程度の知識からiptableの仕組みとチェインを触れる程度までじっくり勉強が出来て良かったと思います。またここでCentOSとかのRHL系でfirewalldとかになると変ってくるのだろうか。そもそもあちら側は相性問題は無いのかな。

docker環境への移行

約一ヶ月ぶりの記事になりますが、夏休みということで。

この夏休みは、雨続きやら新型コロナの蔓延防止やら緊急事態やらの巣ごもりで、夏バテと闘いながら、従来から引続きpython(django)、javascript(Three.js)勉強行いながら、前回の記事でチラッと書いたdockerの勉強・実装実験を行っておりました。もともろvmwareやら仮想環境に興味があったので、比較的敷居が低かった感じで、いろいろ試しているウチに勢い余って、当サイトのWEB環境もApache2の仮想ホスト環境から、dockerコンテナのブリッジネットワーク構成に移行させてしまったというお話。さらにはメールサーバーもdockerコンテナ化・・・8月中に一通り移行を終えたところで一息ついている感じです。

とっかかりは楽しくて次々とdocker化が進んでいったが、やっぱり全く同じ環境を再現しようとすると色々と問題が出てきたり、ネット調査だけでは答えの出ない問題に突き当たったりとネタだけは貯えたので、覚え書きレベルですが追々と記事にして置こうかと思います。

つい今の今も、この記事を書こうとwordpress(これもdockerコンテナ化済み)の記事編集をしていたところ、メディアファイルがアップロード出来ない問題が発生(移行時に試していないのがいけない・・・)。半日がかりの原因究明でしたが、結局はアップロードパスの修正忘れという単純なポカミス。。まぁdockerという新しい環境への移行なので、まずそちら方面を疑ってしまい、思わぬ深みにはまってしまうというよくある罠でした。

今回の移行ですが、従来構成を下記に示す。普通のubuntuのままなのでapache単独サーバーでvhost.confの設定で仮想ホストを構成していた。

従来構成

同様の環境をdockerリバースプロキシ環境に移行した構成を下記に示します。

移行後のdocker構成

dockerブリッジネットワークでほぼ全てのサービスをコンテナ化してリバースプロキシサーバにて80番ポートと443番ポートを受けて文字通りの仮想ホスト構成としている。メールサーバーは受信のために25番ポートを空けているが、こちらは図のように直接外に向けてポートを空けてしまうと、ホスト内のサービスメール(cronやlogwatchなど)が動作しなくなってしまうので、実際はホスト側でもpostfixを動作させホスト内メールの面倒を見ながら外部から来たメールはコンテナのメールサーバーに転送する仕組みとしている。

一ヶ月の試行錯誤の結果ほぼ従来通りの動作を確認できたが、なんか妙に重いかも知れない。。。

久々のブログ記事でいきなりファイルアップロード不可とか、メッチャ焦りました。新環境では十分なテストが必要ですね。メールサーバーのdocker化もテキトウに確認して運用始めては→問題発覚→環境戻し→デバッグやり直し-の繰返しでした。たいていはショウモナイポカミスなんですが、そういうのに限って見つけるのに半日から1日作業となるのが定期。

Windows Terminal

しばらく最新の動向とか触れる機会が少なかったので、最近のツール事情にはいまだに驚かされたりする。当方では趣味でさくらVPSに契約して個人用のサイトを運営をしており、当たり前だが自宅のローカル環境から操作している。今までは通信ソフトのPuttyでサーバーにSSH接続して、サーバー上でemacsなどのエディタで構築作業をおこなっていたが、前回の投稿で書いたようにマイクロソフトのVScodeエディタでSSH接続することでローカル同じ環境で編集作業を行なうことが出来る。

それでも実際ターミナル操作をするときはPuttyを使用していたのだが、今度はWindows Terminalというものを知ることとなる。こちらはWindowsのストアアプリで、コマンドプロンプトからPowershell、WSLも対応しており、プロファイルを追加することでssh接続も可能となる優れもの。

しかもこちらはWindows版のOpenSSHのサービスで動いているので、SSH-Agentサービス(Windowsサービスで自動実行)ログイン時にパスフレーズを入れる必要もないので便利。

ローカル作業時も任意のフォルダで右クリックすると「Windowsターミナルで開く」のメニューが追加されており、Powershellが起動するようになる。(これは、以前よりシフトを押しながら右クリックでPowershellで開くことが可能だったが)

ということで、これも何かあったときのための覚え書き。

VScodeの件もそうだけど、今までは何だったのか?!ってくらい超便利な感じで、きっとまだ触っていないDockerやなんかも同じように感動するんだろうな、、、、と思いつつも、なんだかね。こんなすごい複雑なシステムの開発環境を使って、作っているものと言えば、趣味のオモチャ程度・・・。超豪華な財布の中に2000円くらいしかい入ってない感じw
精進せねばと思うこのごろ。。。

Bootstrap5対応その他

という記事を書こうとWorpressダッシュボードに移動したら、いつのまにかWordpressが5.8にバージョンアップしていた。例によってDBバックアップを取れとの忠告も聞かずに、速攻アップデートボタンポチり。

特に変った様子もないが、変化点は追々チェックするとしよう。

でBootsrtap5対応だが、先月辺りまでのこのサイトの引越作業の時点でベータ版が存在していたが、あまり気にしていなかったところ、ポータルサイト構築を兼ねたWEBアプリ開発の勉強がてら色々調べていたところバージョン5のリリースを知った(今ごろですが)。機能的にはあまり差は無い(というかよく読んでいない)がJQuery依存の削除やIEなど古いブラウザサポート終了など良い感じのアップデートのようなので、まずはdjangoテストサーバー環境で導入実験を行ない、ある程度問題ないことを確認したところで主ブランチに統合して本番環境のポータルサイトでも採用することとした。

見た目はあまり変らないが、テーマのデフォルトの文字サイズなどが異なるため、若干文字が小さかったり、メニューの文字が太字だったりと細かな気にならない違いが見られた。

4.5から5への対応に関する変更点であるが、前回までのSass導入でHTML(テンプレート)にBootstrapのクラスタグを直接書くことは少なくなったので、基本的には階層構造のスタイル定義をしているSassソースファイルを修正することで対応した。

主な違いは、marginやpaddingを表現するクラスで、たとえば右マージンはmr、左マージンはmlというようにrightとleftの頭文字で表現していたが、バージョン5より、左がstart、右がendと表現するようになりそれぞれマージンとしてはms、meと表記するようになった。これはおそらく多言語対応でRTL(右から書く文化)にも対応する事により水平方向の表記は右からと左からというより、始めと終わりという意味で統一したのだろう。

あとハマったのはアコーディオン型のメニューアニメーションの件で、若干名称が異なり、

  • data-toggle → data-bs-toggle
  • data-target → data-bs-target

になっていた。当サイトはこの位しか機能を使っていないので、以上の修正で無事以前と同様に動作するようになった。

というか、せっかくBootstrap5を導入したのに、新機能を調べて(あるのか?)導入実験とかしたいのだが、他にもいろいろやりたいことありで、なかなか優先順位に困っているところ・・・
思いつき順ということもありますが・・・

不正アクセスログ集計ツール④

前回の記事でパイプ接続したプロセスをmapのまま返すとプロセスが終らないまま関数から帰ってきてしまうので、気持ち悪いからlist関数を使ったが、

bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
 ↓
bipset =  [ipv4adrset(x.decode('UTF-8').rstrip('\n')) for x in proc.stdout]

の様に、わざわざmap使うより、下のようにリスト内包表記で直接リストを返すような仕組みを用いた方がスマートな気がした。とくに今回のようにmap直後にlistするような使い方の場合特に。

mapやfilterはイテレータなので直後にforなどのループ処理を行なう場合に適しており、リストそのものが欲しいときにはmapを使わずに[リスト内包表記]をするのが良いきがする。リスト内包表記やlist式のリスト生成では各項目の変換式(関数)が一度に使用され結果のリストが返されるが、イテレータでは生成段階では変換式(関数)は実行されず、for文など繰返し処理が行なわれる時に一つずつ実行されることになる。

たとえばファイルを読み込み関数があったとして、オープンしたファイルハンドルに対して読み出して検索や加工をする処理の結果をイテレータとして返してしまうと、関数からリターンした段階でファイルを閉じると、そのイテレータからは読み出せないという結果になる。

ということでその場合は、リストとして返す関数が正解となる。

あと、イテレータからリストに変換するときに一度繰返し処理が行なわれるという考え方からすると、for文に使用する前にリストに変換するのは効率が悪く。動作速度的にも不利になるようなので、そういう観点で使い分けていく目安になるのではないかと思う。

次に、ログファイルの解析部分。これも前回記事で、djangoの認証バックエンドをチョットいじって、ログイン失敗のログを残せるようにしたので、ここではそれを検出してリストとして返すこととする。ログファイルはApacheユーザーに読取り権限があるので、そのまま読見込めばいい。ただし、/var/logに保存している場合など、logrotateの処理が行なわれる場合それも追跡して行ないたい。また古いログはrotate時にgzip圧縮されるのでこれにも対応したい。

from blocklib import ipv4adrset
import os, glob, gzip
logger = getLogger(__name__)
import re, json

# djangoのアクセスログ(json形式)を読み込に認証履歴のレポートを返す
# 認証履歴のログはdjango認証バックエンドを継承して認証時のログ出力機能を追加したもの
# [引数]djlog : 解析するアクセスログ(json形式)をフルパスで与える(gzipもOK)
#              Noneの場合は、デフォルト django_log_json を使用(logorotateされたものも含み全てを読み込む)
#       st : レポート対象の開始日時(logrotateされたファイルはこの日時より新しいものを読み込み対象とする)
#       ed : レポート対象の終了日時
#       address : レポート対象のIPアドレスオブジェクト(Noneの場合は全て)サブネット検索対応
def get_django_loginfails(djlog, st, ed, address=None):
    if djlog is not None:
        djlogs = [djlog]
    else:
        djlogs = [x for x in glob.glob(django_log_json + '*') if re.match('.*(log|gz|[0-9])',x) and os.path.getmtime(x) > st.timestamp()]
        djlogs.sort(key=lambda x: os.path.getmtime(x))
    retlist = []
    for jlog in djlogs:
        logger.debug(jlog)
        with open(jlog, 'r') if not re.match('.*\.gz$', jlog) else gzip.open(jlog, 'rt') as jf:
            #jl = json.load(jf)
            decoder = json.JSONDecoder()
            jlines = map(lambda x: decoder.raw_decode(x), jf)
                #aa = decoder.raw_decode(line)
            for jl in map(lambda x: x[0], jlines):
                # fromisoformatで読み込むために末尾のZが邪魔なので削除
                # UTCのタイムゾーンを設定してJST時刻に変換する
                ts = datetime.fromisoformat(jl['timestamp'].rstrip('Z')).replace(tzinfo=timezone.utc).astimezone(tz_jst)
                ts = ts.replace(microsecond=0)
                if ts > st and ts < ed:
                    (dat, event) = (jl['ip'], jl['event'])
                    res0 = re.match('user logged in : (.+)$', event)
                    res1 = re.match('login faild.*: (.+)$', event)
                    event = False if res1 else True if res0 else None
                    if event is None:
                        continue
                    name = res1[1] if res1 else res0[1] if res0 else ''
                    if address is not None and address != ipv4adrset(dat):
                        continue
                    logger.debug('%s : %s - %s(%s)' % (str(ts), dat, name, event))
                    retlist.append(
                        {
                            'name':name,
                            'datetime':ts,
                            'address':ipv4adrset(dat),
                            'status':event,
                            'source':'django',
                        }
                    )
                    #print('%s : %s : %s' % (str(ts), dat, event))
    return retlist

ログはdjaongo.structlogでjson形式で出力したものを対象としている

  • 17行目globクラスを使用して指定したファイル名で始まるlogrotateファイルのリストを作成する(検索開始日付stよりも新しいタイムスタンプのファイルという条件も含める)
  • それぞれに対して、ファイルオープンし(拡張子gzだったらgzipオープンに変更)1行ずつ読み出し、対象パターンを見つけたら辞書形式のデータの配列として返す

ソースプログラムを貼付けてるけど、実際はそのままでは動かない一部の公開なので誰の役にも立たないただの覚え書きなのであった。
json形式だからさっくり読めるだろうとして始めたら何かエラーになってサッパリ読めない。。何でだろうと思って先人様たちの記事をたどって調べたら、こんなもんだと上記のようにデコーダを作って1行ずつ読み込んでいくようだ。
ぇぇぇーーー。同じくpython使ってjson形式のログを掃き出してるのになんで直接読めないん??ってなった。こんど時間があったら調べてみよう

不正アクセスログ集計ツール③

パート3と思ったけど、ちょっとdjango開発環境にて問題発生。

Ubuntuサーバー(さくらVPS)上のコンテンツを編集するのに、いろいろゴタゴタした顛末を下記に

  • sshログイン出来るユーザーでremote-SSHのVScodeでdjangoプロジェクトのコンテンツ編集を行なう。
  • このままではプロジェクトのファイル所有者やアクセス権がユーザーのものになってしまうので、Apacheユーザーで動作する本番環境を直接編集するわけには行かない。
  • そこで一旦GitHubプライベートリポジトリにpushして、Apacheユーザーで本番環境にpullすることで動作させることとした。
  • しかし、ちょっとした修正さえ編集して動作を確認するためにpush/pullを繰り返すこととなり、Git履歴がメッチャ汚くなる(一旦pushしてしまうと例え一人で使用しているとはいえrebaseとか履歴編集が容易ではない)。
  • いっそApacheユーザーでSSHログイン出来るようにしようかと、血迷ったことを考えたが、すんでの所で思いとどまる。
  • 既にApache2環境にデプロイ済のポータルプロジェクトだが、ユーザー環境ではもう一度python manage.py runserverで動作させてある程度動作検証した後に本番環境へpushするという流れにしようとした。
  • 注意点は、データベースは同じものを使用するので壊さないように注意。
  • ログファイルなど本番用と混ざらないように、別ログになるようにする
# settings.py で動作中のユーザー名を得る(テスト環境であればローカルユーザーになる)
RUN_USER = pwd.getpwuid(os.stat(__file__).st_uid).pw_name

# ユーザー名でApache2で動作中かどうかを判別してログのベースディレクトリを変更
LogBase = '/var/log/django' if RUN_USER == APACHE_USER else os.path.join(BASE_DIR, '.log')

# あとはログの仕様で設定する
  • 一応デプロイ後のものと同じ構成でdjango runserverテスト環境でも動作することを確認できたが、画像であるとかstaticが読み込めなくなっていた。(Apacheではサーバーの設定でstaticのエイリアス設定をしていたので、djangoのURLconfでの動作はあまり見ていなかった)
  • 調べたところ、静的ファイルの設定はsettings.pyでSTATIC_URLとSTATIC_ROOTで設定してあったのだが、ウチの環境では動作せず(素の状態でdjango-admin startproject XXXX して試してみたがstaticディレクトリを読み取ることが出来なかった)。代わりにSTATICFILES_DIRSという配列を設定することで無事読み込めることが分かった。
STATIC_URL = '/static/'    # これはOK

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
#   ↓
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

  • STATIC_ROOTとSTATICFILES_DIRSを同時に設定するのはダメ。
  • djangoの素の状態でこれでstaticが読み込めることが確認できたが、本番環境からpullしてきたポータルプロジェクトでは上記の修正を行なってもstaticが読み込めなかった。数時間掛けて原因究明した結果、単に404 Not Foundの状態をブラウザが覚え続けていただけで、ブラウザキャッシュクリアすることで無事読み込めるようになった。お粗末。

何ともアホらいいオチでしたが、ブラウザキャッシュは意外に見落としがち。以前のサーバー引越の時も新サーバーに以前のドメインを割り当ててても一向に変化が無くて困ったのも、結局はブラウザキャッシュが原因でした。

不正アクセスログ集計ツール②

不正アクセスに対する対応は当サイトでは悪質な(連続で何度もログインアタック攻撃をするなど)場合、手動でblocklist(ufwの場合、/etc/ufw/before.rules)に放り込むことにしている。そのため集計対象としてはブロックリストに含まれるIPアドレスは対象外としたい。よってapacheユーザーでブロックリストを読み込む必要があるが、こちらのファイルはroot権限でしか開くことが出来ない。apacheユーザーを管理者グループに入れることも考えたが、ここはひとつ。

  1. root権限でブロックリストを読み込むコマンドを作成する(getblocklist)。
  2. apacheユーザーに getblocklistコマンドだけパスワードなしでsudo出来るようにする。
  3. apacheユーザー権限で動作するpythonプログラム(djangoなど)でsubproccessにより上記コマンドを実行し、結果をリストとして得る関数を作る

で、バッチリではないかと、この方針で進めることとする。

1.root権限でブロックリストを取得するコマンド

これは全く問題なしで、以前のblockset.pyより該当部分のみを抜き出したプログラムとなる。

getblocklist.py

ipアドレスを利用するクラスipv4addressはライブラリとして分離している。

2.apacheユーザーに実行権限付加

表示プログラムを/usr/local/sbinなどに置きapacheユーザー(www-data)にsudo実行権限を与える

www-data        ALL=(ALL)       NOPASSWD:/usr/local/sbin/getblocklist.py

www-dataパスワード未設定のsu不可ユーザーなのでsudo ALLでも他のコマンドは実行出来ない。

3. apacheユーザー権限でリストを取得する

subproccessモジュールを利用して標準出力を得るには、Popenでプロセスを起動してプロセスの標準出力を接続する必要がある。プロセスのstdoutは一行づつ取り出すイテレータとなるので、mapによりipv4adrsetのリストとして取得している。(また、なぜかプロセスの出力がバイトとなっているので、UTF-8で文字列にデコードする必要があった”x.decode('UTF-8').rstrip('\n')”)

import subprocess

get_blocklist_proccess_cmd = ['sudo','/usr/local/sbin/getblocklist.py']

# root権限でブロックリスト取得コマンドを走らせて標準入力を得る
def get_blocklist():
    proc = subprocess.Popen(get_blocklist_proccess_cmd, stdout=subprocess.PIPE)
    bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
    return bipset

良く分かっていなかったのだけど、関数からmapのまま返すとイテレータなので関数の外にでてもプロセスが終わってないのね。。listとして全部取得してプロセスを正常に終らせておく必要がありましたとさ。

不正アクセスログ集計ツール①

目的

不正アクセスのログを集計してポータルの管理ページに結果を表示する

不正アクセスの定義

今回の場合はWEBページの認証機構へのログインアタックとする。パスワードを知っていて入って来てしまう場合は対象外。この場合はログイン履歴表示機能で様子をみることが可能とする。

機能

  • djangoポータルページの管理グループのみ表示(ページへのリンクも管理グループのみに表示)
  • django認証ログからの集計と、Wordpressのプラグイン(User Login History)の認証履歴データベーステーブルからの履歴も取得し、合計として集計する。
  • 集計開始日時ー終了日時、リストアップするための認証失敗回数(5回など)設定。
  • 特定のIPアドレスのアクセス詳細を表示ー上記リスト表示にボタン追加

その他機能追加として、直接関係は無いが

  • エラー404などのページカスタマイズ
  • djangoいろいろ勉強用テストページ

現在の所、最終的な完成形は上記のような仕様になるが、管理ページの追加なので訪問者には何も関係が無いです。その他思いついたら仕様追記する予定。

開発に関しては、djangoのプロジェクトディレクトリをgit管理とし、GitHubプライベートリポジトリ経由でsshログインユーザーにclone。VScodeで編集してテストブランチとしてpush。apacheユーザーでプロジェクトディレクトリ(テスト用仮想サーバー)にpullして動作試験。デバッグを行い完成形(公開レベルになった段階)をmasterブランチにmergeしてGitHubにpush。公開用プロジェクトディレクトリにpullして運用するという流れ。

単純にapacheユーザーのプロジェクトをVScodeのssh-remoteで編集したいという欲求のため、このような複雑は構造となってしまった。GitHubの使い方の勉強できて丁度良いか。

python環境整備

ubuntuのインストール直後はpythonコマンドはpython2.7系、3.8系のpythonを使いたければpython3と打たなければならなかった。まぁ仮想環境を構築すれば良いんだけど、サーバー設定環境等全ユーザーで3系を使いたいので、対策した時の覚書き

ググったサイトを参考に、update-alternativesコマンドを使用してデフォルトの起動バージョンを3系とした。

root@server:~# update-alternatives --config python
There are 2 choices for the alternative python (providing /usr/bin/python).

  Selection    Path                Priority   Status
------------------------------------------------------------
  0            /usr/bin/python3.8   2         auto mode
  1            /usr/bin/python2.7   1         manual mode
* 2            /usr/bin/python3.8   2         manual mode

Press <enter> to keep the current choice[*], or type selection number:

こんな感じでpython3が選択されている

ここで、勉強は各種実験用に個人ディレクトリにpython仮想環境を構築(venvを使用)

xxxx@server:~$ python -m venv venv
xxxx@server:~$ cd venv/
xxxx@server:~/venv$ ls
bin  include  lib  lib64  pyvenv.cfg  share
xxxx@server:~/venv$ source bin/activate
(venv) xxxx@server:~/venv$ ls
bin  include  lib  lib64  pyvenv.cfg  share
(venv) xxxx@server:~/venv$ which python
/home/xxxx/venv/bin/python

まずはpipのアップデート

(venv) xxxx@server:~/venv$ pip install -U pip
Collecting pip
  Downloading pip-21.1.2-py3-none-any.whl (1.5 MB)
     |████████████████████████████████| 1.5 MB 4.0 MB/s
Installing collected packages: pip
  Attempting uninstall: pip
    Found existing installation: pip 20.0.2
    Uninstalling pip-20.0.2:
      Successfully uninstalled pip-20.0.2
Successfully installed pip-21.1.2

あとは色々モジュールを入れたりすれば良いですが、折角なので最新バージョンを使用出来るようにと

こちらを参考にpyenv仮想環境を構築した。

git clone https://github.com/pyenv/pyenv.git ~/.pyenv
cd ~/.pyenv
git pull

下記のように環境変数を設定するよう.profileを書き換えて再ログイン

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init --path)"' >> ~/.profile

pyenv install -l | grep 3.9
  3.9.0
  3.9-dev
  3.9.1
  3.9.2
  3.9.3
  3.9.4
  3.9.5

最新は3.9.5なのでこれをインストール

pyenv install 3.9.5

なんか色々エラーが出たらコンパイルのためのライブラリが足らんようです。当方の場合はbz2が足りなかったので、

sudo apt install -y libbz2-dev libsqlite3-dev

あとググって足らなさそうなものも入れておくと、無事インストール終了

pyenv global 3.9,5   -----  3.9,5 を使用する
pyenv global system  -----  システムにインストールされているバージョンを使用

python -V

バージョン確認して無事変更されていたら、さらにvenvによる仮想環境も構築しておく

python -m venv venv
cd venv
source bin/activate
python -V

deactivate

こっちの方がライブラリが探しやすいが、pyenvで最新版のpythonが使う環境も欲しいということでの2本立てとしてみた。ライブラリ何度も入れないといけなさそうだけど、色々体で覚えていけそうw