[PR] 本記事にはプロモーションが含まれています。

【AlmaLinux 9】Tripwire の毎朝のメールを「説明できない変化」だけにした|rpm -Va で更新の分を外し、見張りの範囲とパスフレーズも見直した

チェックの印が並んだ長い報告の紙から、虫めがねで数枚だけを取り出し、ベルの付いた封筒に入れている様子のイラスト 自宅サーバー構築
➔ 自宅サーバー構築メモの目次はこちら

【2026年11月1日時点の情報です】

自宅サーバーには、大事なファイルが書き換えられていないかを毎日調べる Tripwire を入れています(入れ方は「Tripwire を入れたときの記事」)。毎朝3時に、その Tripwire から報告のメールが届いていました。長い日は数千行で、「Added」「Modified」がずらりと並びます。とても全部は読めず、大丈夫なのか分からないまま、毎朝読み流していました。

同じく毎朝メールが来ていた rkhunter は、記録が古いままだったのが原因でした(「rkhunter のメールの記事」)。Tripwire も調べてみると、中身はほとんどが 毎朝の dnf の自動更新で入れ替わったファイル でした。

そこで、変化のうち「パッケージの更新で説明できるもの」を自動で外し、説明できない変化があるときだけ、短いメールが来る 形にしました。組み込みでいえば、CRC の不一致を全部ログに流すのをやめて、「更新のパケットで書き込んだ範囲」以外の不一致だけを知らせるようにしたものです。

あわせて、調べてみて分かった2つの弱いところ、見張りの範囲が狭いことと、スクリプトに書いてあるパスフレーズが短いことも直しました。

この記事では、実際に動かしたまま書きます。登場するサーバー名やユーザー名は、セキュリティのため実際とは異なる架空のものに置き換えています。

場面1:毎朝のメールの正体

まず、毎朝3時に動いているスクリプト(/root/tripwire.sh)の中身を見ました。入れたときの記事で作ったものです。大事なところを抜き出します(パスフレーズは xxxxxxxx に伏せています)。

Bash
LOCALPASS=xxxxxxxx # ローカルパスフレーズ
SITEPASS=xxxxxxxx  # サイトパスフレーズ
…
# Tripwireチェック実行
rm -f /var/lib/tripwire/report/`hostname`-`date +%Y%m%d`-*.twr
${TRIPWIRE} -m c -s -c tw.cfg > /tmp/tripwire.log
if [ $(grep "Total violations found" /tmp/tripwire.log | awk '{print $4}') -ne 0 ]; then
    ${TWPRINT} -m r --report-level 1 -c tw.cfg -r /var/lib/tripwire/report/`hostname`-`date +%Y%m%d`-*.twr | \
    mail -s "Tripwire Integrity Check Report from `hostname`" root
