はじめに
サーバーにSSHでログインして設定ファイルを直接編集する運用は、多くの現場でいまだに残っています。障害対応や緊急のパラメータ変更のたびに手元でviを開き、保存して終わり――となりがちですが、後になって「いつ・誰が・何を・どう変えたのか」を聞かれると答えに窮することが多いのではないでしょうか。
本記事では、Linuxサーバーでこの「変更履歴」を自動的に記録する仕組みを、実際に構築しながら整理します。ゴールは次の3点を同時に満たすことです。
- 誰が、いつ、どのファイルに触れたかが分かること
- その変更が具体的にどう変わったか、差分(diff)まで追えること
- 障害時の復旧に使えるバックアップも兼ねられること
結論から言うと、「auditd(監査ログ)」と「git(差分管理)」を組み合わせ、gitの運用はetckeeperに任せることで、無理なく実現できます。
全体像
まず全体設計を先に示します。
- auditd: 「誰が・いつ・どのファイルに触れたか」という操作ログを記録する
- etckeeper + git: /etc配下をgitリポジトリとして自動コミットし、「何がどう変わったか」の差分を残す
- git-crypt: パスワードハッシュなど、差分履歴に残したくないファイルだけを暗号化する
- 外部サーバーへのpush: ホストごとにリポジトリと鍵を分離した上で、バックアップも兼ねて複製する
以下、順番に組み立てていきます。
auditdで「誰が・いつ・何に」触れたかを記録する
ディレクトリ単位の監視ルール
auditdは、指定したファイルやディレクトリへのアクセスをsyscallレベルで監視できます。単純にディレクトリを指定するだけなら-w(watch)で足りますが、これは対象ディレクトリ直下しか監視できず、サブディレクトリの中までは追いかけません。
サブディレクトリも含めて再帰的に監視したい場合は、-aと-F dir=を組み合わせた構文を使います。
auditctl -a always,exit -F dir=/etc -F perm=wa -k etcwatch
perm=waは「書き込み(write)」と「属性変更(attribute change)」を意味します。これで/etc配下のファイルに対する変更操作がすべて監査対象になります。
sshログイン由来の操作だけに絞り込む
このルールだけだと、systemdが起動するデーモンやcronジョブなど、ログインを経由しないプロセスの操作までノイズとして記録されてしまいます。ここで使えるのがloginuid(auid)というフィールドです。
PAM(pam_loginuid)は、ユーザーが最初にログインした瞬間に「そのユーザーのUID」をプロセスに記録します。この値はsuやsudoで別ユーザーに切り替えても書き換わらず、子プロセスにも引き継がれます。一方、ログインを経由しないプロセス(デーモンやcron)では、この値は「未設定」(4294967295、符号なし32bitの-1)のままになります。
つまり、次のように条件を足すことで、対話的なログインセッション(このサーバーではsshログインのみを許可している前提であれば、実質sshログイン)経由の変更だけに絞り込めます。
auditctl -a always,exit -F dir=/etc -F perm=wa -F auid!=4294967295 -k etcwatch
厳密に「sshかどうか」まで確認したい場合は、記録されたauidとses(セッションID)を使って、ログイン時の監査レコード(ausearch –session <ses番号>)を突き合わせます。ログインレコードにはterminal=sshや接続元IP(addr=)が記録されているため、そこで初めて裏付けが取れます。ルール自体で「ssh限定」を直接指定する仕組みはない点は覚えておいてください。
陥りがちな罠
構築中に実際にハマったポイントを2つ紹介します。
罠1: auditdサービス自体が起動していないと何も記録されない
auditctlはカーネルに直接ルールを送るため、ルールの登録自体はauditdデーモンが停止していても成功します。しかし実際にイベントを受け取ってディスクに書き出すのはauditdデーモンの仕事なので、デーモンが停止していると、ルールは有効なのにログが一切残らないという状態になります。まずはsystemctl status auditdでデーモンが動いているかを確認する必要があります。
罠2: auditd起動時にルールがリセットされる
auditctlで手動投入したルールは揮発性で、systemctl start auditd(またはauditd再起動)のたびに、/etc/audit/rules.d/*.rulesから読み込み直される過程でクリアされてしまいます。恒久的に有効にするには、ルールをファイルとして書いておく必要があります。
cat > /etc/audit/rules.d/etcwatch.rules << 'EOF' -a always,exit -F dir=/etc -F perm=wa -F auid!=-1 -k etcwatch EOF augenrules --load auditctl -l # ロードされたか確認
ここまで設定すると、/etc/hostsを編集した際に次のようなイベントが記録されます(抜粋・簡略化)。
type=SYSCALL syscall=rename ... auid=alice uid=root ses=1234
tty=pts0 comm=vi exe=/usr/libexec/vi key=etcwatch
type=PATH item=0 name=/etc/hosts nametype=DELETE
type=PATH item=1 name=/etc/hosts~ nametype=CREATE
auid=aliceがログインユーザー、sesがセッションID、tty=pts0が疑似端末(=リモート接続)であることを示しています。これで「誰が・いつ・どのファイルに」触れたかは記録できるようになりました。
差分(diff)を残す: etckeeperによるgit管理
auditdは操作の証跡は残せますが、変更前後の中身の差分までは記録しません。syscallの発生を記録するだけで、テキストの内容自体は保持されないためです。差分まで欲しい場合は、別の仕組みが必要になります。
ここで使えるのがetckeeperです。/etcをgitリポジトリとして管理し、パッケージ管理操作時や定期実行(cron)のタイミングで自動的にコミットしてくれるツールで、まさにこの用途のために作られています。
dnf install etckeeper cd /etc etckeeper init etckeeper commit "initial snapshot"
vi編集中の一時ファイルを除外する
viで保存すると、実際には対象ファイルを直接上書きするのではなく、スワップファイル(.hosts.swpのような隠しファイル)や書き込み権限確認用の一時ファイル(4913という数字だけのファイル名)、バックアップファイル(hosts~)が/etc直下に一瞬だけ作られます。これらはauditdのログにもそのまま記録されていた通りで、gitの差分履歴には不要なノイズです。commit対象から外しておきます。
cat >> /etc/.gitignore << 'EOF' .*.swp .*.swpx 4913 *~ EOF cd /etc etckeeper commit "add gitignore for vi swap/temp files"
etckeeperはデフォルトである程度の除外パターンを持っていますが、ディストリビューションやバージョンによって内容が異なるため、vi系の一時ファイルは念のため明示的に追加しておくと安心です。
以後は次のコマンドで、任意のファイルの変更履歴を差分付きで確認できます。
git -C /etc log -p -- hosts
auditdのログ(誰が・いつ)と、gitのコミット(何がどう変わったか)は、タイムスタンプを軸に突き合わせて使います。
git -C /etc log -p --since="10 minutes ago" -- hosts # 何が変わったか ausearch -k etcwatch -i -ts recent # 同じ時間帯に誰が触ったか
シークレットファイルの扱い: git-cryptによる暗号化
ここで一つ大きな注意点があります。/etcには/etc/shadow(パスワードハッシュ)のような機微な情報を含むファイルも存在します。これを無条件でgit管理下に置くと、パスワードハッシュの変更履歴が半永久的にgitログへ残ることになります。gitの履歴は一度コミットされると、後からファイルを除外しても過去のコミットオブジェクトとしては残り続けます(履歴自体を書き換えない限り)。
「バックアップも兼ねたいので対象から外したくない」という場合は、ファイルそのものを除外するのではなく、git-cryptでそのファイルだけを暗号化する方法が現実的です。
dnf install git-crypt cd /etc git-crypt init
.gitattributesに暗号化したいファイルを指定します。
shadow filter=git-crypt diff=git-crypt gshadow filter=git-crypt diff=git-crypt security/opasswd filter=git-crypt diff=git-crypt ssh/ssh_host_*_key filter=git-crypt diff=git-crypt
こうしておくと、作業ディレクトリ上(/etc/shadow自体)はOSが読める平文のままですが、gitのオブジェクトストア内では暗号化された状態で保存されます。鍵を持つ人だけが復号して差分を見られる状態になり、リポジトリを複製・バックアップしても、鍵がなければ中身は読めません。鍵は/etcとは別の、鍵管理システムなど安全な場所に保管しておきます。
なお、ネットワーク経路を接続元IP制限やSSH鍵認証で固めることは意味のある追加対策ですが、それだけでは不十分です。ネットワーク制限は「その瞬間の接続」を防ぐものであり、発生源のサーバー自体がroot権限で侵害された場合や、バックアップ先のディスクが物理的に持ち出された場合には無力です。パスワードハッシュの履歴は一度でも漏れると過去分がまとめて流出するという性質上、経路の防御とデータそのものの暗号化は両方セットで考える必要があります。
バックアップも兼ねて外部サーバーへ複製する
etckeeperのコミットはローカルの/etc内で完結しますが、障害時の復旧やサーバーの改ざん耐性を考えると、外部への複製もしておきたいところです。ここで言う「gitサーバー」は、GitLabのような専用製品である必要はなく、git init --bareしただけのディレクトリで十分です。
ホストごとにリポジトリと鍵を分離する
複数台のサーバーを対象にする場合、バックアップ先ではホスト名ごとにリポジトリを分けて管理するのが安全です。
mkdir -p /home/etcbackup/repos git init --bare /home/etcbackup/repos/server01.git git init --bare /home/etcbackup/repos/server02.git
さらに、各サーバーには専用のSSH鍵を持たせ、authorized_keys側でcommand=により「その鍵は自分のリポジトリにしか触れない」ように強制します。
command="git-shell -c \"git-receive-pack '/home/etcbackup/repos/server01.git'\"",no-agent-forwarding,no-port-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAA...server01の公開鍵...
command=は、SSH認証時に「クライアントが何を要求してきても、サーバー側はこの固定コマンドしか実行しない」という仕組みです。これにより、1台のサーバーの鍵が漏れても、被害はそのサーバー自身のリポジトリに限定されます。接続元IP制限(sshd_configのMatchブロックやファイアウォール)と組み合わせれば、経路と権限の両方を絞り込めます。
各サーバー側では、ホスト名を変数化した上でremoteを設定し、初回pushを行います。
HOST=$(hostname -s)
cd /etc
git remote add origin ssh://etcbackup@backup-host/home/etcbackup/repos/${HOST}.git
git push -u origin master
以降の自動コミットも自動的にpushされるよう、etckeeperのフックに追加しておきます。
cat > /etc/etckeeper/commit.d/60push << 'EOF' #!/bin/sh cd /etc && git push origin master --quiet || true EOF chmod +x /etc/etckeeper/commit.d/60push
これで、cronによる定期コミットのたびに、外部サーバーへも自動的に複製される状態になります。
まとめ
今回組み立てた仕組みを整理すると、次のようになります。
- 誰が・いつ・何に触れたか → auditd(ディレクトリ単位の監視 + loginuidによる絞り込み)
- 何がどう変わったか → etckeeperによるgit自動コミット
- 機微な情報の保護 → git-cryptによるファイル単位の暗号化
- 障害対策・改ざん耐性 → ホスト・鍵を分離した外部リポジトリへの複製
どれも単体では既存のツールですが、組み合わせることで「SSHで何かいじった後に、あとから誰が何をどう変えたか」を無理なく追跡できる体制になります。特にauditdはサービス起動やルールの永続化を忘れると簡単に「記録できているつもり」の空振り状態になるので、導入時は実際に変更を入れてログが残ることを一度手元で確認しておくことを強くおすすめします。


