えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

SELinuxの実践

SELinux_logo

SELinuxシリーズ

本記事は、SELinuxシリーズの4記事目です。

  1. Linuxプロセスアクセス制御の概要
  2. SELinuxの概要
  3. SELinux Type Enforcement
  4. SELinuxの実践 ←今ココ
  5. (参考) SELinuxのRBAC、UBAC、MLS、MCS
  6. (参考) SELinux Module Policyのソースコード読解、ビルド
  7. 参考URL

1〜3記事目は、4記事目を理解するための前提知識をカバーしています。
4記事目が最も重要で、SELinuxの具体的な操作方法やコマンド、トラブルシューティング手順を紹介しています。

5記事目以降は参考情報です。

SELinuxの関連記事は、SELinuxタグから探せます。

一連の記事はFedora環境を前提として書いています。
FedoraRHELに類するディストリビューションであればほぼ同等の挙動になると思いますが、他のディストリビューションでは挙動に差異がある可能性があるのでご注意ください。

お伝えしたいこと

前回のType Enforcementの記事では、デフォルトのtargeted policyに限定しつつもSELinuxを理解するために必要な理論を一通り網羅しました。

本記事では、前回の記事の内容をベースにSELinuxの実践的な知識についてカバーしたいと思います。
具体的には、「SELinuxでアクセス制御エラーのログを検知したときの対処法」を中心に紹介します。

網羅性を重視した結果、本記事には滅多に使わない知識も含まれています。
そういった部分にはセクション名に "(参考)" をつけましたので、関連性が薄いと感じたら必要に応じて読み飛ばし、次に進んでください。

(参考) SELinuxの運用に必要な知識

SELinuxの運用に必要な知識とは、主に「アクセス拒否の対処法」です。
私がそう考えた理由について、以下に補足します。

SELinuxは、ホワイトリスト形式でプロセスのアクセス制御を実装します。
allowルールで許可されないアクセスパターンは、全て拒否されます。

SELinuxのデフォルトの動作モードは、targeted policyです。
targeted policyにおいては、代表的なデーモンプロセスがデフォルトのファイルパスやポート番号で動作するためのアクセス許可設定がある程度網羅的に設定されています。

つまり、特に構成を変更せずにLinuxを使っている限りは、SELinuxのエラーに悩まされることはほぼありません。
Linuxの構成を変更した場合も、多くの場合はsemanageコマンドによってSELinuxのルールを部分的にカスタマイズすることで、新たなアクセスパターンを許可することができます。

場合によっては、デフォルトで許可されたallowルールを取り消して、自分たちが使うルールのみ有効化したいと思うこともあるかもしれません。
しかし、SELinuxにおいて一度許可したルールを後から拒否することはできません。

denyルールというものは存在しませんし、デフォルトでインストールされたModule Policyをアンインストールすることもできません。
(※) Module Policyについては、後続記事の(参考) SELinux Module Policyのソースコード読解、ビルド - Base PolicyとModule Policyに説明があります

SELinuxは「あるプロセスが本来必要なアクセスをすべて許可し、不要なアクセスを拒否する」という動作をします。
脆弱性を突いてroot権限を持つデーモンプロセスの制御を奪取しても、そのデーモンプロセスが本来想定していないような用途のアクセス (※) は拒否されます。
(※) 例えばバックドアのための不正なポート番号のlistenや、無関係なファイルへのアクセスなど

こういった動作によって、システムに侵入された後のリスクを軽減するのがSELinuxの役割であると私は理解しています。
デフォルトで許可されたルールは「そのプロセスにとって本来必要なアクセスを許可するもの」であるため、これを拒否するdenyルールのような仕組みは必要ないと思います。

許可されたアクセスを新たに拒否できない以上、SELinuxの運用においてできることは基本的には「追加でアクセス許可すること」のみです。
言い換えると、SELinux起因のアクセス拒否を直せるようになれればSELinuxの運用知識としては十分だと私は考えています。
アクセス拒否を直す方法は複数ありますが、適切な方法を選択できるようにSELinuxの既存のルール構成も自力で調査できるようになるとなお良いです。

本記事の序盤ではSELinuxの基本操作を扱いますが、メイントピックはアクセス拒否の直し方です。
#エラー切り分けの流れ以降のセクションにて、アクセス拒否の原因調査、そして修正方法について取り扱います。

RPMパッケージ

以下のパッケージはデフォルトで入っていない可能性があるので、必要に応じて導入してください。
Fedoraにおいては、policycoreutils-python-utilssetroubleshoot-serverは初めから入っていました。

特に、policycoreutils-python-utilssetools-consoleは、SELinuxを操作するために必須とも言えるツールです。
またsetroubleshoot-serverpolicycoreutils-restorecondも運用上役に立つので、必要に応じて導入をご検討ください。

パッケージ名 内容
policycoreutils-python-utils semanage, audit2allowコマンドを含む
setools-console seinfo, sesearchコマンドを含む
#(参考) SELinuxの確認コマンド
setroubleshoot-server 通常のアクセス拒否の監査ログ発生直後に、わかりやすい説明文と解決策をシステムログに出力する
#setroubleshoot-serverが出力する追加情報
selinux-policy-doc *_selinuxというmanページを大量に含む
#(参考) manの横断検索
policycoreutils-restorecond restorecondデーモンを含む
#restorecond
policycoreutils-devel Custom Policyをソースコードから記述してビルドするために必要
#(参考) Custom Policyのビルド

SELinuxのステータス

enforcing/permissive/disabled

SELinuxは3つのステータス値のいずれかを取ります1
デフォルトはenforcing (有効) です。

ステータス ラベル付け 監査ログ出力 アクセス制御
enforcing する する する
permissive する する しない
disabled しない しない しない

基本的には、セキュリティを保つためにenforcingのままにしておくべきです。

以下の場合においてはpermissive (拒否しない) が便利です。
permissiveは基本的にSELinux有効なのですが、アクセス拒否をしない部分のみenforcingとは異なります。
アクセス拒否はしませんが、アクセス許可されない場合は監査ログに出力します。

  • permissiveが便利な場面
    • 構築したサーバーの単体試験の一環で、一通りの操作をしてSELinuxのエラーログを洗い出したいとき。エラーログが出たら、出なくなるようにチューニングした上で再度動作確認する
    • 必要なアクセスが拒否されて運用に支障が出ているとき (セキュリティが損なわれるので、あくまで一時的な復旧策として)

disabledは、SELinuxの完全な無効化です。

disabledの状態で作成されたObjectには、Security Contextが割り当てられないということに注意が必要です。
disabledからenforcingに切り替える際は、必ずrelabelしてください。
さもないとファイルのSecurity Contextが想定外の値になり、大量のアクセス拒否が発生する恐れがあります。
relabelの手順は、#全ファイルをrelabelするにて説明します。

disabledにすることはおすすめしません。
上記の通りラベル付けされなくなり、トラブルを誘発するリスクがあるためです。
どうしてもSELinuxによるアクセス拒否を全て止めたい場合には、permissiveを推奨します。
disabledは、「SELinuxを無効化して二度と有効化するつもりはない」ときに使うものです。

SELinuxによるアクセス制御を無効化すると、セキュリティ観点でリスクが上がります。
具体的には、脆弱性を突かれるなどしてroot権限を持つプロセスの制御権を奪われた時の防御手段が1つ失われます。
参考: Linuxプロセスアクセス制御の概要 - #MAC (Mandatory Access Control)

設定ファイルによるステータス変更

/etc/selinux/configを書き換えてOS再起動することで、SELinuxのモードを変更できます。

SELINUX=enforcing
# SELINUX=permissive
# SELINUX=disabled → この設定値は今後サポートされなくなる可能性あり

この設定ファイルを使って設定を書き換えることは、恐らくあまりないでしょう。
あるとしたら、SELinuxによるアクセス拒否を常時無効化したい場合にSELINUX=permissiveに書き換えるぐらいです。

一時的にenforcingからpermissiveに書き換える場合は、sudo setenforce 0コマンドを使う方が便利です。
setenforceコマンドはSELinuxがenforcingかpermissiveの状態であれば使えます。
sudo setenforce 1でenforcingに、sudo setenforce 0でpermissiveになります。

最後に一点補足しますが、SELINUX=disabledというオプションは今後サポートされなくなる可能性があります。
経緯の詳細と代わりの手順については、#(参考) Kernel Command Line ParameterによるSELinuxの無効化を参照してください。
繰り返しになりますが、disabledにすることはそもそもおすすめしません。

ステータス確認

SELinuxのステータスは、2通りのコマンドで確認できます。
どちらを使っても良いです。

getenforce
# Enforcing

sestatus
# SELinux status:                 enabled
# SELinuxfs mount:                /sys/fs/selinux
# SELinux root directory:         /etc/selinux
# Loaded policy name:             targeted
# Current mode:                   enforcing
# Mode from config file:          enforcing
# Policy MLS status:              enabled
# Policy deny_unknown status:     allowed
# Memory protection checking:     actual (secure)
# Max kernel policy version:      33

(参考) Kernel Command-Line ParameterによるSELinuxの無効化

Fedora34、及びRHEL9よりSELINUX=disabledによるSELinuxの無効化はサポートされなくなりました。 2,3
RHEL8においてはまだサポートされていますが、既に非推奨とされています4

なお繰り返しになりますが、SELinuxの無効化はそもそもおすすめしません。

一時的に無効化する方法

今後もサポートされる方法は、Linux KernelのCommand-Line Parameterにselinux=0を渡すことです5
Linux起動時にこのパラメータを指定するには、Linuxの起動中にコンソール画面で以下の操作を行います。

(1) LinuxのBootloaderのKernel選択画面で、eを押すことでBoot Entryの編集画面に入る

boot_parameter1

(2) linuxで始まる行にselinux=0というオプション指定を書き加える。既存のパラメータとはスペースで区切る

boot_parameter2

(3) Ctrl+xを押下してLinuxを起動する

Bootloaderにおけるパラメータ指定はLinux起動前に行うため、SELinuxのエラーによってLinuxの起動自体が失敗する事態においても有効です。
selinux=0の代わりにenforcing=0を指定するとpermissiveモードで起動することも覚えておくと、いざというときに便利です。

永続的に無効化する方法

Kernel Command-Line Parameterのデフォルト値にselinux=0を指定すると、OS再起動してもSELinuxの無効化が持続します。
今回は、grubbyコマンドを使ってBoot Loader (GRUB2) の設定を変更します。
grubbyコマンドを使うには、grubby RPMパッケージが必要です。

まずは現状の設定を確認します。
注目すべきはargs=の行で、デフォルトではselinux=0が入っていません。

ALLを指定すると全Boot Entry分の設定を表示します。
DEFAULTを指定すると、Linux起動時にデフォルトでカーソルのあっているBoot Entryの設定のみ表示します。
1などの数字を指定すると、特定のindexの設定のみ表示します。

# /boot/loader/entries
sudo grubby --info ALL
# (一部抜粋)

# index=0
# kernel="/boot/vmlinuz-5.14.18-300.fc35.x86_64"
# args="ro resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet"

# index=1
# kernel="/boot/vmlinuz-5.14.18-100.fc33.x86_64"
# args="ro resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet"

今回の設定変更手順では、以下のファイルも更新されます。

grep GRUB_CMDLINE_LINUX /etc/default/grub
# GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet"

以下のコマンドで全てのBoot Entryにselinux=0オプションをデフォルトで追記します。
ALLの代わりにDEFAULTやindex番号を指定することもできますが、Boot Entryごとにパラメータを分ける使い方をしないのであればALLで良いと思います。

sudo grubby --update-kernel ALL --args 'selinux=0'

変更後のパラメータを確認します。
オプションの末尾にselinux=0が追加されています。

# /boot/loader/entries
sudo grubby --info ALL
# (一部抜粋)

# index=0
# kernel="/boot/vmlinuz-5.14.18-300.fc35.x86_64"
# args="ro resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet selinux=0"

# index=1
# kernel="/boot/vmlinuz-5.14.18-100.fc33.x86_64"
# args="ro resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet selinux=0"

grep GRUB_CMDLINE_LINUX /etc/default/grub
# GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_fedora-swap rd.lvm.lv=fedora_fedora/root rd.lvm.lv=fedora_fedora/swap rhgb quiet selinux=0"