fi
…
# データベース最新化
rm -f /var/lib/tripwire/*.twd*
${TRIPWIRE} -m i -s -c tw.cfg -P $LOCALPASS
No.部分していること
1${TRIPWIRE} -m c …記録(データベース)とくらべて、変わったファイルを調べる
2if [ … -ne 0 ]; then … mail …変化が1つでもあれば、変わったファイルの一覧(--report-level 1)を丸ごとメールで送る
3${TRIPWIRE} -m i …最後に、今の状態で記録を作り直す

つまり、毎朝届くのは「前の日の朝3時から、今朝3時までの変化」です。そしてこのサーバーは、毎朝6〜7時に dnf の自動更新が入ります。更新があった日の次の朝は、入れ替わったファイルが全部「変化」として並ぶので、長いメールになっていました。

報告のファイルの大きさを見ると、日によって大きく違います。

myserver で実行したコマンドです。

Bash
sudo ls -lt /var/lib/tripwire/report/ | head -6
  • sudo ls -lt /var/lib/tripwire/report/: 報告のファイルを、新しい順に、大きさ・日時つきで出す
  • head -6: 最初の6行(合計の行と、新しい5つ)だけにする

結果です。

Plaintext
合計 12616
-rw-r--r-- 1 root root  386982 11月  1 03:00 myserver-20261101-030001.twr
-rw-r--r-- 1 root root  381166 10月 31 03:00 myserver-20261031-030001.twr
-rw-r--r-- 1 root root   10078 10月 30 03:00 myserver-20261030-030001.twr
-rw-r--r-- 1 root root  564118 10月 29 03:00 myserver-20261029-030001.twr
-rw-r--r-- 1 root root  559782 10月 28 03:00 myserver-20261028-030001.twr

10/30 だけが小さく、ほかの日は数十万バイトでした。前の日の更新の量によって、報告の長さが変わっています。

場面2:7,312件の変化を分ける

今朝(11/1)の報告を読める形にして、変化の中身を分けました。7千件のファイルを1つずつ rpm -V で調べると、同じパッケージを何千回も調べることになり、とても時間がかかります。そこで、次の2つの一覧を1回だけ作って、突き合わせることにしました。組み込みでいえば、7千回ポーリングするのをやめて、1回まとめて読み出してから照合する形です。

No.一覧作り方
1入っている全部のパッケージのファイルrpm -qal
2パッケージの記録と合わないファイルrpm -Va(全部のパッケージを、大きさ・指紋・持ち主・権限などでくらべる)

時間がかかるので、tmux の中で動かしました。

myserver で実行したコマンドです。

Bash
mkdir -p ~/work/tripwire && cd ~/work/tripwire
R=/var/lib/tripwire/report/myserver-20261101-030001.twr
sudo twprint -m r -c /etc/tripwire/tw.cfg --twrfile "$R" -t 1 > report.txt 2>&1
grep -oE '^(Added|Modified|Removed):[[:space:]]+"[^"]+"' report.txt | sed -E 's/:[[:space:]]+"/\t/; s/"$//' > changed.tsv
echo "変わったものの数:"; cut -f1 changed.tsv | sort | uniq -c
awk -F'\t' '$1!="Removed"{print $2}' changed.tsv | while read -r f; do [ -d "$f" ] || echo "$f"; done | sort -u > changed-files.txt
echo "調べるファイル(フォルダと消えたものを除く):$(wc -l < changed-files.txt)"
rpm -qal | sort -u > all-rpm-files.txt
sudo rpm -Va > rpm-Va.txt 2>&1
awk '{print $NF}' rpm-Va.txt | sort -u > rpm-mismatch.txt
comm -23 changed-files.txt all-rpm-files.txt > not-owned.txt
comm -12 changed-files.txt rpm-mismatch.txt > mismatch.txt
echo "パッケージのファイルでないもの:$(wc -l < not-owned.txt)"
echo "パッケージと合わないもの:$(wc -l < mismatch.txt)"
  • sudo twprint -m r … -t 1 > report.txt: 報告を読める形にして、変わったものを1行ずつ(-t 1)ファイルに書く
  • grep -oE '^(Added|Modified|Removed):[[:space:]]+"[^"]+"': 種類(足された・変わった・消えた)とファイル名を取り出す。種類と名前の間はタブなので、[[:space:]]+ で空白もタブも読む
  • sed -E 's/:[[:space:]]+"/\t/; s/"$//': 種類と名前をタブ1つで区切り、引用符を外す
  • cut -f1 … | sort | uniq -c: 種類ごとの数を数える
  • awk -F'\t' '$1!="Removed"{print $2}' | while … [ -d "$f" ] …: 消えたものとフォルダを除いて、ファイルの名前だけの一覧にする
  • rpm -qal | sort -u: 入っている全部のパッケージのファイルの一覧を作る
  • sudo rpm -Va、awk '{print $NF}': パッケージの記録と合わないファイルの一覧を作る
  • comm -23 A B: A にあって B にないもの(変わったファイルのうち、パッケージのファイルでないもの)
  • comm -12 A B: A にも B にもあるもの(変わったファイルのうち、パッケージと合わないもの)

結果です。rpm -Va を含めて、約44秒でした。

Plaintext
変わったものの数:
    191 Added
   7094 Modified
     27 Removed
調べるファイル(フォルダと消えたものを除く):7255
パッケージのファイルでないもの:31
パッケージと合わないもの:0

パッケージと合わないものは 0 でした。変わったパッケージのファイルは、全部パッケージのとおりです。残りの「パッケージのファイルでないもの」31と、消えた27を見ると、次のとおりでした。

No.まとまり数正体
1/var/log の下(変わった・消えた)26・13記録のファイル。毎日書き足され、毎週入れ替わる
2/root の下4前の日に自分で直したスクリプトや設定と、その控え
3/usr/local/bin/cloudflared1パッケージの外で入れた cloudflared が、更新で入れ替わった
4/usr の下で消えたもの14前の日の更新で、古い版のパッケージのファイルが消えた分

危ないものはありませんでした。7,312件の変化は、ほとんどが「dnf の更新」と「記録のファイル」で、残りは自分で直したものと cloudflared の更新でした。

場面3:判断のスクリプトを作る

場面2の分け方を、毎朝自動で行うスクリプトにしました。決まりは次のとおりです。

No.変わったもの扱い理由
1/var/log/ の下数えない毎日必ず変わる
2パッケージのファイルで、パッケージのとおり数えないdnf の更新の分と言える
3パッケージのファイルで、パッケージと合わない知らせる更新では説明できない書き換え
4パッケージの外のファイル(/var/log を除く)知らせる自分で直したものか、知らない書き換え。自分で直した日は「記録された」確かめになる
5消えたもの(/var/log を除く)前の調べのあとに dnf でパッケージを入れた日は、/usr の下で消えたものだけ数えない。それ以外は 知らせる更新で古いファイルが消えるのはふつう。ほかの場所で消えるのはおかしい

スクリプトは、毎朝のスクリプト(パスフレーズが書いてある)とは別のファイルにしました。毎朝のスクリプトを丸ごと書き直すと、パスフレーズを画面に出すことになるためです。

myserver で実行したコマンドです。

Bash
sudo tee /usr/local/sbin/tripwire-filter > /dev/null <<'EOF'
#!/bin/bash
# Tripwire の報告から、dnf の更新などで説明できない変化だけを出す。
# 使い方:tripwire-filter 報告ファイル.twr
# 終わりの番号:0 = 全部説明できた、1 = 説明できない変化がある、2 = 報告を読めなかった
set -u
R="$1"
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
/usr/sbin/twprint -m r -c /etc/tripwire/tw.cfg --twrfile "$R" -t 1 2>/dev/null \
  | grep -oE '^(Added|Modified|Removed):[[:space:]]+"[^"]+"' \
  | sed -E 's/:[[:space:]]+"/\t/; s/"$//' > "$W/all.tsv"
TOTAL=$(wc -l < "$W/all.tsv")
if [ "$TOTAL" -eq 0 ]; then echo "Tripwire の報告を読めなかった:$R"; exit 2; fi
grep -v $'\t/var/log/' "$W/all.tsv" > "$W/c.tsv" || true
awk -F'\t' '$1!="Removed"{print $2}' "$W/c.tsv" | while read -r f; do [ -d "$f" ] || echo "$f"; done | sort -u > "$W/files"
rpm -qal 2>/dev/null | sort -u > "$W/rpmfiles"
rpm -Va 2>/dev/null | awk '{print $NF}' | sort -u > "$W/mismatch"
comm -23 "$W/files" "$W/rpmfiles" | sed 's/^/[パッケージ外] /' > "$W/out"
comm -12 "$W/files" "$W/mismatch" | sed 's/^/[パッケージと違う] /' >> "$W/out"
PREV=$(ls -t /var/lib/tripwire/report/*.twr | grep -A1 -xF "$R" | sed -n 2p)
PREVT=$( [ -n "$PREV" ] && stat -c %Y "$PREV" || echo 0 )
LASTINST=$(rpm -qa --qf '%{INSTALLTIME}\n' | sort -n | tail -1)
if [ "$LASTINST" -le "$PREVT" ]; then
  # 前の調べのあと、パッケージを入れていない:消えたものは全部知らせる
  awk -F'\t' '$1=="Removed"{print "[消えた] "$2}' "$W/c.tsv" >> "$W/out"
else
  # パッケージを入れた:/usr の下で消えたもの(古い版のファイル)だけは数えない
  awk -F'\t' '$1=="Removed" && $2 !~ /^\/usr\//{print "[消えた] "$2}' "$W/c.tsv" >> "$W/out"
fi
N=$(wc -l < "$W/out")
if [ "$N" -gt 0 ]; then
  echo "Tripwire:説明できない変化 ${N}件(調べた変化 ${TOTAL}件のうち)"
  cat "$W/out"
  echo "くわしい報告:$R"
  exit 1
fi
logger -t tripwire-filter "all ${TOTAL} changes explained ($R)"
exit 0
EOF
sudo chmod 755 /usr/local/sbin/tripwire-filter
sudo bash -n /usr/local/sbin/tripwire-filter && echo "syntax OK"
sudo /usr/local/sbin/tripwire-filter /var/lib/tripwire/report/myserver-20261101-030001.twr; echo "rc=$?"
  • sudo tee /usr/local/sbin/tripwire-filter > /dev/null <<'EOF' … EOF: EOF までの中身で、新しいスクリプトのファイルを作る('EOF' と引用符で囲むと、中の $ がそのまま書かれる)
  • set -u、mktemp -d、trap 'rm -rf "$W"' EXIT: 決めていない変数を使ったら止める。作業用の一時フォルダを作り、終わるときに必ず消す
  • if [ "$TOTAL" -eq 0 ]; then … exit 2: 報告が読めなかったときは、黙らずに「読めなかった」と出して止まる。黙って何も知らせないのがいちばん危ないため
  • grep -v $'\t/var/log/': 記録のファイルを外す
  • rpm -qal・rpm -Va・comm: 場面2と同じ。パッケージの外のもの、パッケージと合わないものを取り出す
  • PREV=…、PREVT=…、LASTINST=…: ひとつ前の報告の時刻と、いちばん新しくパッケージを入れた時刻をくらべる
  • if … else … fi: 決まりの No.5。パッケージを入れていない日は、消えたものを全部知らせる。入れた日は、/usr の下以外で消えたものを知らせる
  • exit 1/exit 0: 説明できない変化があれば 1、なければ 0 で終わる。0 のときは、システムの記録に1行残す
  • sudo bash -n …: 動かさずに、書き方の誤りだけを調べる
  • 最後の行: 今朝の報告で試す

結果です。

Plaintext
syntax OK
Tripwire:説明できない変化 5件(調べた変化 7312件のうち)
[パッケージ外] /root/.config/rclone/rclone.conf
[パッケージ外] /root/.config/rclone/rclone.conf.bak-20261031
[パッケージ外] /root/wp-html-backup.sh
[パッケージ外] /root/wp-html-backup.sh.20261031
[パッケージ外] /usr/local/bin/cloudflared
くわしい報告:/var/lib/tripwire/report/myserver-20261101-030001.twr
rc=1

7,312件のうち、残ったのは5件だけでした。どれも前の日に自分で直したものと、cloudflared の更新で、思い当たるものばかりです。消えたもの(/usr の下の14)は、前の日に更新があったので数えていません。

場面4:毎朝のスクリプトのメールの部分を差し替える

毎朝のスクリプトの、メールを出す4行(場面1の if … fi)を、判断のスクリプトを呼ぶ形に差し替えます。パスフレーズの行は画面に出さないよう、ファイルを丸ごと開かず、Python で「今の4行がちょうど1か所あるときだけ、新しい行に置き換える」ようにしました。

新しい形の動きは次のとおりです。

No.場合新しい動き
1報告が見つからない、または調べの結果が読めない「Tripwire ERROR」のメールを出す(黙らない)
2変化が 0何もしない
3変化はあるが、全部説明できるメールを出さない
4説明できない変化がある、または報告を読めなかった判断のスクリプトの短い結果を、メールで出す

myserver で実行したコマンドです。

Bash
sudo cp -p /root/tripwire.sh /root/tripwire.sh.20261101
sudo python3 - <<'PYEOF'
p = "/root/tripwire.sh"
s = open(p, encoding="utf-8").read()
old = '''if [ $(grep "Total violations found" /tmp/tripwire.log | awk '{print $4}') -ne 0 ]; then
    ${TWPRINT} -m r --report-level 1 -c tw.cfg -r /var/lib/tripwire/report/`hostname`-`date +%Y%m%d`-*.twr | \\
    mail -s "Tripwire Integrity Check Report from `hostname`" root
fi
'''
new = '''REPORT=$(ls -t /var/lib/tripwire/report/`hostname`-`date +%Y%m%d`-*.twr 2>/dev/null | head -1)
VIOL=$(grep "Total violations found" /tmp/tripwire.log | awk '{print $4}')
if [ -z "$REPORT" ] || [ -z "$VIOL" ]; then
    printf 'Subject: Tripwire ERROR on %s\\nContent-Type: text/plain; charset=UTF-8\\n\\nTripwire の調べが終わらなかったか、報告が見つからない\\n' "`hostname`" | /usr/sbin/sendmail root
elif [ "$VIOL" -ne 0 ]; then
    /usr/local/sbin/tripwire-filter "$REPORT" > /tmp/tripwire-filter.txt 2>&1
    if [ $? -ne 0 ]; then
        { printf 'Subject: Tripwire unexplained changes on %s\\nContent-Type: text/plain; charset=UTF-8\\n\\n' "`hostname`"; cat /tmp/tripwire-filter.txt; } | /usr/sbin/sendmail root
    fi
fi
'''
n = s.count(old)
if n != 1:
    raise SystemExit(f"置き換える4行が {n} か所見つかった(1 のはず)。何も書き換えていない")
open(p, "w", encoding="utf-8").write(s.replace(old, new))
print("置き換えた")
PYEOF
sudo bash -n /root/tripwire.sh && echo "syntax OK"
  • sudo cp -p /root/tripwire.sh /root/tripwire.sh.20261101: 直す前のスクリプトの控え(元と同じく root だけが読める権限のまま写す)
  • sudo python3 - <<'PYEOF' … PYEOF: スクリプトを読み、今の4行(old)がちょうど1か所あるときだけ、新しい行(new)に置き換える。見つからなければ、何も書き換えずに止まる
  • REPORT=$(ls -t … | head -1): 今日の報告のファイルの名前を取る
  • if [ -z "$REPORT" ] || [ -z "$VIOL" ]; then … sendmail: 報告や変化の数が取れなかったら、エラーのメールを出す
  • /usr/local/sbin/tripwire-filter "$REPORT" > … ; if [ $? -ne 0 ]: 変化があったら判断のスクリプトを動かし、0 以外で終わったときだけ、その結果をメールで出す
  • printf 'Subject: …\nContent-Type: text/plain; charset=UTF-8\n\n' … | /usr/sbin/sendmail root: 件名と文字コードを付けて送る。本文が日本語なので、文字化けしないように charset=UTF-8 を付ける
  • sudo bash -n: 書き方の誤りがないかを見る

結果は 置き換えた と syntax OK でした。続けて、毎朝のスクリプトを今そのまま動かしました。

myserver で実行したコマンドです(tmux の中で)。

Bash
sudo /root/tripwire.sh; echo "rc=$?"
sudo grep "Total violations found" /tmp/tripwire.log
sudo cat /tmp/tripwire-filter.txt
  • sudo /root/tripwire.sh; echo "rc=$?": 毎朝のスクリプトを今動かす(調べる → 判断 → 必要ならメール → 記録を作り直す)
  • sudo grep "Total violations found" /tmp/tripwire.log: Tripwire が見つけた変化の数を出す
  • sudo cat /tmp/tripwire-filter.txt: 判断のスクリプトの結果(メールの本文と同じ)を出す

届いたメールです。

Tripwire の短いメール。説明できない変化 7件(調べた変化 15件のうち)の一覧

朝3時に記録を作り直してからの変化15件のうち、7件が残りました。朝の再起動で書き換わった /boot/grub2/grubenv と /etc/resolv.conf、さっき直した /root/tripwire.sh とその控え、昼に直したスクリプトとその控え、朝に消した設定の控えです。どれも、その日に自分でしたことでした。

長い報告が丸ごと届く代わりに、見るべきものだけが短く届くようになりました。

場面5:見張りの範囲を広げる

メールを見ていて、気づいたことがありました。その日に足したスクリプトや、直した設定ファイル、消したフォルダのいくつかが、メールに出てこないのです。Tripwire の決まり(ポリシー)を見ると、入れたときのまま、Tripwire が最初から持っている RHEL 向けの見本 でした。

見本の決まりは、OS のパッケージのコマンドやライブラリが中心です。見張っていなかったのは、次のような場所でした。

No.種類大事な理由
1毎日動かすものの置き場所の一部侵入者が「自分のプログラムを毎日動かす」ために、よく書き足す
2サービスの設定の置き場所同じく、居座るためによく使われる
3ログインの設定と、ログインに使える鍵鍵を書き足されると、パスワードなしで入られる
4Web サーバー・PHP・データベースの設定の一部サーバーの動きそのもの
5自分で置いた見張りのスクリプトの置き場所見張りを止められても気づけない
6Web のファイル外から書き込まれた悪いファイル(裏口)が置かれやすい

これらを、決まりの最後に新しいまとまり(ルール)として足しました。調べる中身は、見本の SEC_BIN(大きさ・指紋・持ち主・権限など、中身が変わったら分かる)にしています。見本で設定ファイルに使われている SEC_CONFIG は、中身の指紋を調べない作りなので、書き換えられても気づけません。毎日ふつうに変わる場所(画像のアップロードの場所など)は、見張りから外しました。

足したまとまりの形です(場所の一覧は省いています)。

Plaintext
# 2026-11-01 追加:居座りに使われやすい場所と、自分で置いたもの
(
  rulename = "Local additions",
  severity = $(SIG_HI)
)
{
  /見張る場所                 -> $(SEC_BIN) ;
  …
  !/見張りから外す場所 ;
  …
}
  • rulename、severity = $(SIG_HI): まとまりの名前と、大事さ(いちばん高い)
  • /場所 -> $(SEC_BIN) ;: その場所の中身が変わったら分かるように見張る。フォルダなら中も全部
  • !/場所 ;: 先頭の ! は、その場所を見張りから外す

今の決まりを文字に書き出し、このまとまりを足して、Tripwire に入れ直しました。決まりを入れ直すにはサイトのパスフレーズが、記録を作り直すにはローカルのパスフレーズが要ります。決まりを入れたら、すぐに毎朝のスクリプトを動かすのではなく、記録の作り直しだけを先に します。新しく見張る場所のファイルが全部「足された」として出て、とても長いメールになるのを防ぐためです。

myserver で実行したコマンドです(tmux の中で)。

Bash
sudo twadmin -m p -c /etc/tripwire/tw.cfg -p /etc/tripwire/tw.pol -S /etc/tripwire/site.key > ~/work/tripwire/policy-now.txt
cp ~/work/tripwire/policy-now.txt ~/work/tripwire/policy-new.txt
#(policy-new.txt の最後に、上のまとまりを足す)
sudo cp -p /etc/tripwire/tw.pol /etc/tripwire/tw.pol.20261101
sudo twadmin -m P -c /etc/tripwire/tw.cfg -p /etc/tripwire/tw.pol -S /etc/tripwire/site.key ~/work/tripwire/policy-new.txt
sudo tripwire -m i -s -c /etc/tripwire/tw.cfg
sudo tripwire -m c -s -c /etc/tripwire/tw.cfg > /tmp/tw-check.log 2>&1
grep -E "Total objects scanned|Total violations found" /tmp/tw-check.log
  • sudo twadmin -m p … > policy-now.txt: 今の決まりを、読める文字にしてファイルに書く(読むだけなら、パスフレーズは要らない)
  • sudo cp -p …tw.pol …tw.pol.20261101: 今の決まりの控え
  • sudo twadmin -m P … policy-new.txt: 新しい決まりを、サイトの鍵で署名して入れる。ここでサイトのパスフレーズを聞かれる
  • sudo tripwire -m i -s -c …: 新しい決まりで、記録を作り直す。ここでローカルのパスフレーズを聞かれる
  • sudo tripwire -m c -s -c … > /tmp/tw-check.log: 作り直した直後に、もう一度調べる(変化 0 になるはず)
  • grep -E "Total objects scanned|Total violations found": 調べたファイルの数と、変化の数を出す

結果です(パスフレーズを聞かれる行は省いています)。

Plaintext
Wrote policy file: /etc/tripwire/tw.pol
Total objects scanned:  141553
Total violations found:  1

調べるファイルは14万件余りになりました。作り直しも調べも、それぞれ約1分です。変化の1件は、記録を作り直すときに Tripwire が前の記録を .bak の名前で残した、そのファイルでした。毎朝のスクリプトは、作り直す前に古い記録を全部消すので、毎朝は出ません。

場面6:パスフレーズを長いでたらめなものにする

場面1のとおり、毎朝のスクリプトには Tripwire の2つのパスフレーズがそのまま書いてあります。入れたときの記事の作りで、毎朝自動で記録を作り直すには、スクリプトに書いておく必要があるためです。ただ、中身を見ると、ホスト名から作った短いものでした。

Tripwire のパスフレーズは、「root を乗っ取った人が、記録を書き換えて変化を隠す」のを防ぐためのものです。スクリプトに書いてある以上、root を乗っ取られたら読まれてしまうので、ここは自動で動かすことと引き換えにした弱いところです。それでも、推測しやすい短いものよりは、長いでたらめなもののほうがよいので、変えました。

変えるには、鍵を作り直し、設定と決まりを新しい鍵で署名し直し、記録も作り直す必要があります。途中で失敗したら止まるよう、1つの root のスクリプトにまとめて動かしました。新しいパスフレーズは、画面に出しません。

myserver で実行したコマンドです(tmux の中で)。

Bash
sudo bash <<'EOF'
set -euo pipefail
cd /etc/tripwire
H=$(hostname)
B=/root/tripwire-backup-20261101
mkdir -p -m 700 "$B"
cp -p site.key "${H}-local.key" tw.cfg tw.pol "$B"/
cp -p /root/tripwire.sh "$B"/tripwire.sh.before-rekey
twadmin -m f -c tw.cfg > /root/twcfg.txt
twadmin -m p -c tw.cfg -p tw.pol -S site.key > /root/twpol.txt
NEWSITE=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)
NEWLOCAL=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)
echo "新しいパスフレーズ:サイト ${#NEWSITE} 文字、ローカル ${#NEWLOCAL} 文字(中身は出さない)"
rm -f site.key "${H}-local.key"
twadmin -m G -S site.key -Q "$NEWSITE" > /dev/null
twadmin -m G -L "${H}-local.key" -P "$NEWLOCAL" > /dev/null
twadmin -m F -c tw.cfg -S site.key -Q "$NEWSITE" /root/twcfg.txt > /dev/null
twadmin -m P -c tw.cfg -p tw.pol -S site.key -Q "$NEWSITE" /root/twpol.txt > /dev/null
rm -f /var/lib/tripwire/*.twd*
tripwire -m i -s -c tw.cfg -P "$NEWLOCAL"
sed -i "s|^LOCALPASS=[^ #]*|LOCALPASS=${NEWLOCAL}|; s|^SITEPASS=[^ #]*|SITEPASS=${NEWSITE}|" /root/tripwire.sh
bash -n /root/tripwire.sh && echo "syntax OK"
rm -f /root/twcfg.txt /root/twpol.txt
unset NEWSITE NEWLOCAL
echo "完了"
EOF
  • sudo bash <<'EOF' … EOF: EOF までを、root の1つのスクリプトとして動かす。中の変数(パスフレーズ)は、この中だけで使う
  • set -euo pipefail: どれか1つでも失敗したら、その場で止まる
  • mkdir -p -m 700 "$B"、cp -p …: 今の鍵2つ・設定・決まり・スクリプトの控えを、root だけが見られる場所に取る
  • twadmin -m f …、twadmin -m p …: 今の設定と決まりを、文字の形に戻す(新しい鍵で署名し直すため)
  • openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32: 英字と数字だけの、32文字のでたらめなパスフレーズを作る。${#…} は文字数だけを出す
  • twadmin -m G -S site.key -Q …、twadmin -m G -L …-local.key -P …: 新しいパスフレーズで、サイトの鍵とローカルの鍵を作る
  • twadmin -m F …、twadmin -m P …: 設定と決まりを、新しいサイトの鍵で署名し直す(中身は今のまま)
  • rm -f /var/lib/tripwire/*.twd*、tripwire -m i … -P …: 古い記録を消し、新しいローカルの鍵で記録を作り直す
  • sed -i "s|^LOCALPASS=…|…|; s|^SITEPASS=…|…|" /root/tripwire.sh: 毎朝のスクリプトの2行を、新しいパスフレーズに書き換える
  • rm -f /root/twcfg.txt /root/twpol.txt、unset …: 一時ファイルと変数を消す

結果の、大事な行です。

Plaintext
新しいパスフレーズ:サイト 32 文字、ローカル 32 文字(中身は出さない)
syntax OK
完了

最後に、毎朝のスクリプトを今動かして、新しいパスフレーズで「決まりの書き直し」と「記録の作り直し」が通るかを確かめました。終わりの番号は 0 で、パスフレーズのエラーは出ず、届いたメールは次の3件だけでした。

Plaintext
Tripwire:説明できない変化 3件(調べた変化 3件のうち)
[パッケージ外] /root/tripwire.sh
[消えた] /root/twcfg.txt
[消えた] /root/twpol.txt

どれも、いま動かしたスクリプトの分(毎朝のスクリプトの2行の書き換えと、途中で使って消した一時ファイル2つ)です。新しいパスフレーズは、毎朝のスクリプトの中にだけあり、自分で覚えておく必要はありません。

場面7:もう1台も同じ

家には、myserver を仮想マシンとして動かしている土台のサーバー(kvmhost)もあり、同じ Tripwire が動いています。判断のスクリプトを作り、毎朝のスクリプトのメールの部分を同じように差し替えて、今動かしました。

Plaintext
Total violations found:  7228
Tripwire:説明できない変化 4件(調べた変化 7228件のうち)
[パッケージ外] /etc/resolv.conf
[パッケージ外] /root/tripwire.sh
[パッケージ外] /root/tripwire.sh.20261101
[消えた] /root/.config/rclone/rclone.conf.bak-20261031

その日の朝の自動更新で7,228件のファイルが入れ替わっていましたが、説明できないのは4件だけで、どれもその日の作業の分でした。前の形なら、7千行の報告がそのまま届いていたところです。

kvmhost は Web サーバーではないので、足す見張りの場所は、kvmhost に合わせて選びました(Web のファイルの代わりに、仮想マシンの設定、SSD の見張りの設定、メールの転送の設定など)。kvmhost では、決まりを足すのとパスフレーズを変えるのを1回にまとめました。場面6のスクリプトで、「今の決まりを文字に戻す」ところを「足したあとの決まりのファイルを使う」に変えるだけで済みます。

Plaintext
4. 設定と新しい決まりを署名した(Local additions:1)
5. 記録を作り直した:2291076 バイト
6. /root/tripwire.sh の2行を書き換えた
7. 作り直した直後の調べ:Total objects scanned: 48392 Total violations found: 3

毎朝のスクリプトを今動かしても、パスフレーズのエラーはなく、myserver と同じ3件(スクリプトの書き換えと、一時ファイル2つ)だけのメールが届きました。

場面8:カーネルとプラグインの更新の日も、見るべきものだけにする(2026年11月4日に追記)

この形にして3日目の朝、Tripwire のメールが、また長くなりました。myserver は「説明できない変化 10,044件(調べた変化 17,818件のうち)」、kvmhost は「6,027件(13,130件のうち)」です。

中身を見ると、ほとんどが2つでした。

No.出ていたもの正体
1/usr/lib/firmware/<カーネルの版>/intel-ucode/…、/boot/initramfs-…kdump.img、/boot/loader/entries/…、/lib/modules/…/weak-updates/… など前の日に入った新しいカーネルの部品。カーネルを入れるときに、パッケージの外で作られたり、古い版の分が消えたりする
2/var/www/html/wp-content/plugins/google-site-kit/… が2,000件ほどWordPress のプラグインの自動更新。プラグインのファイルが丸ごと入れ替わった(場面5で /var/www を見張りに足したので、出るようになった)

どちらも「本物の更新」なので、判断のスクリプトに決まりを2つ足しました。

No.決まり確かめ方
1前の調べのあとにカーネルを入れた日は、カーネルの部品の場所の「パッケージ外」「消えた」を数えないrpm -q kernel --qf '%{INSTALLTIME}\n' のいちばん新しい時刻が、ひとつ前の調べより新しいか
2WordPress のプラグイン・本体の変化は、公式の配っているファイルと指紋が合えば数えないWP-CLI の wp plugin verify-checksums プラグインの名前、wp core verify-checksums

どちらの決まりでも、「パッケージと違う」(パッケージのファイルが書き換えられた)は、必ず知らせます。プラグインで指紋が合わないもの、公式の置き場所にないプラグイン(指紋を確かめられないもの)、テーマ(指紋を確かめる仕組みがない)は、1行ずつではなく、プラグインやテーマごとに1行にまとめて知らせます。

判断のスクリプト(/usr/local/sbin/tripwire-filter)の、件数を数える行(N=$(wc -l < "$W/out"))の前に足したところです。

# カーネルを入れた日は、カーネルの部品の場所(パッケージの外で作られる・消える)を数えない
KNOTE=""
KLAST=$(rpm -q kernel --qf '%{INSTALLTIME}\n' 2>/dev/null | sort -n | tail -1)
if [ "${KLAST:-0}" -gt "$PREVT" ]; then
  KPAT='^\[(パッケージ外|消えた)\] ((/usr)?/lib/firmware/[0-9][^/]*/|(/usr)?/lib/modules/|/boot/(initramfs-|loader/entries/|\.?vmlinuz-|System\.map-|config-|symvers-)|/etc/sysconfig/kernel$)'
  KN=$(grep -cE "$KPAT" "$W/out" || true)
  grep -vE "$KPAT" "$W/out" > "$W/out2" || true
  mv "$W/out2" "$W/out"
  [ "$KN" -gt 0 ] && KNOTE="(カーネルを入れた日なので、カーネルの部品 ${KN}件は数えていない)"
