MariaDB Galera ClusterのSELinuxモジュールを作る

# はじめに
MariaDB Galera ClusterでSELinuxのせいで起動に失敗したみなさんこんにちは。

今回はSELinuxをEnforcingにしても正常に動作するようにモジュールを作成していきます。

# 作業の流れ
以下の流れで実施します。これはMariaDB Galera Clusterに限ったことではありません。項目2を、対象ソフトウェアのあらゆる操作を実施すれば他のソフトウェアでも同じ手順で実施できます。

1. SELinuxをPermissiveにする
2. IST/SSTそれぞれで同期を実施する
3. auditlogからモジュールを作成する
4. モジュールを適用する

# 対象環境
3台でクラスタを組んだ検証環境で行います。

```
[root@maria1 ~]# mysql -uroot -e "show status like 'wsrep_cluster%';"
+--------------------------+--------------------------------------+
| Variable_name            | Value                                |
+--------------------------+--------------------------------------+
| wsrep_cluster_conf_id    | 109                                  |
| wsrep_cluster_size       | 3                                    |
| wsrep_cluster_state_uuid | cf816ec9-e382-91e7-a290-768f57ae6c01 |
| wsrep_cluster_status     | Primary                              |
+--------------------------+--------------------------------------+
```

構築方法は以下の記事でまとめました。