OS再起動すれば、SELinuxが無効化されます。

sudo reboot

selinux=0オプションを消すには、以下のコマンドを実行します。

sudo grubby --update-kernel ALL --remove-args 'selinux'

Bootloader、/etc/default/grubgrubbyに関するより詳細な情報は、Red Hat社のマニュアルに書いてあります。
RHEL8 - System Design Guide - Configuring kernel command-line parameters

(参考) Boot Entryとは

Boot Entryとは、Linux起動画面に出てくる項目のことです。
下記画像では4つのBoot Entryが存在します。
grubbyがインストールされていれば、sudo grubby --info ALLでも確認できます。

boot_parameter1

Boot EntryはKernel Command-Line Parameterと紐づく概念で、Linux Kernelを更新するたびに1つ追加されます。
更新後のLinux Kernelに不具合があるとLinuxを起動できなくなる恐れがあります。
不具合が起こった場合は、過去バージョンのKernelと紐づくBoot Entryを選択することでLinuxを起動し、復旧操作を行える仕組みになっているようです。
最近のFedora系のディストリビューションでは、systemdが提供するレスキューモードもBoot Entryから選択可能になっています。

Linux Kernelは複数バージョン同時に存在できるようになっており、更新のたびにkernelパッケージが増えていきます。

dnf list kernel --installed
# Installed Packages
# kernel.x86_64  5.8.15-301.fc33  anaconda
# kernel.x86_64  5.14.18-100.fc33  updates 
# kernel.x86_64  5.14.18-300.fc35  @updates 

エラー切り分けの流れ

SELinuxによるアクセス拒否を検知した時、以下の流れで切り分けを行います。
切り分けの方法は固定化されているわけではありません。
ここで紹介するのは、あくまで私のやり方です。

  1. SELinux以外の確認
  2. SELinuxのエラーログ確認
  3. Permissiveで再現させる
  4. File Context周りの確認
  5. ポート番号と紐づくSecurity Contextの確認
  6. Boolean有無の確認
  7. Custom Policyの自力開発の検討

各工程の概要を次のセクションで説明します。
詳細手順は別セクションに書きましたので、リンクから飛んでください。

1. SELinux以外の確認

アクセス拒否された場合、そのアクセスを行っているアプリケーションで問題が発生しているはずです。
まずはそのアプリケーションの状態を確認します。

アプリケーションがPermission deniedかそれに類するエラーを出している場合は、SELinuxによるアクセス拒否が発生している疑いが強まります。

しかし、SELinuxの観点に映る前に、先にDAC (File Permissions + POSIX ACL)の問題が発生していないかを確認しましょう。

過去記事のLinuxプロセスアクセス制御の概要 - DACとMACは両方使われるでも取り上げましたが、LinuxにおいてはDACMACの順にアクセス可否が評価されます。
DACに問題がある場合は、SELinuxに処理が渡るより先にエラーになり、SELinuxのエラーログが出ません。

DACに問題がないことを確認できたら、次に進みます。

2. SELinuxのエラーログ確認

SELinuxがアクセス拒否をすると、syslogと監査ログが出力されます。
以下のコマンドでそれぞれのログを確認できます。
ログが出ていた場合、SELinuxがアクセス拒否していることが確定します。

  • sudo ausearch -m avc,user_avc (監査ログの表示)
  • journalctl -qen all -t setroubleshoot (setroubleshoot-serverによるヒントログの表示)

前者の監査ログからは以下の情報がわかります。

  • 拒否されたアクセスの詳細 (source_type, target_type, class, permission)
  • sourceのプロセス名、プロセスID
  • targetがファイルだった場合はファイル名

後者のsetroubleshootのログには、以下の情報が含まれます。
個人的にはファイルのフルパスを確認できるのが一番便利だと感じます。

  • sourceのプロセス名
  • targetがファイルだった場合はフルパス
  • 解決方法の提案

最後の解決方法の提案ですが、必ずしも最適な解決方法が提案されるとは限りません。
あくまで提案は提案として参考にしつつ、最終的には自分で適切な解決方法を判断することが重要です。

詳細については、#SELinuxのエラーログ確認にてログの読み方も含めて説明します。

3. Permissiveで再現させる

SELinuxでエラーが発生していることを確認できた場合、可能であればPermissiveモードに変更してからエラーメッセージを再現することをおすすめします。

単一コマンドの実行であっても、細かく見れば複数のアクセスが発生しているケースがあります(例: ファイルのopen,read)
Enforcingの場合、openに失敗するとその時点で処理が止まります。
仮にopenを許可した場合、今度はreadに失敗します。
このように、Enforcingのまま切り分けを進めると時間がかかることがあります。

Permissiveであればエラーが発生しても処理を中断しないので、上記の例で言えばopenとreadの両方について監査ログを出力します。
両方の原因に対処して動作確認すれば、次は一発で処理が通ります。

具体的なコマンドは、以下のとおりです。

# EnforcingからPermissiveに変更
sudo setenforce 0

# (エラーの再現)
# (エラーログの確認)

# PermissiveからEnforcingに変更
sudo setenforce 1

セキュリティ観点で本番環境のサーバーをPermissiveにすることは難しいと思います。
少々手間はかかりますが、原則としては検証環境などでこの工程を実施することをおすすめします。

4. File Contextとの差分確認

SELinuxの拒否ログで、targetがファイルである場合はFile Contextとの差分が発生しているかを疑ってください。

ファイルのSecurity ContextがFile Contextと差分がないかを確認します。
最もよく使うコマンドは、以下の2つです。

  • matchpathcon -V ファイル名 (File Contextとの差分チェック)
  • restorecon -v (relabelによる修正)

ファイルのSecurity Contextがアクセス制御ルールの設計と異なる場合、allow Statementとマッチしなくなることでアクセス拒否されてしまいます。
File Context通りのTypeを持っていれば問題ないことがほとんどなので、多くの場合はrelabelによって問題を解決できます。

運用上は、mvコマンドによってファイル移動した時にこの状況がしばしば発生します。
mvはSecurity Contextを含めてファイルの属性情報を全て保持してファイルを移動します。
ホームディレクトリで作ったファイルを/var/etc配下に移動すると、これらのフォルダ配下にuser_home_t Typeのファイルが生成し、アクセス拒否の原因となります。

詳細については、#File Contextとの差分確認にて上記以外のコマンドや修正例も含めて説明します。

5. ポート番号と紐づくSecurity Contextの確認

SELinuxの拒否ログで、targetがTCP/UDPポート番号 (*_port_t) の場合は、ポート番号がデフォルト値から変更されていないかを思い返してみてください。

正しく設定されていれば、source (*_t) とtarget (*_port_t)*に入る文字列が似たものになるはずです。
エラーが発生している場合は、上記の文字列が異なっていることが多いです。

例えば、source=httpd_t, target=http_port_tが正しい姿です。
source=httpd_tに対して、target=ssh_port_tのように明らかに異なる場合は、ポート番号とSecurity Contextの紐付けを修正するか、ポート番号そのものを変更することをご検討ください。

ポート番号とSecurity Contextの紐付けを確認するには、主に以下のコマンドを使用します。

  • seinfo --portcon

ポート番号とSecurity Contextの紐付けを変更して問題を解決するには、以下のコマンドを実行します。

  • sudo semanage port -a ...

詳細については、#ポート番号と紐づくSecurity Contextの確認にて上記以外のコマンドや修正例も含めて説明します。

6. Boolean有無の確認

Object (target) に割り当てられたSecurity Contextが誤っている疑いは、この時点でほぼ払拭されています。
以降は既存のアクセス制御ルールを変更するアプローチになります。

自力でルールを記述するよりも先に、まずは今の状況にマッチしたBooleanが存在しないかを先に確認しましょう。
メジャーなケースであれば、専用のBooleanが用意されていることが少なくありません。

Booleanを探すには、以下のコマンドが有効です。

  • sesearch -A -s ... -t ... -c ... -p ...
  • sudo semanage boolean -l または getsebool -a

まずは、SELinuxの拒否ログと全く同じ条件でsesearchでアクセス許可ルールを検索します。
実際に拒否されているのでアクセス許可ルールは通常ヒットしないのですが、Booleanが存在する場合は以下のような出力を得られます。
以下の例では、httpd_read_user_contentというBooleanがTrueの場合のみ有効化されるallowルールが検索にヒットしました。

sesearch -A -s httpd_t -t user_home_t -c file -p read,open
# allow httpd_t user_home_type:file { getattr ioctl lock open read }; [ httpd_read_user_content ]:True

候補となるBooleanが見つかった場合は、sudo semanage boolean -lなどでBooleanの意味を確認します。
そしてBooleanの意味をより厳密に確認するために、sesearch -bで確認します。

Boolean値が今回の状況にマッチしそうな場合は、以下のいずれかのコマンドでBoolean値を変更します。

  • sudo setsebool -P ...
  • sudo semanage boolean -m ...

詳細については、#Boolean有無の確認にて修正例も含めて説明します。

7. Custom Policyのビルド

上述のいずれの切り分けでも対処できない場合、「最後の手段として」Custom Policyのビルドという手段があります。

自力でallow Statementやtype_transition Statementを記述したソースコードを書き、それをビルド・インストールする方法です。
この方法を使えば、ほぼどんな定義やルールでも追加できます (※)
(※) SID (Security ID) など、Base Policyにしか書けない定義やルールもあります。ここで可能なのは、あくまでModule Policyのソースコードに書けるルールのみです。とはいえ、ほぼ何でもできると思って差し支えありません

具体的には、Custom Policyによって以下のようなことができます。

  • Typeの定義
  • Attributeの定義
  • type_transition Statementの追加
  • allow Statementの追加
  • Booleanの定義
  • File Contextの定義

BooleanやFile Contextについては、semanageコマンドによっても追加できます。
使い分けとしては、Custom PolicyのType定義やallow Statementに関連するものであれば、Custom Policyに含めるのが一般的です。
そうすることで、関連する一連の定義やルールをひとまとめに管理できます。

Custom Policyを追加する状況は、例えば以下が挙げられます。

  • 既存のルール設定にallow Statementを追加したい場合
  • 自作のアプリケーションをSELinuxでアクセス制御したい場合

具体的な手順については、#(参考) Custom Policyのビルドで紹介します。

SELinuxのエラーログ確認

本セクションでは、#2. SELinuxのエラーログ確認の詳細な確認手順を説明します。

一番よく使うのは、以下のコマンドです。

  • sudo ausearch -m avc,user_avc
  • journalctl -qen all -t setroubleshoot

ログの表示コマンド

SELinuxのエラーログは2種類出力されます。

  1. AVC (Access Vector Cache) が出力するアクセス拒否のログ
  2. setroubleshootが出力する追加情報のログ

2のログはsetroubleshoot-server RPMパッケージがインストールされている場合のみ出力されます。

「事象の把握」という意味では、1のAVCから出るログが有用です。
しかし、1のログではファイルへのアクセスが拒否された時のファイルのフルパスが書いてないことがあるので、必要に応じて2のログも確認します。

1のログはaudit2allowコマンドの入力値として利用することがあります。
この使い方については、#(参考) Custom Policyのビルドで説明します。

AVCが出力する監査ログ

SELinuxでアクセス拒否が発生すると、AVCが監査ログ (/var/log/audit/auditd) を出力します。
このログは、sudo ausearch -m avc,user_avcで確認できます。

sudo ausearch -m avc,user_avc

# ----
# time->Sun Nov 21 17:29:41 2021
# type=AVC msg=audit(1637483381.836:583): avc:  denied  { read } for  pid=10602 comm="mandb" name="anki.1" dev="dm-0" ino=919333 scontext=system_u:system_r:mandb_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

上記のログから、以下のような情報を読み取れます。
allow Statementと同等の情報が出力されていることがわかると思います。

  • source (mandb_t)
  • target (user_home_t)
  • class (file)
  • permission (read)

sourceについてはPID (10602) とプロセス名 (mandb) も表示されます。
targetについてはファイル名が表示されますが、フルパスはわかりません (anki.1)
フルパスを確認したい場合は、次のセクションで扱うsetroubleshootのログを見ます。

以下のコマンドは、auditdが停止しているなどの理由でausearchが利用できない場合に使います6
auditdが停止していることは基本ないので、こちらのコマンドを覚える必要はありません。

