はじめに
はてなブログ初投稿です。
ボンサイワークスと申します。
フィヨルドブートキャンプというプログラミングスクールで学習を始めてもうすぐ3ヶ月になりますがまだ一度も記事を書いたことがなかったので
記事を書いて投稿してみようと思います。
前置きはさておいて今回の記事の内容は「Debian系サーバーにSSHログインしたときにLINEに通知するシェルスクリプトを書こう」です。
Linux・シェルスクリプト勉強中のため、ところどころ理解が甘く、間違っている箇所などもあるかもしれませんが、どうかご指摘くださる際はお手柔らかにお願いします。
では今回作るシェルスクリプト(以下、ログイン通知シェルスクリプト)の仕様を簡単に示します。
次に認証ログについて簡単に説明します。
認証ログ
認証ログとは
- いつ誰が何のために認証をしたのかが記録されるログ
- Linuxディストリビューションによって保存先が異なる(詳しくは次項参照)
- ログの書き込みは「rsyslogd」というデーモン(常駐プログラム)が実施している
- 定期的に古いログは圧縮される(設定により色々と変更できる、今回は直近のものだけ使うので圧縮ファイルは使わない)
認証ログの保存先
Linuxディストリビューションごとの認証ログの保存先は以下の通りです。
/var/log/auth.log
/var/log/secure
/var/log/authlog
となっています。
今回、作成するシェルスクリプトはDebianが対象なので認証ログの保存先は/var/log/auth.log
になります。
認証ログの解析
認証ログを解析する上で必要となる認証ログの内容を表示してみると
SSHログインした際のメッセージにはAccepted
という文字列が含まれていることがわかります。
ということでログイン通知シェルスクリプトはこの文字列Accepted
が含まれた行を検索するアプローチで作成していきたいと思います。
LINE Notifyのアクセストークンを発行する
まず、LINE Notifyにアクセスします。アクセスしたら右上の「ログイン」から通知を受け取りたいLINEアカウントでログインします。
LINEアカウントを持っていない方はまずLINEアカウントを作ってください。
ログインできたら右上の自身のアカウント名が表示されているところをクリックして、「マイページ」をクリックしてください。
するとマイページが表示されるので以下の画像の箇所を探して「トークンを発行する」をクリックしてください。
次に以下を参考にトークン名の入力、トークルームの選択をして「発行する」をクリックします。
すると以下のようにアクセストークンが表示されるのでコピーしてどこかに控えておいてください。
ここまで来れば準備は完了です。
次にログイン通知シェルスクリプトを実装していきます。
ここからは今回作成するシェルスクリプトについて書いていくわけですが、シェルスクリプトの基本から書いてしまうととてつもなく文章が長くなってしまいますのでここでは完成したプログラムを示して、要点だけを説明することにします。
シェルスクリプトやLinuxのコマンドについて全く知らない方はインターネット上の他のサイトなどで学習してから以下に示したシェルスクリプトを作ることをオススメします。
以下、ログイン通知シェルスクリプト全文です(説明のため行番号を付加してあります)。
1 #!/bin/bash
2
3 LOGFILE_NAME="/var/log/auth.log"
4 TMP_LOGFILE_NAME="/tmp/tmp_auth.log"
5 TMP_LOGFILE_BACKUP_NAME="/tmp/tmp_auth.log.bak"
6 TMP_LOGIN_LOGFILE_NAME="/tmp/tmp_login.log"
7 ACCESS_TOKEN="LINE Notifyアクセストークン"
8
9 notify_message(){
10 message="$1($2)から$3ユーザーでログインがありました($4)"
11 curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -F "message=$message" https://notify-api.line.me/api/notify > /dev/null
12 }
13
14 if [ -f $LOGFILE_NAME ]; then
15 cp $LOGFILE_NAME $TMP_LOGFILE_NAME
16
17 if [ ! -e $TMP_LOGFILE_BACKUP_NAME ]; then
18 cp $LOGFILE_NAME $TMP_LOGFILE_BACKUP_NAME
19 fi
20
21 # 新しいファイル(TMP_LOGFILE_NAME)の方にあるログインの行だけを抽出
22 diff --old-line-format='' --unchanged-line-format='' --new-line-format='%L' $TMP_LOGFILE_BACKUP_NAME $TMP_LOGFILE_NAME | grep "Accepted" > $TMP_LOGIN_LOGFILE_NAME
23
24 cat $TMP_LOGIN_LOGFILE_NAME | while read line
25 do
26 ip=$(echo $line | cut -f 11 -d " ")
27 country=$(whois $ip | grep "country:" | sort | uniq | sed -e 's/ */ /g' | cut -f 2 -d " ")
28 country=$(echo $country | sed -e 's/ /,/g')
29 user=$(echo $line | cut -f 9 -d " ")
30 month=$(echo $line | cut -f 1 -d " ")
31 day=$(echo $line | cut -f 2 -d " ")
32 time=$(echo $line | cut -f 3 -d " ")
33 date=$month-$day,$time
34
35 notify_message $ip $country $user $date
36 done
37 cp $TMP_LOGFILE_NAME $TMP_LOGFILE_BACKUP_NAME
38 fi
1行目:シバンです。今回はbashで実行するのでこのように書きます。
3行目:認証ログのファイル名を絶対パスで変数に代入しています。
4行目:認証ログのファイルを/tmp
ディレクトリにコピーして保存したかったため、コピー先のファイル名の絶対パスを変数に代入しています。
5行目:通知していないログインメッセージのみ通知するため、一回前に実行されたときの認証ログと最新で実行されたときの認証ログの差分に必要となる一回前の認証ログのコピーが保存されるファイルの絶対パスを変数に代入しています。
6行目:通知の対象となるログインメッセージの行が抽出されるファイルの絶対パスを変数に代入しています。
7行目:「LINE Notifyのアクセストークンを発行する」で発行したアクセストークンを書きます。各人で違うのでここに控えておいたアクセストークンを書いてください。
9〜12行目:LINEにメッセージを通知する関数です。
14行目:認証ログが存在したら以下に続くメインとなる処理を行うように条件分岐をしています。
15〜22行目:15行目で認証ログのコピーを/tmp
ディレクトリにとっています。
また17〜19行目で実行一回前の認証ログのバックアップが存在しなかったらつまり一度もこのシェルスクリプトが実行されていなかったら最新の認証ログのコピーをバックアップすることで最初にこのシェルスクリプトが実行されたときに過去のものが通知されるのを防いでいます。
そして22行目で最新のログと一回前のログの差分を求めて最新のログにある行だけを抽出し、その中からさらにAccepted
が含まれる行を抽出して、ファイルに保存しています。
24〜38行目:22行目で保存されたファイルから行単位でログを読み取り、IPアドレス、国などを求めてLINEにログイン通知する関数を呼び出しています。
最後に37行目で最新のログを一回前のログと同じにすることで差分をなくしています。
ここまででログイン通知シェルスクリプトが作れたので一度サーバー上で動作確認してみます。
サーバー上で動作確認
scpコマンドでサーバーにログイン通知シェルスクリプトをコピーして、ファイルを自作シェルスクリプト置き場である/usr/local/bin
ディレクトリに移動します。
SSHログイン後、試しに実行してみると
$ ./ssh_login_notify.sh
cp: cannot open '/var/log/auth.log' for reading: Permission denied
diff: /tmp/tmp_auth.log.bak: Permission denied
diff: /tmp/tmp_auth.log: Permission denied
cp: cannot open '/tmp/tmp_auth.log' for reading: Permission denied
「権限がないよ」と怒られているようなので権限を確認します。
$ ls -l /tmp/tmp_auth.log /tmp/tmp_auth.log.bak
-rw-r----- 1 root root 814962 Nov 23 17:15 /tmp/tmp_auth.log
-rw-r----- 1 root root 814962 Nov 23 17:15 /tmp/tmp_auth.log.bak
所有者rootのみ読み書きが可能になっているようです。
コピー元の認証ログも念のため確認すると
$ ls -l /var/log/auth.log
-rw-r----- 1 root adm 816062 Nov 23 17:19 /var/log/auth.log
と同じパーミッションが設定されています。
ということでsu
コマンドでroot権限になってからログイン通知シェルスクリプトを実行すると
エラーメッセージは出なくなりました。
今回作成したログイン通知シェルスクリプトの初回実行は最新と一つ前の認証ログの差分がない仕様なので別コンソールでもう一度sshログイン後、ログイン通知シェルスクリプトを再度実行するとちゃんとLINEに通知メッセージが届きました。(本来はバグにより通知メッセージが大量に送られるなどのトラブルを避けるため、通知処理をechoコマンドに置き換えるなど十分にテストしてから通知メッセージを送った方が良いかと思います。今回は記事の都合上、割愛しました)
初学者がつまづきやすいポイント
- シェルスクリプトからファイルにアクセスする際はそのファイルの権限と実行ユーザーを合わせる必要がある(ファイルの読み書きがしたいときに
rootにしかその権限が与えられていないならrootユーザーで実行する)
- curl、diffなどコマンドがインストールされていないとスクリプトが正常に動かないのであらかじめスクリプトで必要となるものはインストールしておく
とりあえず動作確認はできたので最後にそれを定期実行するようにしてみます。
サーバー上での定期実行
Linuxでスクリプトの定期実行をするにはcrontab
コマンドを使います。
読み方はクローンタブです。
以下、簡単な説明です。
crontab
cronjob
- crondに実行して欲しいコマンドとその実行日時のセット
- crontabコマンドを実行したときのユーザー権限で動作する(rootで実行すればroot権限で動作する)
crond
使い方
cronjob
を動作させたいユーザーに変更します。今回はroot権限で動作させたいのでrootユーザーに変更します。
$ su
rootユーザーになったら以下のコマンドを打って設定を編集します。
# crontab -e
ちなみに設定済みのcronjob
の一覧を表示させたい場合は以下のコマンドを打ってください。
# crontab -l
既定のエディタが設定されている場合、そのエディタで開くので以下のように設定を編集します。
* * * * * /usr/local/bin/ssh_login_notify.sh
分 時 日 月 曜日 コマンド
の順で指定します。*
は任意を表しており、上記設定だと毎分 毎時 毎日 毎月 各曜日
つまり1分おき
にログイン通知シェルスクリプトが実行されます。
あと今回はroot権限でcronjob
を動作させましたが、特権が必要な処理以外は安全のためにroot権限のない一般ユーザーの
cronjob
にしておくのが定石らしいです。
また今回はログインの通知のみでしたが、抽出対象の文字列を変えれば不正アクセスの通知にも対応可能です。
設定を保存し、SSHログインを行うと見事LINEにログイン通知が来ました。結果はご自身の目でご確認ください。
お疲れ様でした。