fi

# WordPress のプラグイン・本体は、公式の指紋と合えば数えない。合わない・確かめられないもの、テーマは1行にまとめる
WPNOTE=""
WP="/usr/local/bin/wp --allow-root --path=/var/www/html"
if [ -x /usr/local/bin/wp ] && grep -qE '^\[(パッケージ外|消えた)\] /var/www/html/' "$W/out"; then
  for slug in $(grep -oE '^\[(パッケージ外|消えた)\] /var/www/html/wp-content/plugins/[A-Za-z0-9_.-]+' "$W/out" | sed -E 's#.*/plugins/##' | sort -u); do
    PP="^\[(パッケージ外|消えた)\] /var/www/html/wp-content/plugins/${slug}(/|$)"
    C=$(grep -cE "$PP" "$W/out" || true)
    grep -vE "$PP" "$W/out" > "$W/out2" || true; mv "$W/out2" "$W/out"
    if $WP plugin verify-checksums "$slug" 2>&1 | grep -q "Success: Verified 1 of 1 plugins"; then
      WPNOTE="${WPNOTE}(プラグイン ${slug} の更新 ${C}件は、公式の指紋と合ったので数えていない)"
    else
      echo "[プラグイン] ${slug}:${C}件(公式の指紋と合わないか、確かめられない)" >> "$W/out"
    fi
  done
  # (WordPress 本体の wp-admin・wp-includes も、wp core verify-checksums で同じように扱う。テーマはテーマごとに1行にまとめる)