# dmesg: auditdが動作していない時の代替手段
dmesg | grep -i -e type=1300 -e type=1400

また、Fedora環境の場合は監査ログが/var/log/messagesにも出力されていたので、以下のようにjournalctlでもSELinuxの拒否ログを確認できました。
RHELCentOS Streamでは/var/log/messagesjournalctlでの拒否ログの確認はできませんでした。

journalctl -qen all -t audit -g denied
# Nov 21 17:29:41 pc audit[10602]: AVC avc:  denied  { read } for  pid=10602 comm="mandb" name="anki.1" dev="dm-0" ino=919333 scontext=system_u:system_r:mandb_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

(※) -g avcよりも-g deniedの方がより必要な情報に絞ってフィルタしてくれます

setroubleshoot-serverが出力する追加情報

setroubleshootのログはsetroubleshoot-server RPMパッケージがインストールされている場合のみ出力されます。
AVCから監査ログが出力された少し後のタイミングで、/var/log/messagesに出力されます。

以下のサンプルでは、2つのログが出力されています。
1つ目が発生したエラーの説明、2つ目が解決案の提示です。

journalctl -qen all -t setroubleshoot

# Nov 21 17:29:45 pc setroubleshoot[11309]: SELinux is preventing mandb from read access on the file /usr/local/share/man/man1/anki.1. For complete SELinux messages run: sealert -l 5fe15040-2828-45d4-9e8c-ba8ef5781937


# Nov 21 17:29:45 pc setroubleshoot[11309]: SELinux is preventing mandb from read access on the file /usr/local/share/man/man1/anki.1.

#    *****  Plugin restorecon (99.5 confidence) suggests   ************************

#    If you want to fix the label. 
#    /usr/local/share/man/man1/anki.1 default label should be usr_t.
#    Then you can run restorecon. The access attempt may have been stopped due to insufficient permissions to access a parent directory in which case try to change the following command accordingly.
#    Do
#    # /sbin/restorecon -v /usr/local/share/man/man1/anki.1

#    *****  Plugin catchall (1.49 confidence) suggests   **************************

#    If you believe that mandb should be allowed read access on the anki.1 file by default.
#    Then you should report this as a bug.
#    You can generate a local policy module to allow this access.
#    Do
#    allow this access for now by executing:
#    # ausearch -c 'mandb' --raw | audit2allow -M my-mandb
#    # semodule -X 300 -i my-mandb.pp

1つ目のログにはあまり目新しい情報はありませんが、ファイルのフルパスが書いてあります。

2つ目のログでは、アクセス拒否を修正するための方法を2つ提示しています。
今回の場合は、以下の内容が書かれています。

  • restorecon /usr/local/share/man/man1/anki.1.で直る (99.5%の確度)
  • allowルールを追加し、Custom Moduleとしてインストールする (1.49%の確度)

上記の提案に従えばエラーは解消できますが、最適な解決策とは限りません。
あくまで参考情報として扱い、最終的には自分で判断する必要があります。
setroubleshootが提案しなかった解決策を選択することもしばしばあります。

journalctlが使えない環境では、/var/log/messagesを直接開いてsetroubleshootのログを確認します。

(参考) journalctlの利便性

journalctlは、journaldデーモンが収集したログを一括表示するコマンドです。
以下の点で使い勝手が良いです。

  • journalctlはadmwheel、またはsystemd-journalグループに所属する一般ユーザーでも使える。一方、ログファイルを直接開くにはroot権限が必要
  • /var/log/messages/var/log/audit/audit.logなど複数ファイルのログ情報を横断的に確認できる
  • rotateされず、かなり昔のログまで遡れる
  • コマンドラインオプションのフィルタが便利
  • (SELinuxの文脈では関係ないが、) systemd管理下のサービスの詳細ログが見える

今回のSELinuxのログ確認においても、ログファイルを直接開くよりも扱いやすいのでjournalctlを積極的に活用しています。

(参考) journalctlのオプション

journalctlは複数のログファイルを一元的に表示します。
そのままだとログの量が多くて扱いづらいので、オプションで出力をフィルタして使います。

各種オプションの使い方を下表に示します。

オプション 詳細
-q
  • 制御文字列の挿入を抑制する
  • -qを指定しないと、例えばOS起動のタイミングで-- Boot xxx --という文字列が挿入される
-e
  • 最終行の直近のログから表示する
  • -eを指定しないと、古いログから表示される
  • 暗黙的に-n 1000が指定され、ログ表示の上限が1000行になる
  • -n allなど明示的に指定することで、この挙動を上書きできる
-n [num]
  • 直近のnum行のログを表示する
  • numを省略すると10行になる
  • allで制限なしになる
-t <id>
  • 指定したSyslog Identifier (id) のみに限定して表示する
  • idは、ログ上でタイムスタンプ、ホスト名の隣に書いてある
-g <msg>
  • grep機能
  • メッセージの本文から文字列検索する
  • Perl互換の正規表現を使える
  • 全て小文字で書くと大文字/小文字に限らずヒットする
--no-pager デフォルトのless形式のpagerを抑制する
-f tail -fのようにログが発生するたびに画面を更新する

(※) SELinuxとは無関係ですが、-uで特定サービスの詳細ログを確認するオプションもよく使います。
例: journalctl -u NetworkManager

File Contextとの差分確認

本セクションは、#4. File Contextとの差分確認に関連して、より詳細な確認/修正手順を紹介します。

一番よく使うのは、以下の2コマンドです。

  • matchpathcon -V ファイルパス
  • restorecon -v ファイルパス

本セクションの手順を理解する前提として、File Contextとrelabelの仕組みを理解している必要があります。
参考: SELinux Type Enforcement - #File Contextとrelabel

File Contextの確認

File Contextの定義は、semanage fcontext -lで確認します。

sudo semanage fcontext -l
# SELinux fcontext  type       Context

# /                 directory  system_u:object_r:root_t:s0 
# /.*               all files  system_u:object_r:default_t:s0 
# (以下略)

特定ファイルのFile Context差分確認

ファイルのSecurity ContextとFile Contextの差分を確認するコマンドとして、matchpathcon -Vがあります。
このコマンドにより、指定したファイルのSecurity ContextがFile Contextと一致するかを確認できます。

 touch ~/a
matchpathcon -V ~/a
# /home/endy/a verified.

# Security Contextを変えるとエラーとして検知される
chcon -t tmp_t ~/a
matchpathcon -V ~/a
# /home/endy/a has context unconfined_u:object_r:tmp_t:s0, should be unconfined_u:object_r:user_home_t:s0

rm ~/a

基本matchpathcon -Vだけで事足りますが、他のコマンドも参考情報として紹介します。

ファイルのSecurity Contextを確認するには、ls -Zを使います。

touch ~/a
ls -Z ~/a
# unconfined_u:object_r:user_home_t:s0 /home/endy/a

特定ファイルのFile Contextを確認するには、オプション無しでmatchpathcon ファイルパスを実行します。
正規表現で書かれているsemanage fcontext -lでは探すのが難しいので、この用途ではmatchpathconの方が向いています。

matchpathcon /etc/hosts
# /etc/hosts   system_u:object_r:net_conf_t:s0

matchpathconには、ファイルそのものではなく「File Contextの検索ワード」を指定します。
matchpathconは、File Contextの正規表現で引数の文字列 (ファイルパス) を検索した結果、ヒットしたSecurity Contextを返します。
したがって、matchpathconには、必ずフルパスを指定するようにしてください。
matchpathcon -Vには相対パスを指定しても良いですが、matchpathconには絶対パスを指定する必要があります。

# フルパスを指定しないと想定外の結果になる
cd /etc
matchpathcon hosts
# hosts    <<none>>

(参考) selabel_lookup -k

selabel_lookup -kは、matchpathconと同じ意味を持ちます。
役割が被るので、selabel_lookup -kを使うことは基本ないと思います。

selabel_lookup -k /etc/hosts
# Default context: system_u:object_r:net_conf_t:s0

特定ファイルのrelabel

restorecon -vによって、特定ファイルをrelabelします。
relabelとは、Security ContextをFile Contextの値と揃える処理のことです。

# 事前準備
touch ~/a
chcon -t tmp_t ~/a

# restorecon
ls -Z ~/a
# unconfined_u:object_r:tmp_t:s0 /home/endy/a

restorecon -v ~/a
# Relabeled /home/endy/a from unconfined_u:object_r:tmp_t:s0 to unconfined_u:object_r:user_home_t:s0

ls -Z ~/a
# unconfined_u:object_r:user_home_t:s0 /home/endy/a

# relabelが発生しない場合は、何も出力されない
restorecon -v ~/a
# (出力なし)

-vオプションを指定しない場合、relabelが発生した場合も何も出力されません。

chcon -t tmp_t ~/a

restorecon ~/a
# (出力なし)

restoreconは、fixfilesと同様にデフォルトではTypeしかrelabelしません。
restorecon -FによりUser, Role, Rangeも含めてrelabelできますが、実用上-Fオプションを使うことはないと思います。

全ファイルのFile Context確認

全ファイルのFile Contextを一括確認するには、fixfiles checkを実行します。
restorecon -nvR /とほぼ同じ意味です。

sudo fixfiles check
# Would relabel /home/endy/a from unconfined_u:object_r:tmp_t:s0 to unconfined_u:object_r:user_home_t:s0

デフォルトではTypeの差分しか確認しません。
-FオプションによってUser, Role, Rangeも差分チェックの対象にできますが、実用上使うことはないと思います。

ディレクトリやファイルを指定することで、対象を絞り込むことができます。
ディレクトリを指定した場合、配下のファイルを再帰的に確認します。

sudo fixfiles check ~
# Would relabel /home/endy/a from unconfined_u:object_r:tmp_t:s0 to unconfined_u:object_r:user_home_t:s0

(参考) 複数ファイルのrelabel

全ファイルに対して一括でrelabelを行う手順もありますが、基本的にこの操作を行うことはありません。

relabelは一見すると思考停止で実行すると良さそうにも思えますが、稀に新規作成されてから一度もrelabelされていないファイルのTypeが、relabelによって変化してしまうこともあります。
ファイル新規作成時のLabelとFile Contextの定義は多くの場合整合性が取れていますが、100%とは限りません。
何らかの理由で実行する場合も、事前にfixfiles checkによって影響範囲を確認することをおすすめします。

主なユースケースは、SELinuxをdisabledからenforcing/permissiveに変更した時です。
disabledの場合はファイルがラベル付けされていないので、有効化するタイミングでrelabel処理が必要となります。

システム全体をrelabelする手順はいくつかありますが、代表的なものは以下です。
以下を実行すると、Linuxの再起動中に全ファイルをrelabelします。

sudo fixfiles onboot
sudo reboot

Linuxの起動中に、内部的に以下のコマンドを実行しています。

sudo fixfiles restore
sudo rm /.autorelabel

sudo fixfiles restoreを単体で使うことも可能です。
このコマンド自体はOS再起動を要求しません。
-vオプションをつけることで、restoreconと同様にrelabel処理の詳細を表示します。
sudo fixfiles restoreは、sudo restorecon -R /と同等です。

fixfilesについて、詳細は以下のリンクを参照してください。
参考: SELinux Type Enforcement - #fixfiles

(参考) File Contextの追加

レアケースではありますが、切り分けの結果File Contextの定義が足りないと判明することもあります。
そういった場合は、semanage fcontext -a ...コマンドでFile Contextの定義をLocal Policyとして追加します。

その後、restoreconなどによってrelabelすることで、新しいFile ContextをファイルのSecurity Contextに反映します。

Local Policyで定義したFile Contextを削除するには、sudo semanage fcontext -d ...を実行します。
Module PolicyとしてインストールされたFile Contextを変更/削除することはできないので、そこはご注意ください。

Local Policyの良いところは、ソースコードの記述・ビルド・インストールが不要なところです。
semanageコマンドを1行実行するだけで手軽に実装できます。
Local Policyに対応するのは、semanageのサブコマンドに含まれる以下の要素のみです。
詳細はman semanageを参照してください。

  • File Context
  • Boolean
  • Port
  • Login
  • User

例: 特定ファイルのみ別のSecurity Contextを割り当てたい場合

以下のFile Context定義が示すとおり、デフォルトでは/tmp/直下のファイルはrelabel対象外です。
<<none>>とはrelabel時に対象外とすることを表します7

