モチベーション
SSHで叩かれたログをみたい、という話は昔からあったのですが。
https://qiita.com/nfwork01/items/439aafbca251d3835952
その延長線で、SSHで叩かれたコマンドをチェックして、その実行を許したり許さなかったりする、という処理を実装してみたいと前々から思ってました。
それなりに使いやすいUIを用意する、許可を一定秒数以上しなければ許可とみなす、応答を手入力する、、、などなどとやりたいことはたくさんあるのですが、とりあえ叩かれたコマンドを許す許さないだけ判定する方法は簡単に実装できそうだったので、内容をまとめておきます。
ゆくゆくは、本来やりたかった要件も盛り込みたいですが、全然別の実装にしないとやりようがないので、そっちは無期延期ということで。
SSH(というかShell)で叩かれたコマンドの実行を許したり許さなかったりする
実装は、bashのtrap/DEBUGで関数を呼び出して、その関数の中に仕掛けを仕込みます。
具体的には、trapで呼び出す関数の中で叩かれたコマンドの文字列を名前付きパイプ渡して別スクリプトで表示させる、その上で別スクリプトで入力(リターンとか)を別の名前付きパイプで受け付けて、その値が帰ってくるのを待つ、という実装になります。
って説明しててもややこしいので、コードを。
SSH操作を監視されるアカウント側の設定
今回は、叩いたコマンドをチェック「される」アカウントを、test02とします。
で、サーバの/home/test02/.bashrcをこんなふうにします。
# .bashrc
# Source global definitions
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=
# User specific aliases and functions
MT_FIFO_OUT=/var/log/sshlogs/fifo_out_${USER}_$$
MT_FIFO_IN=/var/log/sshlogs/fifo_in_${USER}_$$
mkfifo ${MT_FIFO_IN}
mkfifo ${MT_FIFO_OUT}
function log_history {
if [[ ! "${BASH_COMMAND}" =~ ^printf[^\;\&\|]+$ ]]; then
echo $(date) $$ $BASH_COMMAND >> ${MT_FIFO_OUT}
cat ${MT_FIFO_IN} > /dev/null
fi
}
readonly -f log_history
function rm_fifo {
rm ${MT_FIFO_IN}
rm ${MT_FIFO_OUT}
}
readonly -f rm_fifo
trap rm_fifo EXIT
trap log_history DEBUG
test02がSSHでログイン(ターミナルでログインしても一緒ですけど)すると、上記の.bashrcが読み出されます。
で、"trap log_history DEBUG"によって、コマンドを叩く度にlog_historyが呼び出されます。
log_historyではMT_FIFO_OUTで作成している名前付きパイプに、叩かれたコマンドの文字列を送ります。
その後、MT_FIFO_INで作成しているパイプに何らかの応答が帰ってくるのを待ちます。
このMT_FIFO_OUTとINは後述の別のスクリプトで拾って、お相手をします。
test02から見ると、MT_FIFO_IN荷何かしらの応答が帰ってくるまで処理がストップされます。
監査者(別スクリプト操作している人)がOK対応してくれない限り、test02が叩いたコマンドが実行されることはないわけです。
ちなみにif処理で、printfが入るコマンドの場合はこの待機をパスさせています。
ターミナルの頭の"[user01@localhost ~]"みたいな表示を出すためのコマンド処理への考慮です。
DEBUGを使うと、そういう処理まで拾っちゃうんですよね。。
かなり乱暴な方法で除外しており、もっときれいな方法はある気はしつつですが、とりあえず上記のとおり雑に外してます。
rm_fifoは、処理終了時のお掃除です。
これをしておなかないと、/var/log/sshlogsがごみごみしてめんどくさいので。
SSHコマンドの内容を監査する方法
test02と別のアカウントの配下に、以下のようなスクリプトをおいておきます。
$ cat ./checker.sh
#!/bin/bash
user=$1
pid=$2
MT_FIFO_OUT=/var/log/sshlogs/fifo_out_${user}_${pid}
MT_FIFO_IN=/var/log/sshlogs/fifo_in_${user}_${pid}
while true; do
cat ${MT_FIFO_OUT}
read -p "Permit? 'n' for kill this session.:" yn
if [ "$yn" == "n" ]; then
sudo kill -9 ${pid}
sudo rm ${MT_FIFO_IN}
sudo rm ${MT_FIFO_OUT}
exit
else
echo "" > ${MT_FIFO_IN}
fi
done
で、test02へのSSHログインがあると/var/log/sshlogsの下にアカウント名とプロセス名を使って命名したfifoファイルが2つできるので、ユーザ名とPID情報を拾って、sudo ./checker.sh と実行します。
(別ユーザの作成したfifoを使っているのでroot権限で無理やり書いています。ここも、ちゃんと作るならきれいなやりようはあると思うのですが。。。今回は無視。)
実行すると、MT_FIFO_OUTに入ってくるtest02ユーザ叩いたコマンド情報を1行づつ待って、それに対してリターン('n'以外)を叩く度に実施を許可して、コマンドが実行されて結果がtest02に帰る、となります。
nを叩くと、プロセスを強制終了し、fifoも閉じます。
test02のbashのプロセスにシグナルが飛ぶので、test02のbashrc側でsigintとsigkillをハンドリングする形の実装にしたかったのですが、trap呼び出しのカンスの中で名前付きパイプの応答を待っている状態に対して更にtrapで割り込むというのがうまう行かなかったので、checker側で終了処理の面倒を見ることにしました。
ちなみにfifo2つの順番がずれるとおかしなことになるのですが、これも無視!!!
まとめ
使いやすさを考えるともう少々手を入れておきたいですが、とりあえずはSSHで叩かれたコマンドを許可する/しない、をフックして判断する仕組みが実装できました。
まぁ使ってみると色々とトラブル起こしそうですが、、そこはおいおい。