fi
  • KLAST=$(rpm -q kernel --qf '%{INSTALLTIME}\n' | sort -n | tail -1):
    入っているカーネルのうち、いちばん新しく入れた時刻
  • if [ "${KLAST:-0}" -gt "$PREVT" ]:
    それが、ひとつ前の調べの時刻(PREVT、場面3のスクリプトにある)より新しければ、カーネルを入れた日
  • KPAT='^\[(パッケージ外|消えた)\] (…)':
    数えない行の形。カーネルの部品の場所だけで、[パッケージと違う] は入れない。\.?vmlinuz- は、頭に . が付いた隠しファイル(.vmlinuz-….hmac)も拾うため
  • grep -cE と grep -vE … > out2:
    当てはまる行の数を数え、当てはまらない行だけを残す
  • for slug in $(grep -oE '…/wp-content/plugins/[A-Za-z0-9_.-]+' … | sort -u):
    報告に出てきたプラグインの名前を、重なりなしで1つずつ取り出す
  • wp plugin verify-checksums "$slug" | grep -q "Success: Verified 1 of 1 plugins":
    そのプラグインのファイルが、wordpress.org の配っているその版と、指紋まで同じか。公式の置き場所にないプラグインは「とばした」のに終わりの番号が 0(成功)になるので、番号ではなく「1 of 1」の文字で見る
  • 合わない・確かめられないとき:
    そのプラグインを1行にまとめて知らせる