sudo semanage fcontext -l | grep '^/tmp/\.\*'
# SELinux fcontext  type       Context
# /tmp/.*           all files  <<None>>

今回の例では、/tmp/auser_home_tにrelabelするようにFile Contextを追加定義します。
(※) 良いユースケースを思いつかず、実用性のない例となってしまいました。
(※) 構文について補足ですが、relabel対象外にするためのルールは右記のように指定します sudo semanage fcontext -a -t '<<none>>' '/tmp/a'

sudo semanage fcontext -a -t user_home_t '/tmp/a'

作成したFile Contextを確認します。
Local Policyはsemanage fcontext -lの一番下に表示されます。
今回は、-Cオプションを追加で指定することでLocal Policyのみ表示します。

sudo semanage fcontext -lC
# SELinux fcontext  type       Context

# /tmp/a            all files  system_u:object_r:user_home_t:s0 

実際にrelabelを試してみます。

touch /tmp/a
ls -Z /tmp/a
# unconfined_u:object_r:user_tmp_t:s0 /tmp/a

restorecon /tmp/a
ls -Z /tmp/a
# unconfined_u:object_r:user_home_t:s0 /tmp/a

検証が終わったので元の構成に戻します。
semanage fcontext -dでLocal Policyを削除できます。

rm /tmp/a
sudo semanage fcontext -d '/tmp/a'

sudo semanage fcontext -lC
# (出力なし)

今回の例ではTypeのみ指定しましたが、追加のオプションによってUser, Role, Rangeなども指定してFile Contextを定義できます。
基本的には、Typeのみの指定で十分です。

例: File Contextとの差分によるSELinuxのエラー

File Contextとの差分によってSELinuxのエラーが発生している例を挙げます。
このようなエラーは、mvコマンドでファイル移動した後にrestoreconの実行を忘れることでしばしば発生します。

事前準備として、Apache (WEBサーバ) を事前にインストール・起動しておきます。

sudo dnf install httpd
sudo systemctl start httpd

a.htmlファイルを作成し、curlでアクセス確認します。
以下のやり方であれば問題なく通ります。

sudo bash -c 'echo "aaa" > /var/www/html/a.html'

cat /var/www/html/a.html
# aaa

curl localhost/a.html
# aaa

しかし、以下のように一旦ホームディレクトリでb.htmlを作成してから/var/www/html/にmvで配置した場合はアクセス失敗します。

sudo bash -c 'echo "bbb" > /home/endy/b.html'
sudo mv /home/endy/b.html /var/www/html/

# a.htmlとb.htmlでPermissionに差はない
ls -l /var/www/html
# -rw-r--r--. 1 root root 4 Jan 15 17:35 a.html
# -rw-r--r--. 1 root root 4 Jan 15 17:39 b.html

curl localhost/b.html
# <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
# <html><head>
# <title>403 Forbidden</title>
# </head><body>
# <h1>Forbidden</h1>
# <p>You don't have permission to access this resource.</p>
# </body></html>

403 Forbiddenは、アクセス権限がないことを表します。
ログは割愛しますが、sudo chmod o-r /var/www/a.htmlを実行し、apacheユーザーからa.htmlにアクセスできない状態にしてからcurl /var/www/a.htmlを実行したときも同じエラーになります。

Apacheのログを確認すると、Permission Deniedが発生していました。
先ほどのls -lの実行結果から通常のFile Permissionsはa.htmlとb.htmlに差がないので、SELinuxが原因である疑いが強まります。

sudo tail -1 /var/log/httpd/error_log
# [Sat Jan 15 17:40:44.293322 2022] [core:error] [pid 2795:tid 2895] (13)Permission denied: [client 127.0.0.1:37052] AH00132: file permissions deny server access: /var/www/html/b.html

続いて、SELinuxの監査ログを確認します。
httpd (httpd_t) からb.html (user_home_t) へのreadアクセスが拒否されていることがわかります。
このことから、今回のPermission Deniedエラーは、SELinuxが原因であったことがわかります。

journalctl -qen all -t audit -g denied --no-pager

# AVC avc:  denied  { read } for  pid=2795 comm="httpd" name="b.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

setroubleshootのログ確認は、ここでは割愛します。

また、Permissiveでの動作確認も省略します。

targetがファイルなので、File Contextとの差分を確認します。
matchpathcon -Vにより、File Contextとの差分が存在することがわかりました。

matchpathcon -V /var/www/html/b.html 
# /var/www/html/b.html has context unconfined_u:object_r:user_home_t:s0, should be system_u:object_r:httpd_sys_content_t:s0

mvでファイル移動した場合、Security Contextが維持されます。
~/b.htmlにファイル生成した段階ではuser_home_t Typeを持っていて、それをmvで移動したためにFile Contextとの差分が生まれています。

relabelによって今回の事態が直ると想定されますが、念のためallowルールを確認します。

上述の監査ログをヒントに、拒否されるアクセスパターンからルール検索してみます。
検索にヒットしているように見えますが、httpd_read_user_contentというBooleanがFalseなのでこのルールは無効化されています。
このBooleanを有効化することでもエラーは解消しますが、アクセス許可の範囲が広すぎるのでセキュリティ観点では好ましくありません。

sesearch -A -s httpd_t -t user_home_t -c file -p read
# allow httpd_t user_home_type:file { getattr ioctl lock open read }; [ httpd_read_user_content ]:True

getsebool httpd_read_user_content
# httpd_read_user_content --> off

続いて、relabelした後の条件でルール検索します (-t httpd_sys_content_t)
今度はヒットしたので、relabelによって今回のエラーが解消できるとわかります。

sesearch -A -s httpd_t -t httpd_sys_content_t -c file -p read
# (一部抜粋)
# allow httpd_t httpd_content_type:file { getattr ioctl lock map open read };

relabelして再度アクセスしてみます。
今度はアクセスに成功しました。

sudo restorecon -v /var/www/html/b.html
# Relabeled /var/www/html/b.html from unconfined_u:object_r:user_home_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0

curl localhost/b.html
# bbb

File Contextとの差分によるSELinuxのエラーは、大抵の場合は操作ミスによって起こります。
今回の場合、以下のいずれかで対策可能です。

  • mvした後はrestoreconでrelabelする
  • mvの代わりにcpを使う

または、次のセクションに示すとおりrestorecondを使うという手もあります。

restorecondによる利便性向上

restorecondデーモンを起動していると、対象のファイルが新規作成/移動されたタイミングで自動的にrestoreconによってrelabelしてくれます。
ユーザーが頻繁にファイルを手動アップロードをするような環境でSELinuxを動作させる場合は便利かもしれません。

restorecondを起動していれば、mvコマンドでファイルを移動した後にrestoreconの実行を忘れても、SELinuxがエラーにならないという効果を見込めます。

詳細は#(参考) restorecondを参照してください。

(参考) restorecond

restorecondとは

restorecondとは、ファイルの更新を検知して自動的にrestoreconを実行するデーモンプロセスです8
/etc/selinux/restorecond.confに書いたファイルのみが対象です。

restorecondは、思いつく範囲では以下のような状況で役に立ちます。

  • SELinuxを知っている管理者が構築したサーバをSELinuxに詳しくないユーザーが操作する場合
    • 例: WEBサーバのファイルアップロードを手動で行う場合、/var/www/配下をrestorecondの監視対象としておくことで「mvの後のrestorecon忘れ」によるエラーを防ぐことができる
  • SELinuxを意識していないソフトウェアをインストールしている場合
    • 例: ダウンロードしたZipファイルを展開して、スクリプト実行によりmvコマンドでファイルを配布する形式のインストーラが世の中にある。そういったソフトを正しく動作させるために利用する

いずれもマイナーなケースかもしれませんが、運用にハマると便利だと思います。

restorecondは、デフォルトではインストールされていません。
policycoreutils-restorecond RPMパッケージをインストールすることで導入できます。

ファイル更新の検知は、inotify9という仕組みを利用しています。

restorecond.conf

restorecondによる監視対象は、/etc/selinux/restorecond.confで指定します10
ファイルの中は、デフォルトで以下のように指定されていました。

/etc/services
/etc/resolv.conf
/etc/samba/secrets.tdb
/etc/updatedb.conf
/run/utmp
/var/log/wtmp
/root/*
/root/.ssh/*

ファイルの記載方法について公式情報はほとんどないのですが、実機で試したところ以下がわかりました。

  • ディレクトリを指定しても、再帰的に配下を監視することはない
  • *によるワイルドカード指定が可能 (例: /var/www/html/*)
  • *は隠しファイルも含む (/var/www/html/*を設定ファイルに書くと、/var/www/html/.cも監視対象になる)
  • #で始まる行はコメント扱いになる
  • **による/を含むワイルドカード指定は不可
  • /home/endy/a aのようにスペースを含むファイル名も認識される
  • "'も純粋に文字列として認識され、特殊な意味を持たない。"/home/endy"のような指定は不可
  • \エスケープ文字として扱われる。/home/endy/a a/home/endy/a\ aは同じ意味
  • restorecond.confに関しては、~は使えない
  • 同一フォルダ内で*と他の文字列の組み合わせは不可 (例: /var/www/htm*のような指定は、ワイルドカードとして動作しない)
  • *を2箇所以上使用すると動作しない (例: /var/www/html/*/*/var/www/htm*/*)

なお、htm*/*/*/のように、対応できない形式を指定した上でrestorecondを起動すると、以下のようなエラーが出ます。

sudo systemctl restart restorecond

journalctl -eu restorecond
# Jan 15 23:37:59 fedora35.test restorecond[5387]: Unable to watch (/var/www/htm*/*) No such file or directory
# Jan 15 23:37:59 fedora35.test restorecond[5387]: Unable to watch (/var/www/*/*) No such file or directory

実際には上記のようなエラーが出ていても、restorecondを再起動した直後だけはrelabelしてくれました。
その後は、ログに書いてある通りWatchできていないため、mvなどで該当パスにファイルを移動してもrelabelしてくれなくなります。

このように、restorecond.confの書き方にはかなり癖があります。
restorecondを使う際は、設定変更のたびにこまめにjournalctl -eu restorecondからエラーが出ていないか確認するようにしましょう。

(参考) restorecond_user.conf

restorecondは一般ユーザー権限でも起動できます。
しかし、手元の環境ではsystemctl --user start restorecondで起動できるような一般ユーザー用のUnitファイルは自動生成していませんでした。
ユーザー権限で常時起動させたい場合は、自前でUnitファイルを作成する必要があります。

一般ユーザー権限で起動した場合、/etc/selinux/restorecond_user.confが読み込まれます。
このファイルでは~がユーザーのホームディレクトリを意味する記号として利用できます。

restorecondをユーザー権限で起動するユースケースは、あまり思いつきませんでした。
強いて言うなら、デスクトップ用途でLinuxを利用している場合に便利かもしれません。

一般ユーザー権限でrestorecondを起動するには、以下のコマンドを実行します。

restorecond -u

restorecondによってrelabelされるタイミング

restorecondによってrelabelされるタイミングは、実機検証で観測できた範囲では以下の通りです。

  • restorecondが起動/再起動したとき
  • mvによってファイル移動したとき
  • cp --archiveによってSecurity Contextも含めて保持しつつコピーしたとき
  • touchなどによってファイルを新規作成したとき

一方で、既に存在するファイルに書き込んだり、touchによってmodified timeを更新しただけではrelabelされませんでした。

【重要】File Contextを更新したら、restorecondを再起動する

restorecondは、起動したタイミングでFile Contextをメモリ上に読み込んでいるようです。
そして、デーモンが再起動するまで再び読みに行くことはありません。
恐らく処理効率化のためだと思います。

ここで注意なのですが、semanage fcontextコマンドなどによってFile Contextを更新した後、restorecondデーモンを再起動する必要があります。
再起動するまでの間、restorecondは古いFile Contextの定義に基づいて動作してしまいますので、くれぐれもご注意ください。

このことはドキュメントには特に書いていないのですが、私が実機操作中に本事象にあたったことで気づきました。

【重要】restorecond起動時に監視対象のファイルが存在すること

restorecondが起動したタイミングで設定ファイルに記述したファイルが存在しなかった場合、後追いでファイルを作成しても監視が開始されません。
この場合、restorecondのログとして以下のエラーメッセージが出ます。