[MariaDB Galera Cluster 構築ポイント](https://qiita.com/tk117/items/baad6947ae0f8b169868)

MariaDBのバージョンは5.5.45、OSはCentOS 6.9です。

# 作業
## SELinuxをPermissiveにする
現在の設定値を確認します。

```
[root@maria1 ~]# getenforce
Permissive
```

Enforcingの場合はsetenforce 0を実施してください。

## ISTでの同期
通常、クラスタ内の1台のサービスを停止し、すぐにサービスを開始した場合はISTによる同期が行われます。

任意の1台をサービスダウンしたあと、適当にDBに変更を加えて、サービスを開始しましょう。後でログを採取する範囲を限定するため、時刻を取得しておきましょう。

```
# サービス停止
[root@maria3 ~]# service mysql stop
Shutting down MySQL... SUCCESS! 

# 適当にDB作る
[root@maria1 ~]# mysql -uroot -e "create database selinuxtest;"
[root@maria1 ~]# mysql -uroot -e "show databases;" | grep selinuxtest
selinuxtest

# サービス開始する
[root@maria3 ~]# date; service mysql start
2018年  1月 19日 金曜日 11:22:29 JST
Starting MySQL...SST in progress, setting sleep higher. SUCCESS! 
```

SST in progressと出るのでSSTかなーと思いきや、ちゃんとISTで同期がされています。ログを見てみましょう。

```
180119 11:22:35 [Note] Event Scheduler: Loaded 0 events
180119 11:22:35 [Note] WSREP: Signalling provider to continue.
180119 11:22:35 [Note] WSREP: SST received: cf816ec9-e382-91e7-a290-768f57ae6c01:388972
180119 11:22:35 [Note] WSREP: Receiving IST: 1 writesets, seqnos 388972-388973
180119 11:22:35 [Note] /usr/sbin/mysqld: ready for connections.
Version: '5.5.45-MariaDB-wsrep'  socket: '/var/lib/mysql/mysql.sock'  port: 3306  MariaDB Server, wsrep_25.11.r4026
180119 11:22:35 [Note] WSREP: IST received: cf816ec9-e382-91e7-a290-768f57ae6c01:388973
180119 11:22:35 [Note] WSREP: 0.0 (maria3): State transfer from 1.0 (maria2) complete.
180119 11:22:35 [Note] WSREP: Shifting JOINER -> JOINED (TO: 388973)
180119 11:22:35 [Note] WSREP: Member 0.0 (maria3) synced with group.
180119 11:22:35 [Note] WSREP: Shifting JOINED -> SYNCED (TO: 388973)
180119 11:22:35 [Note] WSREP: Synchronized with group, ready for connections
180119 11:22:35 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
180119 11:22:35 [Note] WSREP: (9a972f81, 'tcp://0.0.0.0:4567') turning message relay requesting off
```

ISTによってwritesetを1つ受信していることがわかります。

ただ、その少し前にSSTが行われているような様子も見られるので、謎。

```
180119 11:22:32 [Note] WSREP: New cluster view: global state: cf816ec9-e382-91e7-a290-768f57ae6c01:388972, view# 111: Primary, number of nodes: 3, my index: 0, protocol version 3
180119 11:22:32 [Warning] WSREP: Gap in state sequence. Need state transfer.
180119 11:22:32 [Note] WSREP: Running: 'wsrep_sst_rsync --role 'joiner' --address '192.168.1.139' --auth '' --datadir '/var/lib/mysql/' --defaults-file '/etc/my.cnf' --parent '5023''
180119 11:22:32 [Note] WSREP: Prepared SST request: rsync|192.168.1.139:4444/rsync_sst
180119 11:22:32 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
180119 11:22:32 [Note] WSREP: REPL Protocols: 7 (3, 2)
180119 11:22:32 [Note] WSREP: Service thread queue flushed.
180119 11:22:32 [Note] WSREP: Assign initial position for certification: 388973, protocol version: 3
180119 11:22:32 [Note] WSREP: Service thread queue flushed.
180119 11:22:32 [Note] WSREP: Prepared IST receiver, listening at: tcp://192.168.1.139:4568
180119 11:22:32 [Note] WSREP: Member 0.0 (maria3) requested state transfer from '*any*'. Selected 1.0 (maria2)(SYNCED) as donor.
180119 11:22:32 [Note] WSREP: Shifting PRIMARY -> JOINER (TO: 388973)
180119 11:22:32 [Note] WSREP: Requesting state transfer: success, donor: 1
180119 11:22:33 [Note] WSREP: 1.0 (maria2): State transfer to 0.0 (maria3) complete.
180119 11:22:33 [Note] WSREP: Member 1.0 (maria2) synced with group.
WSREP_SST: [INFO] Joiner cleanup. (20180119 11:22:33.752)
WSREP_SST: [INFO] Joiner cleanup done. (20180119 11:22:34.258)
180119 11:22:34 [Note] WSREP: SST complete, seqno: 388972
```

私の理解だと、State Transferという状態確認をしたあと、データ転送にISTとSSTの2種類があるということでしたので。

[State Transfers — Galera Cluster Documentation](http://galeracluster.com/documentation-webpages/statetransfer.html)

ざっくり言うとデータファイル丸ごとコピーがSST、gcacheを用いた増分コピーがISTです。

## SSTでの同期
さて次は意図的にSSTを起こすため、サービス停止後、データディレクトリを全削除して、サービス起動してみましょう。

```
[root@maria3 ~]# service mysql stop
Shutting down MySQL... SUCCESS! 
[root@maria3 ~]# rm -rf /var/lib/mysql/
[root@maria3 ~]# date;service mysql start
2018年  1月 19日 金曜日 11:33:16 JST
Starting MySQL...SST in progress, setting sleep higher................ SUCCESS! 
```

ログを確認します。

stateを保存しているgrastate.datごとなくなっているので、現在のpositionは以下のように認識します。

```
180119 11:33:19 [Note] WSREP: Service thread queue flushed.
180119 11:33:19 [Note] WSREP: Assign initial position for certification: -1, protocol version: -1
180119 11:33:19 [Note] WSREP: wsrep_sst_grab()
180119 11:33:19 [Note] WSREP: Start replication
180119 11:33:19 [Note] WSREP: Setting initial position to 00000000-0000-0000-0000-000000000000:-1
180119 11:33:19 [Note] WSREP: protonet asio version 0
180119 11:33:19 [Note] WSREP: Using CRC-32C for message checksums.
180119 11:33:19 [Note] WSREP: backend: asio
```

[gcomm](http://galeracluster.com/documentation-webpages/architecture.html#group-communication-plugins)によるお互いの状態確認が終わったあと、State Transferを要求します。

```
180119 11:33:19 [Note] WSREP: State transfer required: 
        Group state: cf816ec9-e382-91e7-a290-768f57ae6c01:388972
        Local state: 00000000-0000-0000-0000-000000000000:-1
```

先ほどと同様にSSTを実施します。今回はstate UUIDが異なるのでISTが使えない旨を示すログが出ていますね。

```
180119 11:33:19 [Warning] WSREP: Gap in state sequence. Need state transfer.
180119 11:33:19 [Note] WSREP: Running: 'wsrep_sst_rsync --role 'joiner' --address '192.168.1.139' --auth '' --datadir '/var/lib/mysql/' --defaults-file '/etc/my.cnf' --parent '5831''
180119 11:33:19 [Note] WSREP: Prepared SST request: rsync|192.168.1.139:4444/rsync_sst
180119 11:33:19 [Note] WSREP: wsrep_notify_cmd is not defined, skipping notification.
180119 11:33:19 [Note] WSREP: REPL Protocols: 7 (3, 2)
180119 11:33:19 [Note] WSREP: Service thread queue flushed.
180119 11:33:19 [Note] WSREP: Assign initial position for certification: 388973, protocol version: 3
180119 11:33:19 [Note] WSREP: Service thread queue flushed.
180119 11:33:19 [Warning] WSREP: Failed to prepare for incremental state transfer: Local state UUID (00000000-0000-0000-0000-000000000000) does not match group state UUID (cf816ec9-e382-91e7-a290-768f57ae6c01): 1 (Operation not permitted)
         at galera/src/replicator_str.cpp:prepare_for_IST():456. IST will be unavailable.
180119 11:33:19 [Note] WSREP: Member 0.0 (maria3) requested state transfer from '*any*'. Selected 1.0 (maria2)(SYNCED) as donor.
180119 11:33:19 [Note] WSREP: Shifting PRIMARY -> JOINER (TO: 388973)
180119 11:33:19 [Note] WSREP: Requesting state transfer: success, donor: 1
180119 11:33:22 [Note] WSREP: (1c364d23, 'tcp://0.0.0.0:4567') turning message relay requesting off
WSREP_SST: [INFO] Joiner cleanup. (20180119 11:35:52.370)
180119 11:35:52 [Note] WSREP: 1.0 (maria2): State transfer to 0.0 (maria3) complete.
180119 11:35:52 [Note] WSREP: Member 1.0 (maria2) synced with group.
WSREP_SST: [INFO] Joiner cleanup done. (20180119 11:35:52.922)
180119 11:35:52 [Note] WSREP: SST complete, seqno: 388973
```

## auditlogからモジュール作成
さて、最初にISTを実施した"2018年  1月 19日 金曜日 11:22:29 JST"以降のaudit.logを採取します。ただし、想定する動作が網羅できているか自信がない場合は日付で絞らないほうがいいかもしれません。

これは念のためJoinerとDonorの両方で行ってください。結果がズラっと出たと思うので、適当なファイルに保存して、そのログをcatで連結して、モジュールを作成します。

```
[root@maria2] # ausearch -i -if /var/log/audit/audit.log -ts 11:22:29 > donor_audit.log
[root@maria3] # ausearch -i -if /var/log/audit/audit.log -ts 11:22:29 > joiner_audit.log
# どっちかに転送
[root@maria3] # cat donor_audit.log donor_audit.log | audit2allow -M galera
******************** IMPORTANT ***********************
To make this policy package active, execute:

semodule -i galera.pp
```

audit2allowがなければinstallしましょう。

```
# yum install policycoreutils-python
```

参考：[CentOS6にSELinuxで便利なコマンドをインストール](https://www.sssg.org/blogs/hiro345/archives/16561.html)

作成したポリシーを見てみます。galera.teというファイルが生成されています。

```
[root@maria3 ~]# cat galera.te 

module galera 1.0;

require {
        type var_run_t;
        type mysqld_safe_t;
        type rsync_exec_t;
        type mysqld_t;
        type kerberos_master_port_t;
        type anon_inodefs_t;
        type proc_net_t;
        type port_t;
        type var_lib_t;
        class process { signull setpgid };
        class sock_file { getattr unlink create };
        class file { execute setattr read create ioctl execute_no_trans write getattr unlink open append };
        class dir { write setattr remove_name create add_name };
        class tcp_socket { name_bind name_connect };
}

#============= mysqld_safe_t ==============
allow mysqld_safe_t mysqld_t:process signull;
#!!!! The source type 'mysqld_safe_t' can write to a 'dir' of the following type:
# cluster_conf_t

allow mysqld_safe_t var_lib_t:dir { write remove_name create add_name setattr };
#!!!! The source type 'mysqld_safe_t' can write to a 'file' of the following types:
# mysqld_var_run_t, mysqld_db_t, mysqld_log_t, root_t, cluster_conf_t, cluster_var_lib_t, cluster_var_run_t

allow mysqld_safe_t var_lib_t:file { setattr read create getattr write ioctl unlink open append };
allow mysqld_safe_t var_lib_t:sock_file { getattr unlink };
#!!!! The source type 'mysqld_safe_t' can write to a 'dir' of the following types:
# var_log_t, mysqld_var_run_t, mysqld_db_t, mysqld_log_t, root_t, cluster_conf_t, cluster_var_lib_t, cluster_var_run_t

allow mysqld_safe_t var_run_t:dir { write remove_name };

#============= mysqld_t ==============
allow mysqld_t anon_inodefs_t:file { write getattr };
allow mysqld_t kerberos_master_port_t:tcp_socket name_bind;

#!!!! This avc can be allowed using the boolean 'mysql_connect_any'
allow mysqld_t kerberos_master_port_t:tcp_socket name_connect;

#!!!! This avc can be allowed using the boolean 'allow_ypbind'
allow mysqld_t port_t:tcp_socket { name_bind name_connect };
allow mysqld_t proc_net_t:file { read getattr open };
allow mysqld_t rsync_exec_t:file { read getattr open execute execute_no_trans };
allow mysqld_t self:process setpgid;
allow mysqld_t var_lib_t:dir setattr;
allow mysqld_t var_lib_t:file { getattr open ioctl append };
allow mysqld_t var_lib_t:sock_file { create unlink getattr };
```

それっぽいのができてますね。なんかいろいろ言われてるけど。

## モジュール適用
モジュール作成時に言われている通り、-iでppファイルをimportします。もちろん、クラスタの全ノードに適用してください。

```
[root@maria2 ~]# semodule -i galera.pp 
[root@maria2 ~]# semodule -l | grep galera
galera  1.0 
```

## 確認
enforcingにした状態で同じ動作をして、実行できるかどうか確認しましょう。

# 本当はどうするべき？
audit.logからの採取の許可は、確かに動くようになりますが、本当に過不足ないポリシーになっているのでしょうか？ポリシーファイルを見るとなんとなくはわかりますが、なんとなくしかわかりません。

できれば公式が配布してくれるとありがたいので、探してみました。

[SELinux Configuration — Galera Cluster Documentation](http://galeracluster.com/documentation-webpages/selinux.html)

先にこっち読めよって感じでしたね。雑にメモると、

* policyをpermissiveに設定
    * mysqldが使うportを定義
    * mysqld_tに対してpermissive設定
* selinux policy定義
    * サービス起動、だが以下のパターンあり
        * SST
        * IST
        * wsrep_notify_cmdによるrestart
* policy有効化
    * mysqldでauditlogをgrepして、audit2allow
    * コンパイル
    * パッケージ
    * 読み込み
    * permissiveの無効化
    
まぁ、だいたい同じ方法でしたね。

# おわりに
今回はSELinuxモジュールをaudit.logから作成しました。公式にある通りプロセスで絞って、運用期間中の全ログから生成するのが良さそうです。

### 参考
* [maiha/GaleraとGRのレプリケーションの違い.md](https://gist.github.com/maiha/b30a3dd45a10b55d0896bee48bbeb5c2) ... 本題と関係ないですがgalera replicationとgroup replicationの違いについて詳しい
* [CentOS7 MariaDB10.1で Galera Cluster (systemd、SELinux対応)](https://qiita.com/egnr-in-6matroom/items/f6af7318e850206bc424) ... 対処を3つのレベル感で説明してくれています
* [CONFIGURING SELINUX FOR GALERA CLUSTER](http://nirbhay.in/selinux-for-galera-cluster/) ... ほぼほぼ公式通りで、mysqld_tをpermissiveにしてaudit.logから生成
* [CentOS 7.x で MariaDB Galera Cluster に参加できない ( SELinux で拒否される )場合の対処方法！](https://blog.neoflow.jp/oss/mariadb-galera-cluster/2472/) ... ポリシーの育て方解説。少ない。OSによって使うコマンドが違えばポリシーも変わってきますね。