TECH MEMO / FIELD NOTE『とりあえず無効化』を卒業。Google Cloud(GCE)× AlmaLinuxで学ぶSELinuxの基本
Amazon Linux 2027を機にSELinuxを学び直す
2026年9月、AWSから「Amazon Linux 2027」のパブリックプレビューが発表されました。SELinuxはデフォルトでEnforcingモードになります。発表内容は「Amazon Linux 2027がパブリックプレビューで利用可能に」を参照してください。SELinuxの仕様は「Security updates and features」にまとまっています。執筆時点ではプレビューであり、本番環境での利用は推奨されていません。
Amazon Linux 2023では、SELinuxは警告ログだけを出すPermissiveモードがデフォルトです。ポリシー違反があっても処理は止まらないため、AVCログを見ていなければ問題に気づきません。
Amazon Linux 2027では方針が変わり、SELinuxがデフォルトで「Enforcing(強制)モード」に設定されます。Enforcingモードでは、SELinuxポリシーに反するアクセスが拒否されます。root権限を持つプロセスでも、制限対象のSELinuxドメインで動いていればポリシーの対象です。
「とりあえず無効化」する前に原因を確認する
Linuxサーバーでは、「設定もパーミッションも合っているはずなのに動かない」ことがあります。
ApacheやNginxの設定ファイルに問題がなく、ファイル所有者(chown)や権限(chmod)も正しい。それでもサービスが起動せず、ブラウザから 403 Forbidden や 502 Bad Gateway が返ることがあります。
エラー文で検索すると、技術記事やQ&Aサイトで次のような手順を見かけます。
# SELinuxを一時的にPermissiveモードへ変更
sudo setenforce 0
# 古い記事などで見かける設定例(本記事では実行しません)
sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
ここでサービスが動くようになると、原因を直さないまま無効化したくなります。ただし、setenforce 0 は一時的にPermissiveモードへ切り替えるコマンドであり、SELinux自体を無効化するものではありません。RHEL 9系でSELinuxを完全に無効化するには、/etc/selinux/config だけでなくカーネルコマンドラインの変更が必要です。
Amazon Linux 2027へ移行する場合、Permissiveモードではログだけに現れていた拒否が、サービスの起動や処理を止める可能性があります。移行前にAVCログを確認し、必要な設定を洗い出す必要があります。
SELinux(Security-Enhanced Linux)は、プロセスが侵害された場合のアクセス範囲をポリシーで制限する、多層防御の一つです。脆弱性そのものを修正する機能ではありませんが、侵害後の被害を抑える助けになります。理由を確認せず無効化すると、その防御層を一つ失います。
今回確認すること
今回は、標準でSELinuxがEnforcingモードになっている AlmaLinux 9 を使います。検証環境は Google CloudのCompute Engine(GCE) に作りました。VMとディスクはGoogle Cloudの無料枠に収まります。ただし、後述する外部IPv4アドレスは無料枠の対象外です。
TerraformとIAP(Identity-Aware Proxy)を組み合わせて環境を作り、あえて拒否を発生させます。ログから原因を絞り込み、SELinuxを有効にしたまま設定を直す流れを確認します。
SELinuxと従来のアクセス制御の違い
DACではrootの権限が強い
従来のLinux標準のセキュリティは、DAC(Discretionary Access Control / 任意アクセス制御) と呼ばれます。ファイルやディレクトリごとに「所有者(Owner)」「グループ(Group)」「その他(Other)」の読み取り・書き込み・実行権限(rwx)を設定する仕組みです。
DACでは、rootは通常のファイルパーミッションを回避できる強い権限を持ちます。
Webサーバー(NginxやApache)やコンテナが侵害され、攻撃者がroot権限まで取得すると、DACだけでは影響範囲が広がります。Linux capabilities、コンテナ分離、読み取り専用ファイルシステムなど、ほかの制約が働く場合は別です。
SELinuxが提供する強制アクセス制御(MAC)
これに対してSELinuxは、MAC(Mandatory Access Control / 強制アクセス制御) を実装するLinuxカーネルのセキュリティモジュールです。
MACでは、DACの判定に加えて、LinuxカーネルがSELinuxポリシーを適用します。RHEL系で標準の targeted ポリシーは、すべてのプロセスを同じ強さで制限するわけではありません。Nginxのような対象サービスは専用ドメインへ閉じ込める一方、通常のログインユーザーは既定で unconfined_t として動きます。
- 最小特権をポリシーで補う: プロセスがアクセスできるポートやファイルを、DACとは別のルールで絞れます。
- root権限だけでは回避できない: 制限対象のドメインで動くプロセスは、Linux上の実行ユーザーがrootでもSELinuxポリシーに従います。
SELinuxのメリット・デメリット
SELinuxは侵害時の影響を抑える助けになる一方、ログの読み方やラベル管理を覚える必要があります。
| 観点 | 従来のLinuxアクセス制御(DAC) | SELinuxによる強制アクセス制御(MAC) |
|---|---|---|
| 制御の単位 | ユーザー / グループごとの権限(rwx) | プロセスとリソースの「ラベル(型)」によるポリシー制御 |
| root権限の扱い | 通常のファイルパーミッションを広く回避できる | 制限対象のドメインでは、root権限だけでポリシーを回避できない |
| 侵害時の影響 | root権限まで奪われると影響範囲が広がりやすい | ポリシーが許可していない操作を拒否し、影響範囲を狭められる可能性がある |
| 運用の容易さ | 直感的で学習コストが低い | ラベルやブール値の理解が必要で導入障壁が高い |
| トラブル時の挙動 | 通常のPermission denied(原因特定が容易) | AVC拒否ログ(監査ログを読まないと原因が分かりにくい) |
たとえば、Webサーバーのプロセスが httpd_t などの制限対象ドメインで動いていれば、ポリシーで許可されていないファイルの読み取りや通信を拒否できます。ただし、ポリシーで許可済みの操作やSELinux自体の脆弱性まで防げるわけではありません。
運用では、「型(Type)」「ドメイン」「AVCログ」「SELinuxブール値」などの概念を理解し、ポリシー変更を環境間で管理する必要があります。それでも、原因を確認せず無効化するより、拒否された操作を特定して必要な許可だけを追加する方が、意図した防御を保てます。
検証環境の設計:GCE × AlmaLinux 9 × Terraform
検証目的とはいえ、パブリックIPのSSHポート(22番)をそのままインターネットに公開するのは避けます。今回は、SSHの接続元をIAPに限定する環境をTerraformで構築します。
すでにAlmaLinux 9の検証環境がある場合は、「Nginxのポート変更による拒否の再現」まで読み飛ばせます。
VMとブートディスクは、Google Cloudの無料枠の条件を満たす構成にします。無料枠の対象や上限は変更される可能性があるため、作成前に公式の「無料のクラウド機能とトライアル特典」を確認してください。
無料枠(Always Free)の主な条件
- マシンタイプ: 非プリエンプティブルの
e2-micro(ゲストOSからは2 vCPU、共有CPUは合計25%相当、1 GBメモリ) - ブートディスク: 標準永続ディスク(
pd-standard)を月30 GBまで - リージョン:
us-central1、us-west1、us-east1のいずれか
これは1請求先アカウントあたりの月間無料使用量です。複数の対象リージョンやVMで使った時間は合算されます。また、外部IPv4アドレスや無料枠を超えたデータ転送は別料金です。
IAP接続とアウトバウンド通信の注意点
IAPトンネリングを利用すると、外部からのインバウンドSSH通信をGoogleの管理IP帯(35.235.240.0/20)経由に限定できます(参考:IAPを使用したTCP転送)。ただし、IAP用の許可ルールを追加しただけでは、default-allow-ssh など既存の許可ルールは無効になりません。後述のTerraformでは、対象VMへの22番ポートをIAP以外から拒否するルールも追加します。
VMに外部IPを付与しない完全プライベート構成では、そのままでは VMから外部インターネットへのアウトバウンド通信ができません。そのため、パッケージ管理ツール(dnf)でリポジトリからメタデータを取得できず、タイムアウトエラーが発生します。
Errors during downloading metadata for repository 'appstream':
- Curl error (28): Timeout was reached for https://mirrors.almalinux.org/...
IAPは「外から中へのSSH接続」を中継する仕組みであり、「中から外への通信」を代理するNATではありません。外部IPを持たないVMからインターネットへ出るには、Cloud NATやプロキシなどが別途必要です。
今回は、パッケージ導入とブラウザからの動作確認を簡単にするため、エフェメラル外部IPv4アドレスを1つ付与します。エフェメラルIPv4アドレスもVMの起動中は課金対象です。料金は「Virtual Private Cloud の料金」で確認できます。無料枠だけで完結する構成ではない点に注意してください。
Terraformコード
以下の3ファイルを用意してデプロイします。
main.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
# 1. Google IAP経由のSSH接続(TCP:22)を許可
resource "google_compute_firewall" "allow_iap_ssh" {
name = "allow-iap-ssh"
network = "default"
description = "Allow SSH from Google IAP"
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["35.235.240.0/20"]
target_tags = ["iap-ssh"]
priority = 1000
}
# IAP以外から対象VMのTCP:22へ接続できないようにする
# allow_iap_sshの方が優先度が高いため、IAPのIP帯だけは許可される
resource "google_compute_firewall" "deny_direct_ssh" {
name = "deny-direct-ssh"
network = "default"
description = "Deny direct SSH except traffic allowed by a higher-priority rule"
direction = "INGRESS"
priority = 1100
deny {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["iap-ssh"]
}
# 2. Web確認用ポート(80, 8080, 8888)のアクセスを許可
resource "google_compute_firewall" "allow_web" {
name = "allow-web-traffic"
network = "default"
description = "Allow Web traffic from anywhere"
allow {
protocol = "tcp"
ports = ["80", "8080", "8888"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["http-server"]
}
# 3. GCEインスタンス(AlmaLinux 9 / 無料枠スペック)
resource "google_compute_instance" "selinux_lab" {
name = "selinux-lab-instance"
machine_type = "e2-micro"
zone = var.zone
boot_disk {
initialize_params {
image = "almalinux-cloud/almalinux-9"
size = 30
type = "pd-standard"
}
}
network_interface {
network = "default"
# パッケージ導入やブラウザ確認のためエフェメラル外部IPを付与
access_config {
}
}
tags = ["http-server", "iap-ssh"]
allow_stopping_for_update = true
}
output "instance_external_ip" {
description = "VMの外部IPアドレス"
value = google_compute_instance.selinux_lab.network_interface[0].access_config[0].nat_ip
}
output "ssh_iap_command" {
description = "IAP経由の接続コマンド"
value = "gcloud compute ssh ${google_compute_instance.selinux_lab.name} --zone=${var.zone} --tunnel-through-iap --project=${var.project_id}"
}
variables.tf
variable "project_id" {
description = "Google Cloud Project ID"
type = string
}
variable "region" {
description = "Region for Free Tier"
type = string
default = "us-central1"
}
variable "zone" {
description = "Zone for Free Tier"
type = string
default = "us-central1-a"
}
terraform.tfvars
project_id = "your-gcp-project-id"
region = "us-central1"
zone = "us-central1-a"
環境を作成したら、出力されたコマンドを使ってIAP経由でVMへログインします。
このコードは、default VPCが存在し、対象プロジェクトでCompute Engine APIとIAP APIが有効になっていることを前提にしています。実行ユーザーには、IAPトンネルとVMログインに必要なIAM権限も必要です。80・8080・8888番は検証のためインターネット全体へ公開するので、検証後は terraform destroy で削除します。
terraform init
terraform apply
# IAPトンネル経由でSSH接続
gcloud compute ssh selinux-lab-instance --zone=us-central1-a --tunnel-through-iap --project=your-gcp-project-id
実践:Nginxのポート変更による拒否の再現
ログインしたら、Nginxの待ち受けポートを標準の80番から変更し、SELinuxによる拒否を再現します。
1. Nginxの導入と8080番ポートの挙動
まずはNginxをインストールして起動します。
sudo dnf install -y nginx policycoreutils-python-utils
sudo systemctl enable --now nginx
OS側で firewalld が有効な場合は、Google Cloudのファイアウォールとは別に検証用ポートを許可します。
sudo firewall-cmd --permanent \
--add-port=80/tcp \
--add-port=8080/tcp \
--add-port=8888/tcp
sudo firewall-cmd --reload
ブラウザで http://<外部IP>/ にアクセスすると、Nginxのデフォルトページ(Web Server Test Page)が表示されます。
ここで、ポートを8080番に変更してみます。設定ファイルを書き換えてサービスを再起動すると、問題なく起動し、ブラウザからもアクセスできてしまいます。
なぜ8080番は動くのでしょうか。SELinuxのポート型を確認すると理由が分かります。
sudo semanage port -l | grep 8080
# 出力例:
# http_cache_port_t tcp 8080, 8118, 8123, 10001-10010
SELinuxの標準ポリシーでは、httpd_t ドメインに http_port_t(80、443など)だけでなく、http_cache_port_t(8080など)へのバインドも許可されています。Red Hatの「非標準構成のアプリケーションおよびサービス用にSELinuxを設定する」にも、ポート型の一覧を確認し、非標準ポートへ http_port_t を追加する手順があります。
2. 未許可ポート(8888)への変更
次に、今回のAlmaLinux 9環境ではWebサーバー用の型が付いていなかった 8888番ポートへ変更します。ポート型はポリシーのバージョンやローカル変更で異なる可能性があるため、先に確認します。
sudo semanage port -l | grep -w 8888
何も表示されないことを確認してから進めます。8888番が表示された場合は、割り当て済みの型と用途を先に確認します。別の型が割り当てられているポートには、後述の semanage port -a をそのまま実行できません。
/etc/nginx/nginx.conf の設定を変更します。
server {
listen 8888;
listen [::]:8888;
server_name _;
root /usr/share/nginx/html;
...
設定ファイルの文法チェックを実行します。
sudo nginx -t
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
文法チェックは正常に通ります。nginx -t は設定ファイルの構文や参照先を確認しますが、設定した待ち受けポートでサービスを起動する処理ではないため、この時点では name_bind の拒否は発生しません。
一方、systemd経由でNginxを再起動すると起動に失敗します。
sudo systemctl restart nginx
# Job for nginx.service failed because the control process exited with error code.
# See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
設定に誤りはありませんが、プロセスが起動できません。SELinuxによる拒否が疑われるため、AVCログを確認します。
原因の特定:AVC監査ログを読み解く
「動かないからとりあえずPermissiveにする」と判断する前に、まずは監査ログを確認します。
SELinuxのアクセス拒否ログ(AVC: Access Vector Cache)は /var/log/audit/audit.log に記録されます。監査ログ検索ツール ausearch を使うと、目的の拒否ログをすばやく抽出できます。
sudo ausearch -m avc -c nginx --interpret
# または直近の拒否ログを確認
sudo ausearch -m avc -ts recent
出力されるログから、該当箇所を確認します。次は読み方を示すために項目を抜き出した例であり、時刻、PID、監査イベント番号は環境ごとに変わります。
type=AVC msg=audit(1727600000.123:456): avc: denied { name_bind } for pid=31523 comm="nginx" src=8888 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
この1行から、今回の拒否に関係する主体、対象、操作を読み取れます。
| 項目 | ログの値 | 内容・意味 |
|---|---|---|
comm | "nginx" | アクションを実行しようとしたコマンド名 |
denied { ... } | { name_bind } | ソケットへのポート割り当て(bind)が拒否された |
src | 8888 | 対象となったポート番号 |
scontext | ...:httpd_t:... | プロセス側の型(ドメイン)。Webサーバー用の httpd_t |
tcontext | ...:unreserved_port_t:... | リソース側の型(タイプ)。未予約ポートの unreserved_port_t |
tclass | tcp_socket | 対象となったリソースのクラス(TCPソケット) |
このログから、「Webサーバーのプロセス(httpd_t)が、Web用として許可されていないポート(unreserved_port_t: 8888)にバインドしようとしたため、SELinuxが遮断した」という事実が読み取れます。
SELinuxを有効にしたまま解決する
原因が「8888番ポートにWebサーバー用の型が付いていないこと」だと分かれば、8888番へ http_port_t を割り当てます。
管理ツールの導入
ポートやファイルコンテキストを管理する semanage コマンドは、先ほどインストールした policycoreutils-python-utils パッケージに含まれます。未導入の場合は、次のコマンドで追加します。
sudo dnf install -y policycoreutils-python-utils
一時的な切り分けの作法(Permissiveモード)
SELinuxが原因かを切り分けるときは、設定ファイルを disabled に書き換えず、一時的に Permissiveモード(拒否は行わずログだけを出す状態) を使います。
# Permissiveモードに変更(再起動不要で即時反映)
sudo setenforce 0
# サービスが起動することを確認
sudo systemctl restart nginx
# 確認が済んだら即座に Enforcing に戻す
sudo setenforce 1
# Enforcingで同じ拒否が再現することを確認
sudo systemctl restart nginx
Permissiveモードで起動できたら、AVCログを確認してEnforcingへ戻します。その後、Nginxを再起動して同じ拒否が再現すれば、SELinuxポリシーが起動失敗に関係していると判断できます。Permissiveモードで起動したプロセスは、setenforce 1 を実行しただけでは再起動されません。
恒久対応:ポート型(http_port_t)の追加
semanage port コマンドで、8888番ポートを http_port_t に追加します。
sudo semanage port -a -t http_port_t -p tcp 8888
-a は未登録のポートを追加する指定です。8888番に別の型がすでに割り当てられている場合はエラーになるため、既存用途との競合を確認し、別のポートを選ぶか、意図を確認した上で -m による変更を検討します。
登録内容を確認します。
sudo semanage port -l | grep http_port_t
http_port_t tcp 8888, 80, 81, 443, 488, 8008, 8009, 8443, 9000
http_port_t の許可リストに 8888 が追加されたことを確認できます。
Nginxの再起動と確認
再度Nginxを再起動します。
sudo systemctl restart nginx
sudo systemctl status nginx
エラーなく Active: active (running) となり、ブラウザから http://<外部IP>:8888/ へアクセスしてWebページが表示されることを確認できます。
SELinuxを無効化せず、必要なポートだけをWebサーバー用として登録できました。
SELinuxは型とポリシーでアクセスを判断する
1. プロセスとリソースを型で識別する
SELinuxでは、プロセス(主体)と、ファイルやポート、ディレクトリなどのリソース(客体)にセキュリティコンテキストと呼ばれる「ラベル」が付きます。
- プロセスの型(ドメイン): プロセスに付与されるラベル(例:
httpd_t) - リソースの型(タイプ): ファイルやポートに付与されるラベル(例:
http_port_t、httpd_sys_content_t)
カーネル内のSELinuxは、「プロセス側のドメイン」が「対象リソースのタイプ」に対して指定の操作を実行してよいかを、ロードされたポリシーと照合して判定します。
2. /etc/selinux/config は起動時のモードを指定する
/etc/selinux/config では、OS起動時のモードや利用するポリシーを指定します。enforcing はポリシーを強制し、permissive は拒否せずにAVCログを記録します。
RHEL 9系では、SELINUX=disabled だけでSELinuxを完全に無効化する方式は廃止されました。完全に無効化する場合は、カーネルコマンドラインへ selinux=0 を追加します。Red Hatは、トラブルシューティングにはPermissiveモードを使うよう案内しています(参考:SELinuxの状態およびモードの変更)。
個別の設定変更には、semanage や setsebool などの専用コマンドを使います。semanage で追加したローカル変更は永続化され、起動時にロードされるポリシーへ反映されます。
3. targeted ポリシーという前提
AlmaLinux 9では、ディストリビューションが提供する targeted ポリシーを利用します。制限対象のドメインでは、ポリシーで許可されていない操作を拒否するのが基本です。一方、すべてのユーザープロセスやサービスが制限対象になるわけではありません。
Nginx、Apache、MySQL、SSHなどの主要サービスにはあらかじめ型とルールが定義されています。管理者は、カスタムポートや非標準ディレクトリなど、標準構成との差分を確認して設定します。
ポート以外で起きるSELinuxのトラブル
ポート変更以外でも、ファイルの型やブール値が原因でサービスが動かないことがあります。
1. ドキュメントルートの変更(403 Forbidden)
Webサイトの公開ディレクトリを標準の /var/www/html から /data/my-site などに変更すると、パーミッションが正しくても403エラーが発生することがあります。
原因: 新しい配置先が
var_tやdefault_tなど、Webコンテンツ用ではない型になっていると、httpd_tからの読み取りが拒否されるため。実際の型はls -Zd /data/my-siteで確認します。対処手順:
# ディレクトリ配下の正規ルールとして httpd_sys_content_t を登録 sudo semanage fcontext -a -t httpd_sys_content_t "/data/my-site(/.*)?" # 登録したコンテキストを既存ファイル群へ再帰的に適用 sudo restorecon -Rv /data/my-site
2. リバースプロキシや外部APIへの接続(502 Bad Gateway)
NginxからバックエンドのAPサーバー(Node.jsやPHP-FPM)や外部APIへ接続する構成では、SELinuxに通信を拒否されると502エラーやタイムアウトが発生します。
原因: Webサーバープロセスからバックエンドや外部APIへの接続に必要なSELinuxブール値が、既定で
offになっているため。すべての502エラーやタイムアウトがSELinuxに起因するわけではないので、AVCログも合わせて確認します。対処手順:
# 現在のブール値を確認 getsebool httpd_can_network_connect # 恒久的に許可(-P オプションを付与) sudo setsebool -P httpd_can_network_connect 1
3. SSHポートの変更によるサーバー締め出し
SSHポートを標準の22番から2222番などに変更し、sshd の再起動後に新しい接続ができなくなるケースです。
原因: 2222番ポートが
ssh_port_tに登録されていないため、sshdプロセスのバインドが拒否されてサービスが停止する。対処手順:
# 必ずポートを変更・sshd再起動する「前」に実行 sudo semanage port -a -t ssh_port_t -p tcp 2222
SELinuxだけでなく、OSのファイアウォールやクラウド側のファイアウォールでも新しいポートを許可する必要があります。既存セッションを残したまま別ターミナルから新しいポートへ接続できることを確認し、問題があれば元へ戻せる経路も用意します。
運用視点:手動操作による環境ドリフトを防ぐ
サーバー上で semanage や setsebool を手作業で実行すると、「検証環境では動いたが本番環境では動かない」といった環境ドリフト(設定差異)が起きやすくなります。
複数台へ同じ設定を配る場合は、SELinuxのカスタム設定もコードとして管理すると差分を追いやすくなります。
AnsibleでSELinuxの設定を管理する
Ansibleでは、community.general.seport、community.general.sefcontext、ansible.posix.seboolean の各モジュールを使い、必要なSELinux設定をPlaybookへ記述できます。sefcontext は既存ファイルへラベルを適用しないため、必要に応じて restorecon も実行します(参考:seport、sefcontext、seboolean)。
ゴールデンイメージを使う環境では、Packerなどのビルド工程でSELinux設定を適用する方法もあります。稼働中のサーバーを手作業で変更せず、同じイメージからVMやコンテナを作り直す運用にすると、設定差異を抑えられます。
AVCログを起点に必要な設定だけ直す
今回の検証では、Nginxの設定構文に問題がなくても、SELinuxのポート型が合わなければ起動に失敗することを確認しました。拒否が起きたときの流れは次のとおりです。
- 拒否されたら:
ausearch -m avc -ts recentで「誰が、何に対して、どんな操作を拒否されたか」を確認する。 - 切り分けたいときは:
disabledにするのではなく、setenforce 0(Permissive)で挙動を確かめ、原因を確認したらsetenforce 1(Enforcing)に戻す。 - 恒久的に直すなら:
semanage portやrestorecon、setseboolを使って適切な型やブール値を登録する。
AVCログで httpd_t、unreserved_port_t、name_bind を確認できれば、SELinux全体を緩めずに対象ポートだけを修正できます。
運用で闇雲に無効化するのではなく、拒否の理由や設定方法を理解して向き合えば、SELinuxとも良い付き合いができると思います。