systemctl status restorecond
# (一部抜粋)
# Jan 22 16:07:13 fedora35.test restorecond[770]: Unable to watch (/var/www/html/*) No such file or directory

このエラーが出た場合、ファイルを後追いで作成した上でrestorecondを再起動する必要があります。

例: restorecondによってmv直後に自動relabelする

#例: File Contextとの差分によるSELinuxのエラーと同様の操作を行った時、restorecondが有効の場合にどう変わるかを説明します。

事前準備として、restorecondをインストールします。

sudo dnf install policycoreutils-restorecond

続いて、/etc/selinux/restorecond.confを編集し、管理対象のファイルを追加します。

/var/www/html/*

なお、/var/www/html/dir/index.htmlのようなファイルを監視するには、restorecond.conf上の記述でも/var/www/html/dir/*のように階層を表現する必要があるのでご注意ください。

restorecondを起動、または再起動します。

sudo systemctl start restorecond
# またはsudo systemctl restart restorecond

前回の例で使ったファイルが残っている場合は、一旦削除しておきます。

sudo rm /var/www/html/b.html

続いてサンプルファイルを設置します。
一旦ホームディレクトリ配下にファイルを作成してからmvで移動します。
mvはSecurity Contextを保持しますが、今回はrestorecondによってファイル配置直後にrelabelされています。

sudo bash -c 'echo "bbb" > /home/endy/b.html'
ls -Z /home/endy/b.html
# unconfined_u:object_r:user_home_t:s0 /home/endy/b.html

sudo mv /home/endy/b.html /var/www/html/
ls -Z /var/www/html/b.html
# unconfined_u:object_r:httpd_sys_content_t:s0 /var/www/html/b.html

b.htmlにHTTPアクセスすると、今回は成功します。

curl localhost/b.html
# bbb

このようにrestorecondの働きにより、Apacheの基本オペレーションをSELinuxを意識することなく実施できました。

運用担当者が手動で配置/削除するファイルをrestorecondに監視させると、SELinuxに詳しい人とそうでない人が混在した現場においてもシームレスにLinuxサーバを運用できるようになります。

ポート番号と紐づくSecurity Contextの確認

本セクションは、#5. ポート番号と紐づくSecurity Contextの確認に続けてより具体的なコマンドを説明します。

アプリケーションの設定ファイルを書き換えてデフォルト値以外の待受ポート番号を利用する場合には、本セクションの手順の実施が必要かご検討ください。

ポート番号のSecurity Contextの確認コマンド

seinfo --portconによってポート番号のSecurity Contextを確認できます。

seinfo --portcon
# Portcon: 652
#    portcon sctp 1-511 system_u:object_r:reserved_port_t:s0
# (以下略)

seinfo --portconの後にポート番号を指定することで、表示内容を絞り込みできます。

seinfo --portcon 80

# Portcon: 4
#    portcon sctp 1-511 system_u:object_r:reserved_port_t:s0
#    portcon tcp 1-511 system_u:object_r:reserved_port_t:s0
#    portcon tcp 80 system_u:object_r:http_port_t:s0
#    portcon udp 1-511 system_u:object_r:reserved_port_t:s0

semanage port -lでも確認できますが、root権限が必要です。
また、ポート番号の検索機能もありません。
基本的には、seinfo --portconの方がおすすめです。

sudo semanage port -l
# SELinux Port Type              Proto    Port Number
# afs3_callback_port_t           tcp      7001
# (以下略)

ポート番号とSecurity Contextの紐付け追加コマンド

semanage portコマンドにより、semanage fcontextと同様にLocal Policyを定義してポート番号とSecurity Contextの紐付けを追加定義できます。

以下は、TCP10080をhttp_port_tに紐付けるコマンドです。
-a, --addは追加、-t, --typeはType指定、-p, --protoTCP/UDPプロトコル指定、最後のコマンドライン引数はポート番号の指定です。
-rオプションでRangeも指定できますが、Type Enforcmentのみ利用する環境ではRangeの指定は不要です。

sudo semanage port -a -t http_port_t -p tcp 10080

作成したLocal Policyは、seinfoまたはsemanageで確認できます。
semanage-Cオプションをつけることで、Local Policyのみに限定して表示できるので便利です。

sudo semanage port -lC
# SELinux Port Type  Proto    Port Number
# http_port_t        tcp      10080

他の表示方法の場合、既存ルールと並んで表示されるので少々紛らわしいかもしれませんね。

seinfo --portcon 10080
# Portcon: 6
#    portcon sctp 1024-65535 system_u:object_r:unreserved_port_t:s0
#    portcon tcp 10080 system_u:object_r:http_port_t:s0
#    portcon tcp 10080-10083 system_u:object_r:amanda_port_t:s0
#    portcon tcp 1024-32767 system_u:object_r:unreserved_port_t:s0
#    portcon udp 10080-10082 system_u:object_r:amanda_port_t:s0
#    portcon udp 1024-32767 system_u:object_r:unreserved_port_t:s0

sudo semanage port -l | grep 10080
# SELinux Port   Type  Proto    Port Number
# amanda_port_t  tcp      10080-10083
# amanda_port_t  udp      10080-10082
# http_port_t    tcp      10080, 80, 81, 443, 488, 8008, 8009, 8443, 9000

次のセクションで、具体例を示します。

例: httpdの待受ポート番号を10080に変更する

httpdの待受ポート番号をデフォルトのTCP80からTCP10080に変更する例で、ポート番号のSecurity Contextを変更する必要があるケースの例を示します。

事前準備として、httpdをインストールしておきます。

sudo dnf install httpd

そして/etc/httpd/conf/httpd.confを編集します。
もともとListen 80と書かれていた部分をListen 10080に書き換えます。

#Listen 80
Listen 10080

そしてhttpd.serviceを起動 (または再起動) します。

sudo systemctl start httpd
# または、sudo systemctl restart httpd

すると、httpdの起動に失敗します。
httpdのログを確認すると、Permission deniedによってネットワークソケットのbindに失敗していることがわかります。
root権限でhttpdを起動していることから、SELinuxが怪しいということがわかります (※)
(※) DACの世界では、root権限で起動したプロセスによる操作がPermission Deniedで拒否されることはありません。Permission Deniedになっているということは、MAC、つまりSELinuxによって拒否されているということです

# またはsystemctl status httpd
journalctl -eu httpd
# Jan 16 16:16:24 fedora35.test httpd[1274]: (13)Permission denied: AH00072: make_sock: could not bind to address [::]:10080
# Jan 16 16:16:24 fedora35.test httpd[1274]: (13)Permission denied: AH00072: make_sock: could not bind to address 0.0.0.0:10080

SELinuxのログを確認します。
httpd_t---[name_bind]--->amanda_port_tというアクセスが拒否されていることがわかります。
ポート番号のTypeがhttp_port_tではないことから、ポート番号とSecurity Contextの紐付けに問題があると推測できます。

# 拒否内容: httpd_t---[name_bind]--->amanda_port_t
journalctl -qen all -t audit -g denied
# Jan 16 16:16:24 fedora35.test audit[1274]: AVC avc:  denied  { name_bind } for  pid=1274 comm="httpd" src=10080 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:amanda_port_t:s0 tclass=tcp_socket permissive=0

調べてみると、TCP10080は確かにamanda_port_tでした。

seinfo --portcon 10080
# (一部抜粋)
  #  portcon tcp 10080-10083 system_u:object_r:amanda_port_t:s0

http_port_tにはTCP10080が含まれません。

sudo semanage port -l | grep ^http_port_t
# http_port_t                    tcp      80, 81, 443, 488, 8008, 8009, 8443, 9000

AVCのログを参考に、targetをhttp_port_tに変更した場合のallowルールが存在するかを確認します。
当然ながら、httpd_tはhttp_port_tをbindする権限を持っています。

sesearch -A -s httpd_t -t http_port_t -c tcp_socket -p name_bind
# (一部抜粋)
# allow httpd_t http_port_t:tcp_socket name_bind;

これまでの調査で、TCP10080ポートをhttp_port_tに紐付ければ問題が解決することがわかりました。
以下のコマンドでポート番号とSecurity Contextの紐付け設定を変更します。

sudo semanage port -a -t http_port_t -p tcp 10080

sudo semanage port -lC
# SELinux Port Type  Proto    Port Number
# http_port_t        tcp      10080

今回は既存ファイルではなく、まだbindされていないポート番号のSecurity Context定義変更なので、relabelのような反映操作は不要です。
次回ポート番号が生成するときに、上記定義を参照してTCP10080がhttp_port_tと紐づきます。

では、改めてhttpdを起動します。
今度はちゃんと成功します。

sudo systemctl start httpd

curl localhost:10080
# <!doctype html>
# (以下略)

Security Contextの設定を元に戻します。

sudo semanage port -d -p tcp 10080

最後に補足ですが、今回追加したTCP10080amanda_port_tのport rangeの一部として含まれていました。
今回の場合、既存のTCP10080-10083という定義とTCP10080は「範囲が完全に一致するわけではない」ためうまく定義できたようです。
既存のポート番号の範囲と完全に一致するような定義の仕方をしようとすると、以下のようにエラーになります。

sudo semanage port -a -t http_port_t -p tcp 10080-10083
# ValueError: Port tcp/10080-10083 already defined

Boolean有無の確認

本セクションは、#6. Boolean有無の確認に続けてより具体的なコマンドを説明します。

一番よく使うコマンドは、以下の3つです。

  • sesearch -A --type_trans ...
  • semanage boolean -l
  • setsebool -P ...

1つ目は既存のアクセス許可ルール等を検索するコマンドですが、Booleanの検索にも使えます。
拒否されたときの条件を指定して検索すると、「BooleanがTrueなら有効になるルール」がヒットすることがあります。
その場合、Booleanの有効化によってエラーを解消できると気づくことができます。
具体的な使用例は、#例: BooleanによるSELinuxエラー対応で紹介します。

本セクションの手順を理解する前提として、Booleanの概念について理解している必要があります。
参考: SELinux Type Enforcement - #Boolean

Booleanの一覧表示

Booleanの一覧表示をするコマンドは2種類あります。
getsebool -aは一般ユーザー権限で実行可能なのに対し、semanage boolean -lは特権が必要です。
semanage boolean -lの方が詳細な情報を表示できるので、権限があるのであればこちらのほうがおすすめです。

getsebool -a
# abrt_anon_write --> off
# (以下略)

sudo semanage boolean -l
# SELinux boolean  State  Default Description
# abrt_anon_write  (off  ,  off)  Allow ABRT to modify public files used for public file transfer services.

semanage boolean -lによって以下の情報がわかります。

  • Boolean名
  • 現在値
  • OS起動直後にセットされる値 (※)
  • 簡単な説明

(※) コマンドライン上はDefaultと表記されていますが、これは初期インストールされたLinuxのデフォルト値ではなく、PolicyかLocal Policyに書き込まれたBoolean値、つまりLinux再起動時にセットされるBoolean値を表しています。後述のsemanage boolean -mによってBoolean値を変更するLocal Policyを追加すると、ここに表示されるDefault値も変化します。インストール直後の初期値を知りたい場合は、#manの横断検索を参考にしつつ、manコマンドからご確認ください

以下のようにgrepと組み合わせて使うことも多いです。
httpdではなくhttpでフィルタすることで、なるべく多くの情報にヒットするようにしています。

sudo semanage boolean -l | grep -i http
# (一部抜粋)
# awstats_purge_apache_log_files (off  ,  off)  Determine whether awstats can purge httpd log files.
# httpd_can_connect_zabbix       (off  ,  off)  Allow http daemon to connect to zabbix
# mysql_connect_http             (off  ,  off)  Allow mysqld to connect to http port

検索結果より、ApacheをZabbixやMySQLと連携するときに便利そうなBooleanが存在することがわかりました。
また検索ワードをhttpとしたことで、mysql_connect_httpも含めてヒットしました。

Booleanの意味を調べる

Booleanのざっくりとした意味であれば、semanage boolean -lで知ることができます。
しかし、1行の説明でBooleanの意味を正確に理解することは難しいと思います。

sesearch -b Boolean 名を実行することで、Booleanと紐づくアクセス許可ルールを表示できます。
これによりBooleanを変更することによって、どのルールが変化するのか最も直接的に理解できます。

sesearch -A --type_trans -b httpd_can_connect_zabbix
# allow httpd_t zabbix_port_t:tcp_socket name_connect; [ httpd_can_connect_zabbix ]:True

上記出力より、httpd_can_connect_zabbixというBooleanはApache (httpd_t) がZabbixのポート番号 (zabbix_port_t) に対して接続 (name_connect) するallow Statementを有効化していることがわかりました。

Booleanの変更

Booleanを変更するコマンドは2種類あります。
setseboolsemanage boolean -mです。
(※) Booleanの場合はもともと定義された値を書き換えることになるので、semanageのオプションは-a,--addではなく-m,--modifyになります

setseboolsemanage boolean -mの間にあまり違いはないのですが、細かい機能に若干の違いがあります。
やりたいことに合わせて両者を使い分ける必要があります。
ただ、多くの場合はどちらを使っても問題ありません。

まず、両者に共通する機能から紹介します。
Local PolicyにBoolean値を保存しつつ、現在の挙動も変えるには以下のコマンドを実行します。
以下に2つのコマンドを記載していますが、どちらも全く同じ意味を持ちます。

# -1でBooleanに1をセット。-0でBooleanに0をセット
sudo semanage boolean -m -1 httpd_can_connect_zabbix

sudo setsebool -P httpd_can_connect_zabbix 1

設定の変更結果は以下のように確認できます。
semanage boolean -lCのDefault列も書き換わっているので注意してください。
semanageで表示されるDefaultは、SELinux起動時に読み込まれる値のことであり、Security Policyのソースコード上に定義されたデフォルト値のことではありません。

# -CでLocal Policyのみ表示
sudo semanage boolean -lC
# SELinux boolean           State  Default Description
# httpd_can_connect_zabbix  (on   ,   on)  Allow http daemon to connect to zabbix

# getsebool -a | grep httpd_can_connect_zabbixとほぼ同じ
getsebool httpd_can_connect_zabbix
# httpd_can_connect_zabbix --> on

続いて、semanagesetseboolの細かな違いを一部だけ紹介します。

先ほどはsetsebool -PでBoolean値を設定しましたが、-Pは、Persistentを意味します。
-Pを指定するとLocal Policyに書き込むことで設定を永続化します。
-Pを省略すると現在のBoolean値のみ書き換え、Local Policyは書き換えません。
-Pを指定しないsetseboolは、一時的な試験をしたいときに便利かもしれません。
...が、setseboolを-Pなしで使うことは実用上ほぼないと思います。

Booleanの設定を戻したいときは、以下のように再度変更することで元に戻すのが基本です。

sudo semanage boolean -m -0 httpd_can_connect_zabbix
# または、sudo setsebool -P httpd_can_connect_zabbix 0

設定をもとに戻しても、Local Policyには0という値で記録されてしまいます。

sudo semanage boolean -lC
# SELinux boolean           State  Default Description
# httpd_can_connect_zabbix  (off  ,  off)  Allow http daemon to connect to zabbix

semanage booleanには、-dで特定のLocal Policyを削除する機能がありません。
あるのは、-DによってBooleanに関する「全ての」Local Policyを削除する機能のみです。
当然、影響範囲が大きいので気軽には実行できません。

# 「全ての」Booleanに関するLocal Policyを削除する
sudo semanage boolean -D

sudo semanage boolean -lC
# (出力なし)

semanage boolean -Dを実行しても、現在のBooleanの値が書き換わるわけではないのでご注意ください。
事前にBooleanをデフォルト値に手動で戻した上で実行しないと、思わぬ挙動になる懸念があります。
最も確実なのは、semanage boolean -Dを実行した後にOSを再起動することです。
それによってBooleanがPolicyから再読込されて、確実に意図した値に収束します。

例: BooleanによるSELinuxエラー対応

ここでは、httpd_read_user_contentというBooleanを例に紹介します。
(※) セキュリティの観点ではこの機能を使うこと自体望ましくないとは思うのですが、主目的はSELinuxの検証なので目をつぶってください。

事前準備として、Apacheをインストールしておきます。

sudo dnf install httpd

そして/etc/httpd/conf.d/userdir.confを以下のように書き換えます。
元々はuserDir disabledが有効でUserDir public_htmlコメントアウトされていますが、これら2行のコメントを反転させます。

<IfModule mod_userdir.c>
    #UserDir disabled
    UserDir public_html
</IfModule>

この設定により、以下のようにホームディレクトリのHTMLファイルをWEB公開できるようになります。

  • $HOME/public_html/配下に配置したhtmlファイルがhttpdに公開される
  • 上記に公開したファイルは、http://xxx.com/~user/xxx.htmlのようにHTTPアクセスすることで参照できる

具体例は後ほど示します。

ちなみに設定行がIfModuleに囲われていますが、以下の通りデフォルトでモジュールをロードしているので、上記設定はアクティブになります。

httpd -M | grep userdir
#  userdir_module (shared)

ここで、httpdを起動します。
特にエラーは起こりません。

sudo systemctl start httpd

続いて、ファイルを配置してコンテンツを公開します。
また、ACLによってapacheユーザーがホームディレクトリ配下を見えるようにrx権限を付与しておきます (※)
(※)セキュリティホールなので、本番環境ではやらないでください

mkdir ~/public_html
chgrp apache ~/public_html
echo aaa > public_html/a.html

setfacl -m u:apache:rx ~

公開したコンテンツにアクセスを試みます。
すると、403 Forbidden応答が返ります。
403応答は、アクセス権限がないことを表します。

curl localhost/~endy/a.html
# <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
# <html><head>
# <title>403 Forbidden</title>
# </head><body>
# <h1>Forbidden</h1>
# <p>You don't have permission to access this resource.</p>
# </body></html>

エラーの詳細をerror_logから確認します。
Permission Deniedになっていることがわかります。

sudo tail /var/log/httpd/error_log
# (一部抜粋)
# [Sat Jan 22 22:49:19.460845 2022] [core:error] [pid 6034:tid 6077] (13)Permission denied: [client 127.0.0.1:50336] AH00035: access to /~endy/a.html denied (filesystem path '/home/endy/public_html') because search permissions are missing on a component of the path

SELinuxの監査ログを確認します。
ログが出ていることから、原因がSELinuxにあることがわかります。

journalctl -qen all -t audit -g denied --no-pager
# Jan 22 23:26:51 fedora35.test audit[6034]: AVC avc:  denied  { getattr } for  pid=6034 comm="httpd" path="/home/endy/public_html/a.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file permissive=0

必要なアクセス権限が上記のgetattrのみとは限らないので、Permissiveにしてから再度アクセスを試みます。
そして監査ログを再度確認します。
監査ログから、getattr以外にもread, open, mapが必要であることがわかりました。

sudo setenforce 0

curl localhost/~endy/a.html
# aaa

sudo setenforce 1

journalctl -qen all -t audit -g denied --no-pager
# Jan 22 23:28:20 fedora35.test audit[6034]: AVC avc:  denied  { getattr } for  pid=6034 comm="httpd" path="/home/endy/public_html/a.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file permissive=1
# Jan 22 23:28:20 fedora35.test audit[6034]: AVC avc:  denied  { read } for  pid=6034 comm="httpd" name="a.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file permissive=1
# Jan 22 23:28:20 fedora35.test audit[6034]: AVC avc:  denied  { open } for  pid=6034 comm="httpd" path="/home/endy/public_html/a.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file permissive=1
# Jan 22 23:28:20 fedora35.test audit[6034]: AVC avc:  denied  { map } for  pid=6034 comm="httpd" path="/home/endy/public_html/a.html" dev="dm-0" ino=131994 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file permissive=1

setroubleshootのログを見ると、今回はBooleanの有効化を提案しています。
確度は32.5%と低めです。
今回は練習なので、この提案がなかったものとして先に進みます。

journalctl -qen all -t setroubleshoot
# (一部抜粋)
# ****  Plugin catchall_boolean (32.5 confidence) suggests   ****
# setsebool -P httpd_enable_homedirs 1

File Contextを確認してみます。
今回はmvで移動していないので、基本的に差分はないはずです。
実際に、Object Transitionが正しく働いたことで~/public_html作成時に適切なTypeが割り当てられていることがわかりました。

matchpathcon -V ~/public_html/
# /home/endy/public_html verified.

matchpathcon -V ~/public_html/a.html 
# /home/endy/public_html/a.html verified.

sesearch --type_trans -s unconfined_t -D httpd_user_content_t
# type_transition unconfined_t user_home_dir_t:dir httpd_user_content_t public_html;

次にBooleanの存在を確認します。
sesearchで拒否されたアクセスパターンを条件に指定してルール検索します。
複数のBooleanが出てきますが、httpd_enable_home_dirsが名前的に適切そうで、なおかつ全てのPermissionをカバーできていることがわかります。

sesearch -A --type_trans -s httpd_t -t httpd_user_content_t -c file -p getattr,read,open,map
# allow httpd_t httpd_user_content_type:file map; [ httpd_enable_homedirs ]:True
# allow httpd_t httpd_user_content_type:file { getattr ioctl lock open read }; [ httpd_enable_homedirs ]:True

# allow domain file_type:file map; [ domain_can_mmap_files ]:True
# allow httpd_t httpdcontent:file { append create getattr ioctl link lock open read rename setattr unlink watch watch_reads write }; [ ( httpd_builtin_scripting && httpd_unified && httpd_enable_cgi ) ]:True
# allow httpd_t httpdcontent:file { execute execute_no_trans getattr ioctl map open read }; [ ( httpd_builtin_scripting && httpd_unified && httpd_enable_cgi ) ]:True

Booleanの意味を2通りの方法で確認します。
まずは、semanageでざっくり確認します。
httpdがホームディレクトリを読み込めるようにする」とあるので、今回の意図に合ってそうです。

sudo semanage boolean -l | grep httpd_enable_homedirs
# httpd_enable_homedirs          (off  ,  off)  Allow httpd to read home directories

続いて、Booleanの正確な効果をsesearchで確認します。
-bでBoolean名を指定することで、特定のBooleanに関連するルールのみ表示するようフィルタしています。
合計で56行ものルールがヒットしました。
一部のみ抜粋していますが、他のBooleanと組み合わせることでNFSなどにも対応できるようにルールが書かれていることがわかります。

sesearch -A --type_trans -b httpd_enable_homedirs
# (一部抜粋)
# allow httpd_t httpd_user_content_type:file map; [ httpd_enable_homedirs ]:True
# allow httpd_t httpd_user_content_type:file { getattr ioctl lock open read }; [ httpd_enable_homedirs ]:True
# allow httpd_t nfs_t:dir { ioctl lock read }; [ use_nfs_home_dirs && httpd_enable_homedirs ]:True

以下のコマンドでBooleanを有効化します。
すると、今度はアクセスに成功することがわかります。

sudo setsebool -P httpd_enable_homedirs 1

curl localhost/~endy/a.html
# aaa

以下のコマンドでBooleanを削除します。

0をセットするだけでは、httpd_enable_homedirs=0という情報がLocal Policyに残ってしまうため、全Boolean設定をクリアしています。
非常に影響が大きいコマンドなので、意味を理解した上で実行してください。
このコマンドによって「全ての」Boolean「設定」値がデフォルトに戻ります。
現在のBoolean値は変わりません。

なお、以下の手順の場合は1コマンド目のsetseboolの-Pオプションは無くても結果は変わりません。

sudo setsebool -P httpd_enable_homedirs 0
#または、sudo semanage boolean -m -0 httpd_enable_homedirs

sudo semanage boolean -D

ここで要注意なのですが、sudo semanage boolean -Dを実行しただけでは現在のBoolean値は書き換わりません。
必ず、setseboolによって現在の値もセットで書き換えるようにしてください。

また、途中で設定したACLももとに戻しておきます。

setfacl -b ~

Apacheアンインストールは最後にまとめて行います。
#(参考) Apacheのアンインストール

(参考) Custom Policyのビルド

本セクションは、#7. Custom Policyのビルドに続けてより具体的なコマンドを説明します。

Custom Policyを書く状況は滅多に発生しません。
本セクションに書いてある情報は、「必要なときに改めて詳細を思い出せれば良い」という気持ちで楽に読んでいただけますと幸いです。

Custom Policyのビルドの手順やソースコードの読み方について、詳細は別の記事に詳しくまとめています。
参考: SELinux Module Policyのソースコード読解、ビルド

本セクションでは、具体的なコマンドにフォーカスしてお伝えします。

どんなケースでビルドが必要となるのか?

Custom Policyを書く状況は、主に以下の2つがあると思います。

  1. 既に制御されているプロセス用にallowルールを追加する
  2. 全く制御されていないアプリケーションを新たに制御する

まずは1のケースです。
多くの場合、あるアクセスパターンが拒否されたことをきっかけに検討を始めると思います。
そして既存のType定義やallow Statementなどを確認し、必要なallow Statementが足りていないことを確認します。
そして、relabelやBoolean値の変更によってもこの問題を解決できなかったものとします。

この場合、既存のルールにallow Statementを追加定義することになります。
足りていないallow Statementは、自分で一から書くよりもaudit2allowというプログラムから自動生成するのが簡単です。
audit2allowは、AVC (Access Vector Cache) から発行されたアクセス拒否のログから自動的にallow Statementを発行するプログラムです。
詳細な手順は、#例1: 既に制御されているプロセス用にallowルールを追加するにて説明します。

続いて2のケースです。
自分でソースコードを書き、アプリケーションを開発したとしましょう。
このアプリケーション用のアクセス制御ルールは、デフォルトでは当然存在しません。

今回新たに開発したアプリケーションは、一部のアクセスについては既存のルールで許可されるかもしれません。
例えば、アプリケーションをbashからのコマンド実行によって起動する場合には、unconfined_t Typeが割り当てられるので、大半のリソースには問題なくアクセスが可能です。

しかし、targeted policyによってアクセス制御の対象となっているリソース (※) については、デフォルトでアクセスが拒否されることも考えられます。
また、systemdから起動するように構成した場合、Type Transitionを別途定義しなければsystemdと同じinit_t Typeになってしまうことでほとんどのアクセスに失敗します。
(※) 例えばsshdの設定ファイルなど

こういったケースに対応するために、2の場合は以下のように実装します。

  • 専用のTypeやAttributeを定義する
  • type_transitionルールによってTypeを割り当てる
  • allowルールによってアクセスを許可する (1の例と同じ)
  • File Contextを定義する

こちらの例は非常に応用的な内容であり、私のスキルも超えているので本記事ではビルドの手順のみお伝えします。
ソースコードの記述については触れません。
詳細な手順は、#例2: 全く制御されていないアプリケーションを新たに制御するにてお伝えします。

例1、例2の手順は、以下のRHEL8のマニュアルを参考にしました。
RHEL8 - Using SELinux - 7.2. Creating and enforcing an SELinux policy for a custom application

では、具体的な内容に入っていきます。

例1: 既に制御されているプロセス用にallowルールを追加する

本セクションでは、既存で足りていないallowルールを追加する手順をお伝えします。

正直、デフォルトのルールに足りていない設定を見つけるのは大変です...。
本セクションでは手順をお伝えすることを重視し、実用性のない具体例となっていますがご了承ください。

今回は、「httpd_tからtmp_tへのread権限を追加する」ことを例に説明します。

まずは事前準備として、Apacheをインストールしておきます。

sudo dnf install httpd

続いて、DocumentRootにtmp_t Typeを持つファイルを配置し、Apacheを起動します。

sudo bash -c 'echo aaa > /var/www/html/a'
sudo chcon -t tmp_t /var/www/html/a

ls -lZ /var/www/html/a
# -rw-r--r--. 1 root root unconfined_u:object_r:tmp_t:s0 4 Jan 22 16:43 /var/www/html/a

sudo systemctl start httpd

作成したファイルにアクセス確認します。
アクセス成功すればaaaというファイルの中身が表示されるはずですが、Permission Deniedになります。

curl localhost/a
# <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
# <html><head>
# <title>403 Forbidden</title>
# </head><body>
# <h1>Forbidden</h1>
# <p>You don't have permission to access this resource.</p>
# </body></html>

一旦Permissiveにして再度アクセスを実施し、エラーメッセージを全て出しておきます。

sudo setenforce 0

curl localhost/a
# aaa

sudo setenforce 1

続いて、SELinuxのログを確認します。

journalctl -qen all -t audit -g denied --no-pager

# Jan 22 17:17:14 fedora35.test audit[1714]: AVC avc:  denied  { open } for  pid=1714 comm="httpd" path="/var/www/html/a" dev="dm-0" ino=655778 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:tmp_t:s0 tclass=file permissive=1

# Jan 22 17:17:14 fedora35.test audit[1714]: AVC avc:  denied  { map } for  pid=1714 comm="httpd" path="/var/www/html/a" dev="dm-0" ino=655778 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:tmp_t:s0 tclass=file permissive=1

ログから、以下のアクセスが拒否されていることがわかりました。

  • Subject: httpd_t
  • Object: tmp_t
  • Class: file
  • Permission: { open map }

sesearchで確認すると、確かに該当するルールがないことがわかります。
mapのルールのみ1行表示されていますが、BooleanがFalseなので該当しません。

sesearch -A -s httpd_t -t tmp_t -c file -p open,map
# allow domain file_type:file map; [ domain_can_mmap_files ]:True

getsebool domain_can_mmap_files
# domain_can_mmap_files --> off

allow Statementで表現すると、以下のようになります。
このルールをPolicy Packageファイル (.pp) に変換し、インストールすることでエラーを解消できます。

allow httpd_t tmp_t : file { open map };

今回はエラー件数が少ないので自分でも簡単に書けますが、これをAVCから自動生成する方法もあります。
以下のようにAVCのログをaudit2allowコマンドに渡します。
すると、AVCログに書いてある情報をパースしてallow Statementを表示してくれます。
以下はjournalctlで行っていますが、もちろんsudo ausearch -m avc,user_avc | audit2allowでも同様にallow Statementの表示が可能です。

journalctl -qen all -t audit -g denied --no-pager | tail -2 | audit2allow
# #============= httpd_t ==============
# allow httpd_t tmp_t:file open;

# #!!!! This avc can be allowed using the boolean 'domain_can_mmap_files'
# allow httpd_t tmp_t:file map;

必要に応じてこの情報をファイルに出力し、更にallow httpd_t tmp_t:file { open map };のようにまとめて書くことでallowルールを楽に記述できます。

さて、ここからModule Policyをビルドしてインストールする手順に入ります。
以下の手順を行うために、前提としてpolicycoreutils-devel RPMパッケージをインストールしておきます。

sudo dnf install policycoreutils-devel

今回はコマンドのみ示しますが、処理の中身やソースコードの書き方に興味のある方は後続記事のSELinux Module Policyのソースコード読解、ビルド - #Module Policyのビルドの流れをご覧ください。
上記リンクは参考情報のため、スキップしても問題はありません。

まず、作業用フォルダを作って入っておきます。

mkdir ~/work

以下の内容を持つ~/work/my_httpd.teファイルを作成します。
my_httpdはModule Policyの名前にもなるので、必要に応じて名前を変更してください。

policy_module(my_httpd, 1.0.0)
gen_require(`type httpd_t, tmp_t;')
allow httpd_t tmp_t : file { open map };

以下のコマンドでビルドします。
makeによって必要な中間ファイルが自動生成していることがわかります。
今回はmy_httpd.fcmy_httpd.ifに何も書いていないので、自動生成したファイルの中身も空っぽです。

cd ~/work
make -f /usr/share/selinux/devel/Makefile my_httpd.pp

ls -F
# my_httpd.fc  my_httpd.if  my_httpd.pp  my_httpd.te  tmp/

生成したmy_httpd.ppファイルがCustom PolicyのPolicy Packageです。
このファイルをインストールすることで、Security Policyが更新されて効果を発揮するようになります。

sudo semodule -i my_httpd.pp
# または、sudo semanage module -a my_httpd.pp

sudo semanage module -l | grep my_httpd
# my_httpd                  400       pp 

再びcurlアクセスし、httpd_tからtmp_tにアクセスできるか確認します。
今度はうまくいきました。

curl localhost/a
# aaa

確認できたので、my_httpdをアンインストールしておきます。

sudo semodule -r my_httpd
# libsemanage.semanage_direct_remove_key: Removing last my_httpd module (no other my_httpd module exists at another priority).

# または、sudo semanage module -r my_httpd

作業フォルダも削除しておきます。

rm -rf ~/work

例2では、上記とは異なる方法でビルド・インストールします。
例1についても、例2と同様の方法でインストールすることは可能です。
その場合、以下のようなコマンドでビルド・インストールします。
manファイルやrpmファイルを自動生成したいときには便利かもしれません。

こちらのビルド手順については、次のセクションで詳細に説明します。

sudo dnf install policycoreutils-devel rpm-build

mkdir ~/work
cd ~/work
sepolicy generate --init my_httpd

vi my_httpd.te
# (上述のmy_httpd.teと同じ内容に書き換える)

# デフォルトの記述を削除
echo > my_httpd.fc
echo > my_httpd.if

# ビルド、インストール
sudo ./my_httpd.sh 

# 確認
sudo semodule -l | grep my_httpd
# my_httpd

# 後片付け
sudo semodule -r my_httpd
sudo rm -rf ~/work

例2: 全く制御されていないアプリケーションを新たに制御する

既存で制御されていないアプリケーション用のCustom Policyを記述するには、allow Statementだけでなく以下のような定義が必要になります。

  • Type, Attribute
  • type_transitionルール (Domain Transition, Object Transition)
  • allowルール (例1と同じ)
  • File Context

こちらの例では、ソースコードの書き方までは触れません。
ビルドの手順のみざっとお伝えします。

ご参考までに、ソースコード記述の基礎については後続の記事で軽く触れています。
ソースコードを記述する場面はほぼないと思いますが、必要な方はご覧いただければと思います。
参考: SELinux Module Policyのソースコード読解、ビルド

では、手順の紹介に入ります。

例1と同様、手順の実施にはpolicycoreutils-devel RPMパッケージが必要になります。
またビルド・インストールそのものには必須ではありませんが、RPMファイルの自動生成も行う場合はrpm-buildも必要になります。

sudo dnf install policycoreutils-devel rpm-build

先ほどと同様に、作業フォルダを作ります。

mkdir ~/work

作業フォルダに入り、必要なファイルを一括自動生成します。
本来であれば、引数には制御対象のプロセスを起動するための実行ファイルのパスを指定します。
ただ今回は練習ということで、「カレントディレクトリにあるmy_appファイル」を実行ファイルとして扱い、スクリプトを起動します (※)
(※) 実運用で使う際は、こちらのファイルパスを正確に指定する必要があります (/usr/local/bin/my_appなど)。rpmファイルでインストールしたときに配置するファイルパスや、manページの記述、te/if/fcファイルの記述、シェルスクリプト中で自動実行されるrestoreconの対象ファイルなどに影響します。

my_appファイルは実際に存在しなくても以下の--initコマンドは通りますが、後続のmy_app.shのrestoreconコマンドが失敗してしまいます。
それを防ぐため、今回はtouchコマンドによって事前にmy_appファイルを作っておきます。

touch my_app
sepolicy generate --init my_app

# ***************************************
# Warning /home/endy/work/my_app does not exist
# ***************************************

# Created the following files:
# /home/endy/work/my_app.te # Type Enforcement file
# /home/endy/work/my_app.if # Interface file
# /home/endy/work/my_app.fc # File Contexts file
# /home/endy/work/my_app_selinux.spec # Spec file
# /home/endy/work/my_app.sh # Setup Script

本来であれば、ここでte, if, fcファイルにアクセス制御ルールを記述します。
また、RPMファイルを活用する場合はspecファイルに細かいオプションを指定します。
ただ、今回はビルド手順の説明なのでその工程はスキップします。

ソースコード記述の流れとしては、以下のように進めるのが効率的かなと想定しています。

  • allowルール以外を先に全て記述する
  • Permissiveモードで動作確認する
  • 監査ログをaudit2allowに渡してallowルールを自動生成させたものを参考にallowルールも記述する

ですが、今回はソースコードの記述についても割愛します。
デフォルトの自動生成したソースコードを利用し、このままビルド・インストールします。

sudo ./my_app.sh
# Building and Loading Policy
# + make -f /usr/share/selinux/devel/Makefile my_app.pp
# make: 'my_app.pp' is up to date.
# + /usr/sbin/semodule -i my_app.pp
# + sepolicy manpage -p . -d my_app_t
# ./my_app_selinux.8
# + /sbin/restorecon -F -R -v /home/endy/work/my_app
# ++ pwd
# + pwd=/home/endy/work
# + rpmbuild --define '_sourcedir /home/endy/work' --define '_specdir /home/endy/work' --define '_builddir /home/endy/work' --define '_srcrpmdir /home/endy/work' --define '_rpmdir /home/endy/work' --define '_buildrootdir /home/endy/work/.build' -ba my_app_selinux.spec
# setting SOURCE_DATE_EPOCH=1642896000
# Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.GYx2lV
# + umask 022
# + cd /home/endy/work
# + '[' /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64 '!=' / ']'
# + rm -rf /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64
# ++ dirname /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64
# + mkdir -p /home/endy/work/.build
# + mkdir /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64
# + install -d /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/selinux/packages
# + install -m 644 /home/endy/work/my_app.pp /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/selinux/packages
# + install -d /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/selinux/devel/include/contrib
# + install -m 644 /home/endy/work/my_app.if /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/selinux/devel/include/contrib/
# + install -d /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/man/man8/
# + install -m 644 /home/endy/work/my_app_selinux.8 /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/usr/share/man/man8/my_app_selinux.8
# + install -d /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64/etc/selinux/targeted/contexts/users/
# + /usr/lib/rpm/check-buildroot
# + /usr/lib/rpm/redhat/brp-ldconfig
# + /usr/lib/rpm/brp-compress
# + /usr/lib/rpm/brp-strip /usr/bin/strip
# + /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump
# + /usr/lib/rpm/redhat/brp-strip-lto /usr/bin/strip
# + /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip
# + /usr/lib/rpm/check-rpaths
# + /usr/lib/rpm/redhat/brp-mangle-shebangs
# + /usr/lib/rpm/redhat/brp-python-bytecompile '' 1 0
# + /usr/lib/rpm/redhat/brp-python-hardlink
# Processing files: my_app_selinux-1.0-1.fc35.noarch
# Provides: my_app_selinux = 1.0-1.fc35
# Requires(interp): /bin/sh /bin/sh
# Requires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1
# Requires(post): /bin/sh policycoreutils selinux-policy-base >= 35.7-1
# Requires(postun): /bin/sh policycoreutils
# Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64
# Wrote: /home/endy/work/my_app_selinux-1.0-1.fc35.src.rpm
# Wrote: /home/endy/work/noarch/my_app_selinux-1.0-1.fc35.noarch.rpm
# Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.CVjduT
# + umask 022
# + cd /home/endy/work
# + /usr/bin/rm -rf /home/endy/work/.build/my_app_selinux-1.0-1.fc35.x86_64
# + RPM_EC=0
# ++ jobs -p
# + exit 0

シェルスクリプトの中身では、主に以下の処理を実行しています。

  • make -f /usr/share/selinux/devel/Makefile my_app.pp || exit
  • /usr/sbin/semodule -i my_app.pp
    • my_app Moduleのインストール
  • sepolicy manpage -p . -d my_app_t
    • manページファイルを生成。配置はしない
  • /sbin/restorecon -F -R -v /home/endy/work/my_app
  • rpmbuild ...
    • RPMファイルの生成

シェルスクリプトによって、my_appモジュールがインストールされています。

sudo semodule -l | grep my_app
# my_app

RPMパッケージとmanファイルが生成しています。

file my_app_selinux.8
# my_app_selinux.8: troff or preprocessor input, ASCII text

file noarch/my_app_selinux-1.0-1.fc35.noarch.rpm 
# noarch/my_app_selinux-1.0-1.fc35.noarch.rpm: RPM v3.0 bin i386/x86_64 my_app_selinux-1.0-1.fc35

manファイルはカレントディレクトリにありますが、/usr/share/man/man8には配置されていないため、mandbには登録されていません。

man my_app_selinux
# No manual entry for my_app_selinux

man ./my_app_selinux.8
# ファイルを直接指定すれば見える

一旦Moduleをアンインストールして、RPMをインストールしてみます。
すると、Moduleがインストールされ、manも表示できるようになります。
Moduleを配布するときはRPM形式が便利です。

sudo semodule -r my_app
sudo dnf install noarch/my_app_selinux-1.0-1.fc35.noarch.rpm

sudo semodule -l | grep my_app
# my_app

man my_app_selinux
# (表示される)

RPMをアンインストールすると、my_app Moduleもアンインストールされます。

sudo dnf remove my_app_selinux

sudo semodule -l | grep my_app
# (出力なし)

man my_app_selinux
# No manual entry for my_app_selinux

my_app_selinux.specを見るとインストール/アンインストール時に実行されるコマンドが書いてあります。
また、rpm -ql my_app_selinuxからRPMインストール時に配布するファイルを確認できます。
興味のある方はぜひ実機で試してみてください。

最後に、workフォルダを削除しておきます。

sudo rm -rf ~/work

(参考) SELinuxの確認コマンド

ここでは、他のセクションで言及されなかったコマンドや調査方法を中心にここで紹介します。
また、#sesearch#seinfoの詳しい使い方にも軽く触れます。

sesearch

sesearchによって、SELinuxのアクセス制御ルールを検索できます。

検索対象を指定するオプションを1つ、または複数指定する必要があります。
検索対象を指定するオプションは、主に以下です。

オプション 意味
-A allow, allowxpermルールを検索する
--type_trans type_transitionルールを検索する

デフォルトでは、全てのルールを表示します。
フィルタしないと、数万行ものルールが表示されてしまいます。

フィルタを指定するオプションは、主に以下があります。
オプションに指定するパラメータは、カンマ区切りで複数指定できます。

オプション 意味
-s Type source Typeでフィルタする。Attributeも指定可
-t Type target Typeでフィルタする。Attributeも指定可
-c Class Object Class でフィルタする
-p Permission Permission でフィルタする
-b Boolean Boolean に関連するもののみにフィルタする
-D Default --type_transなどと一緒に使い、Default Typeなどでフィルタする

デフォルト動作では、例えば-s Typeを指定するとsourceに指定したType、またはそのTypeを内包するAttributeが指定されたルールのみにフィルタされます。
以下のオプションを-s-tと共に指定することで、上記のフィルタ挙動を変更することができます。

オプション 意味
-ds -s Typeと共に指定すると、そのTypeを内包するAttributeがヒットしなくなる
-dt -dsと同様。-tとセットで使う
-rs -sの検索に正規表現を使用する
-rt -tの検索に正規表現を使用する

他にも色々オプションがあるので、お試しください。

seinfo

seinfoは、Type、Attribute、Class、ポート番号などのObjectの情報を表示するコマンドです。

何も付けずに実行すると、統計情報を表示します。

seinfo
# (一部抜粋)
  # Classes:             134    Permissions:         456
  # Types:              4990    Attributes:          251

代表的なオプションは以下の通りです。

オプション 意味
-t Typeの一覧表示
-a Attributeの一覧表示
-c Classの一覧表示
--common Commonの一覧表示
--portcon ポート番号とSecurity Contextの紐付け一覧表示

-xと併用すると、オプションによってはより詳細な情報が表示されます。
よく使うのは、以下の組み合わせです。

組み合わせ 用途
-xa, -xt TypeとAttributeの紐付け
-xc, -x --common ClassとPermissionの紐付け

例えば、TypeやAttributeの意味を調べたいときは以下のようなコマンドをよく使います。

ClassとPermissionの紐付けを知りたい時はseinfo -xcseinfo -x --commonを使います。
更にPermissionの意味を知りたいときは、The SELinux NotebookのAppendix Aを参照します。

現在のSecurity Contextの調べ方

現在のSecurity Contextを調べるコマンドは、以下のとおりです。

調査対象 コマンド例
ファイル ls -Z
プロセス
  • ps -eZ
  • ps -eo user,cmd,label
ポート番号
  • seinfo --portcon
  • sudo semanage port -l
ユーザー(※) id -Z

(※) id -ZはRBACを利用している環境においては便利なこともありますが、基本的には使いません。id -Zps -Zで表示されるログインシェルのコンテキストは同じなので、id -Zが必須になる場面も実はありません。

(※) ss -Zというコマンドはほぼ使わないため、表に載せていません

(参考) manの横断検索

selinux-policy-doc RPMパッケージがインストール済みであれば、*_selinuxというmanページを参照できます。
情報量はmanページによってまちまちですが、少なくともBooleanの初期値は確実に書いてあります。
他の情報はあまり書いてないことが多いので、「見つかったらラッキー」ぐらいの気持ちで利用するのがちょうど良いと思います。

例えば、cron_userdomain_transitionというBoolean値のデフォルト値を知りたかったとすると、以下のようにキーワード検索します。

man -Kw cron_userdomain_transition
# /usr/share/man/man8/openshift_app_selinux.8.gz
# /usr/share/man/man8/openshift_selinux.8.gz
# /usr/share/man/man8/staff_selinux.8.gz
# /usr/share/man/man8/sysadm_selinux.8.gz
# /usr/share/man/man8/unconfined_selinux.8.gz
# /usr/share/man/man8/user_selinux.8.gz
# /usr/share/man/man8/crond_selinux.8.gz

続いて、検索にヒットしたmanページを確認します。
試しに一番上にあるman openshift_app_selinuxを確認すると、デフォルトがTrueであることがわかります。

cron_userdomain_transition boolean. Enabled by default.

他にもTypeやAttributeでも検索できますが、ヒットしなかったり、必要な情報が書いていないことがほとんどです。
manで情報が見つからなかった場合は、以下のように別のアプローチに切り替える必要があります。

  • #seinfoでTypeとAttributeの紐付け情報を探る
  • #sesearchでどのようなallowルールで使われているか探る

(参考) Local Policyのexport/import

semanageを活用することで、ソースコードを記述すること無くLocal Policyを定義してSELinuxの動作を変えることができます。

このLocal Policyをexport、importすることで、複数のマシンに同じ設定を適用することが可能です。

exportは以下のコマンドで行います。
出力されているのはsemanageのサブコマンドです。
最初に-Dで設定をクリアしてから、一番下の行で今の設定を適用するようなスクリプトになっています。

sudo semanage export
# boolean -D
# login -D
# interface -D
# user -D
# port -D
# node -D
# fcontext -D
# module -D
# ibendport -D
# ibpkey -D
# permissive -D
# boolean -m -1 httpd_can_connect_zabbix

exportした内容は、-fオプションでファイルに保存できます。

sudo semanage export -f semanage.txt
cat semanage.txt
# boolean -D
# login -D
# interface -D
# user -D
# port -D
# node -D
# fcontext -D
# module -D
# ibendport -D
# ibpkey -D
# permissive -D
# boolean -m -0 httpd_can_connect_zabbix

exportしたファイルをsemanage import -fに渡すことで、設定をインポートできます。
exportしたファイルをGit管理して、Ansibleのような自動化ツールによって共通したSELinux設定を配布するような使い方ができるのではないかと思います。

sudo semanage boolean -D
sudo semanage boolean -lC
# (出力なし)

sudo semanage import -f semanage.txt
sudo semanage boolean -lC
# SELinux boolean           State  Default Description
# httpd_can_connect_zabbix  (off  ,  off)  Allow http daemon to connect to zabbix

(参考) Apacheのアンインストール

本記事で検証用にインストールしたApacheのアンインストール手順を書いておきます。

sudo dnf remove httpd
sudo rm -rf /etc/httpd /var/log/httpd /var/www ~/public_html/

今回編集したファイル+αはアンインストール後も残るので、手動である程度消しています。

まとめ

SELinuxでアクセス拒否エラーが出た時の切り分け手順と、原因調査や修正に必要なコマンドを紹介しました。
また、基本操作としてSELinuxを無効化する手順にも触れました。

次の記事

SELinuxシリーズの本編は、本記事で最後です。
次回以降の記事は、全て参考情報です。

次の記事では、Type Enforcement以外のアクセス制御方式であるRBAC, UBAC, MLS, MCSの概要を紹介します。
MCS以外についてはmls policyでしか基本使われないため、多くの方にとってはあまり知らなくても良い内容だと思います。

「なぜ多くの人は知らなくてよいのか」といった部分が気になる方は、その部分を中心にお読みいただければと思います。

endy-tech.hatenablog.jp