足したあと、その日の朝の報告で、判断し直しました。

myserver で実行したコマンドです。

sudo /usr/local/sbin/tripwire-filter /var/lib/tripwire/report/myserver-20261104-030001.twr
  • sudo /usr/local/sbin/tripwire-filter 報告のファイル:
    判断のスクリプトだけを、その日の報告で動かす(メールは出さず、画面に出す)

結果です(あいだの行は省いています)。

Tripwire:説明できない変化 20件(調べた変化 17818件のうち)(カーネルを入れた日なので、カーネルの部品 6009件は数えていない)(プラグイン google-site-kit の更新 2052件は、公式の指紋と合ったので数えていない)
[パッケージ外] /boot/grub2/grubenv
[パッケージ外] /etc/cron.daily/clamscan-www
[パッケージ外] /etc/resolv.conf
…
[テーマ] cocoon-master:1964件(テーマは指紋を確かめられないので、更新したかを自分で見る)
No.機械前後
1myserver10,044件20件(残りは、前の日に自分で直した設定と、自分で更新したテーマ)
2kvmhost6,027件20件(同じく、前の日の作業の分)

テーマの Cocoon は、前の日に自分で更新していたので、説明できる変化でした。カーネルやプラグインが更新された日も、見るべきものだけが短く届くようになりました。

まとめ

No.やったこと中身
1毎朝のメールの正体を見た変化が1つでもあれば報告を丸ごと送る作りで、毎朝の dnf の更新の分が全部届いていた
2変化を分けたrpm -qal と rpm -Va を1回ずつ作って突き合わせた。パッケージと合わないものは 0
3判断のスクリプトを作った記録のファイル、パッケージのとおりのもの、更新の日の古いファイルを外し、残りだけを短く出す
4メールの部分を差し替えた説明できない変化がある日だけ、短いメール。報告が読めないときは、エラーのメール
5見張りの範囲を広げた毎日動かすものやサービスの置き場所、ログインの設定と鍵、自分のスクリプト、Web のファイルなど
6パスフレーズを変えたホスト名から作った短いものを、でたらめな32文字に
7更新の日の決まりを足した(11/4)カーネルを入れた日はカーネルの部品を、WordPress のプラグイン・本体は公式の指紋と合えば数えない。テーマは1行にまとめる

rkhunter の記事と合わせて、毎朝のメールは 「来たら何かある」 形になりました。届いたメールに見覚えのない行があったら、そこから調べればよい、という状態です。毎朝の長いメールを読み流していたころより、ずっと安心できます。

Tripwire を入れたときの手順は「Tripwire を入れたときの記事」にあり、毎朝のスクリプトの形も、この記事に合わせて直しました。自宅サーバーの記事の一覧は「自宅サーバー構築メモ」にまとめています。

コメント