SE-PostgreSQLでリモートプロセスのセキュリティラベルを使用する

PostgreSQL Advent Calendar 2012に参加しています。

何を設定する必要があるのか?

何か世のため人のためになりそうなネタは・・・という事で、SE-PostgreSQLでリモートプロセスのセキュリティラベルを使用する方法をまとめてみました。

v9.1からサポートされたSE-PostgreSQLは、OSの機能であるSELinuxと連携して、SQLを介したDBオブジェクトへのアクセスに対して強制アクセス制御を行う機能です。
かいつまんで言えば、○○のテーブルは機密度の高い情報を持っている、□□は機密度が低いと言った形で、PostgreSQL標準のDBアクセス制御機能に加えて、OSのセキュリティポリシーに従ってアクセス制御を行います。そのため、同じ”情報”であるにも関わらず、保存先がファイルかデータベースかといった”格納手段”によって異なるポリシーでアクセス制御が行われる事を避ける事ができます。
SE-PostgreSQLが追加のアクセス制御を行う際には、『誰が(subject)』『何に(object)』『何をする(action)』のかをひとまとめにして、OSの一部であるSELinuxに伺いを立てるのですが、この時に『誰が』『何に』を識別するために、セキュリティコンテキストと呼ばれる特別な識別子が使用されます。
セキュリティコンテキストというのはこんな感じです。ファイルのセキュリティコンテキストを確認するには ls -Z を、プロセスの場合は ps -Z や id -Z を実行して確認できます。

[kaigai@iwashi ~]$ ls -Z
drwxr-xr-x. kaigai users unconfined_u:object_r:user_home_t:s0 patch/
drwxr-xr-x. kaigai users unconfined_u:object_r:user_home_t:s0 repo/
drwxr-xr-x. kaigai users unconfined_u:object_r:user_home_t:s0 rpmbuild/
drwxr-xr-x. kaigai users unconfined_u:object_r:user_home_t:s0 source/
drwxr-xr-x. kaigai users unconfined_u:object_r:user_home_t:s0 tmp/
[kaigai@iwashi ~]$ ps -Z
LABEL                             PID TTY          TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 28791 pts/4 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 29093 pts/4 00:00:00 ps

DBオブジェクトの場合はSECURITY LABEL構文を用いて設定できるほか、システムビューのpg_seclabelsを用いて参照する事ができます。

postgres=# SELECT provider,label FROM pg_seclabels
              WHERE objtype = 'table' AND objname = 't1';
 provider |                  label
----------+------------------------------------------
 selinux  | unconfined_u:object_r:sepgsql_table_t:s0
(1 row)

そしてDB利用者の場合。やっと今回の記事の主題にたどり着けたワケですが、SE-PostgreSQLは『接続元プロセスのセキュリティコンテキスト』をDB利用者のセキュリティコンテキスト(= DB利用者の権限)として扱います。

SELinuxgetpeercon(3)というAPIを持っており、この人はソケットのファイルディスクリプタを引数として与えると、接続相手のセキュリティコンテキストを返します。SE-PostgreSQLはこのAPIを使用して『接続元プロセスのセキュリティコンテキスト』を取得しています。
で、UNIXドメインソケットによるローカル接続の場合は特別な設定は何も必要ないのですが、TCP/IPによるリモート接続の場合、OSは相手側プロセスの情報を持っていないため、ネットワーク接続の確立に先立って双方のセキュリティコンテキストを交換する Labeled Network の設定を行う必要があります。
この機能、Linux kernel 2.6.16 からサポートされた機能で、IPsecの鍵交換デーモンである racoon と連携してネットワーク接続先とセキュリティコンテキストを交換します。つまり、IPsecで通信経路が保護されている(少なくとも、改ざん検知できる)事が前提です。IPsecが前提ですよ。大切な事なので2回書きました。

Linux kernelのバージョンからも分かるようにそれほど新しい機能ではないのですが、意外と設定手順がドキュメント化されていませんので、少しまとめてみました。

テスト環境の構成

IPsecの設定を極める事が目的ではありませんので、今回は、最も単純なHost-to-HostのPSK(Pre-Shared Key)の構成でIPsec通信経路を作成します。その上で、Labeled Networkの設定を追加する事にします。


今回のテスト環境では、2台のホストをそれぞれサーバとクライアントに見立てて設定を行いました。kg1(192.168.1.42)がクライアント、kg2(192.168.1.43)がサーバであると想定し、この2台の間にIPsecの接続を設定します。ディストリビューションFedora 17 を使用し、Development Workstation構成でクリーンインストールしました。

なお、IPsec接続の設定に関してはFedora Projectのドキュメントを参考にしています。
詳しくはIPsec Host-to-Host Configurationを参照してください。

事前の設定

パッケージの追加

インストール直後の状態では ipsec-tools と system-config-network パッケージが導入されていませんので、両ホストでこれらのパッケージをインストールします。

# yum install -y ipsec-tools system-config-network

また、サーバ側には postgresql-server と postgresql-contrib パッケージが、クライアント側には postgresql パッケージが必要です。

# yum install -y postgresql-server postgresql-contrib
# yum install -y postgresql
ファイアウォールの設定

IPsecの鍵交換デーモンの通信とPostgreSQLサーバへの接続にはファイアウォールの設定変更が必要です。
system-config-firewallを実行して関連するポートを解放してください。

IPsecの鍵交換デーモン用ポートは、サーバ側/クライアント側の双方で解放が必要です。


PostgreSQLサーバ用ポートは、サーバ側で解放が必要です。

これらの設定を保存し、有効化してください。

NetworkManagerの無効化

ちょっとダサいのですが、NetworkManager経由でうまくipsec0デバイスを自動起動させる事ができなかったので、レガシーなnetworkスクリプトによってネットワークデバイスをup/downさせる事にします。

# chkconfig NetworkManager off
# chkconfig network on

Host-to-Host IPsec デバイスの作成

今回は、system-config-networkを使ってipsec0デバイスの定義ファイルや、racoonの設定ファイルを自動生成させる方法でHost-to-HostのIPsecデバイスを作成します。
まず、system-config-networkを実行して設定GUIを起動します。

起動画面です。『IPsec』のタブを選択します。


左上の『New』をクリックすると、ウィザード形式でIPsec設定を追加することができます。


デバイス名を入力します。ここでは『ipsec0』としました。


今回は『Host-to-Host encryption』を選択します。


『Automatic encryption mode selection via IKA (racoon)』を選択します。


この設定での接続相手先のIPアドレスを入力します。画面は192.168.1.42ホストのものなので、接続先は『192.168.1.43』となります。


事前共有型の認証キーを入力します。ここでは『sepgsql920』としました。


入力したパラメータを確認して『Apply』をクリックします。


これで『ipsec0』デバイスが作成されているのが分かります。


設定を保存すると、右上の『Activate』『Deactivate』を選択することができるようになりますので、『Activate』をクリックしてください。

もう一方のノードでも同様の設定を行います。

IPsecの動作確認

ひとまず、SELinux関連の設定が絡まない範囲でHost-to-Host設定のIPsecがちゃんと動作しているかを確認します。双方のノードでネットワークを再起動します。

# service network restart

tcpdumpでパケットを観察すると、IPsecのAH/ESPプロトコルが使われている事が分かります。

[root@kg2 ~]# tcpdump -n -i eth1 host 192.168.1.42
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes
16:55:34.979277 IP 192.168.1.42 > 192.168.1.43: \
    AH(spi=0x03850f37,seq=0x10): ESP(spi=0x0f3c888b,seq=0x10), length 76
16:55:34.979505 IP 192.168.1.43 > 192.168.1.42: \
    AH(spi=0x0ebad42b,seq=0x6): ESP(spi=0x0183d073,seq=0x6), length 76
16:55:34.980000 IP 192.168.1.42 > 192.168.1.43: \
    AH(spi=0x03850f37,seq=0x11): ESP(spi=0x0f3c888b,seq=0x11), length 68
    :
    :

Labeled Network の設定

ipsec0デバイスを起動する時に、どの相手との接続にIPsecを使うのか、その際のアルゴリズムは何なのか…という事がOS側に伝えられます。これを行うのがsetkeyコマンドなのですが、通常、これはifcfg-ipsec0の記述に基づいて(ここにスクリプトファイル名)が自動的に実行されます。
実際には以下のような書式をsetkeyコマンドに与えるのですが、Labeled Networkを使用するにあたって重要なのは、2行目の-ctxから始まる行です。これを指定する事で、このIPsec通信経路ではLabeled Networkを使用するという事を宣言できます。

spdadd 192.168.11.6 192.168.11.8 any
-ctx 1 1 "system_u:object_r:ipsec_spd_t:s0"
-P out ipsec esp/transport//require;

が、FedoraのInitスクリプトから実行される/etc/sysconfig/network-scripts/ifup-ipsecは、この辺の設定を行うための処理が入っていません。そこで、パッチを当てる事にしました(うげげ…)。

--- ifup-ipsec.orig	2012-12-03 17:41:38.809689669 +0100
+++ ifup-ipsec	2012-12-04 17:52:43.356150231 +0100
@@ -123,6 +123,8 @@ if [ "$ESP_PROTO" = "none" ]; then
     unset SPI_ESP_IN SPI_ESP_OUT KEY_ESP_IN KEY_ESP_OUT SPD_ESP_IN SPD_ESP_OUT
 fi
 
+test -n "$SELINUX_CONTEXT" && SELINUX_CONTEXT="\"${SELINUX_CONTEXT}\""
+
 /sbin/setkey -c >/dev/null 2>&1 << EOF
 ${SPI_AH_OUT:+delete $SRC $DST ah $SPI_AH_OUT;}
 ${SPI_AH_IN:+delete $DST $SRC ah $SPI_AH_IN;}
@@ -164,33 +166,45 @@ EOF
 # This looks weird but if you use both ESP and AH you need to configure them together, not seperately.
 if [ "$ESP_PROTO" != "none" ] && [ "$AH_PROTO" != "none" ]; then
 /sbin/setkey -c >/dev/null 2>&1 << EOF
-spdadd $SPD_SRC $SPD_DST any -P out ipsec
+spdadd $SPD_SRC $SPD_DST any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P out ipsec
             ${SPD_ESP_OUT:+esp/$MODE/${TUNNEL_MODE:+$SRC-$DST}/require}
             ${SPD_AH_OUT:+ah/$MODE/${TUNNEL_MODE:+$SRC-$DST}/require}
 	    ;
 
-spdadd $SPD_DST $SPD_SRC any -P in ipsec
+spdadd $SPD_DST $SPD_SRC any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P in ipsec
 	    ${SPD_ESP_IN:+esp/$MODE/${TUNNEL_MODE:+$DST-$SRC}/require}
 	    ${SPD_AH_IN:+ah/$MODE/${TUNNEL_MODE:+$DST-$SRC}/require}
 	    ;
 EOF
 elif [ "$AH_PROTO" = "none" ]; then
 /sbin/setkey -c >/dev/null 2>&1 << EOF
-spdadd $SPD_SRC $SPD_DST any -P out ipsec
+spdadd $SPD_SRC $SPD_DST any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P out ipsec
             ${SPD_ESP_OUT:+esp/$MODE/${TUNNEL_MODE:+$SRC-$DST}/require}
 	    ;
 
-spdadd $SPD_DST $SPD_SRC any -P in ipsec
+spdadd $SPD_DST $SPD_SRC any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P in ipsec
 	    ${SPD_ESP_IN:+esp/$MODE/${TUNNEL_MODE:+$DST-$SRC}/require}
 	    ;
 EOF
 elif [ "$ESP_PROTO" = "none" ]; then
 /sbin/setkey -c >/dev/null 2>&1 << EOF
-spdadd $SPD_SRC $SPD_DST any -P out ipsec
+spdadd $SPD_SRC $SPD_DST any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P out ipsec
             ${SPD_AH_OUT:+ah/$MODE/${TUNNEL_MODE:+$SRC-$DST}/require}
 	    ;
 
-spdadd $SPD_DST $SPD_SRC any -P in ipsec
+spdadd $SPD_DST $SPD_SRC any
+            ${SELINUX_CONTEXT:+-ctx 1 1 ${SELINUX_CONTEXT}}
+            -P in ipsec
 	    ${SPD_AH_IN:+ah/$MODE/${TUNNEL_MODE:+$DST-$SRC}/require}
 	    ;
 EOF

この修正により、TYPE=IPSEC として定義されている /etc/sysconfig/network-scripts/ifcfg-* 設定ファイルに「SELINUX_CONTEXT=...」という行が存在すれば、IPsec通信経路を定義する際に、Labeled Networkの定義も同時に行われる事となります。
ipsec_spd_t』というのは、IPsec通信経路に対して定義されているタイプです。現時点ではこれ以外のタイプは定義されていませんので、”お約束”という事にしておきます。

# cat /etc/sysconfig/network-scripts/ifcfg-ipsec0
DST=192.168.1.43
TYPE=IPSEC
ONBOOT=yes
IKE_METHOD=PSK
# KaiGai added them
SELINUX_CONTEXT=system_u:object_r:ipsec_spd_t:s0

うまく設定できていれば、setkey -DPの出力結果の中に『security context: ...』がどーたらという行が出力されているハズです。

# setkey -DP
    :
192.168.1.42[any] 192.168.1.43[any] 255
        out prio def ipsec
        esp/transport//require
        ah/transport//require
        created: Dec  4 16:45:31 2012  lastused: Dec  5 21:26:24 2012
        lifetime: 0(s) validtime: 0(s)
        security context doi: 1
        security context algorithm: 1
        security context length: 33
        security context: system_u:object_r:ipsec_spd_t:s0
        spid=1 seq=0 pid=3500
        refcnt=2

SE-PostgreSQLのインストール

続いて、SE-PostgreSQLのインストールに移ります。
インストール手順は公式ドキュメントにも記載されていますので、Fedoraのパッケージを前提にざっくりと紹介します。

まず、initdbを実行します。

# postgresql-setup initdb
Initializing database ... OK

次に、postgresql.conf を編集して shared_preload_libraries に '$libdir/sepgsql' を追加します。

# vi /var/lib/pgsql/data/postgresql.conf
    :
    :
#max_files_per_process = 1000           # min 25
                                        # (change requires restart)
shared_preload_libraries = '$libdir/sepgsql'

# - Cost-Based Vacuum Delay -

PostgreSQLをシングルユーザモードで起動し、各データベースに初期セキュリティラベルを割り当てます。

# su - postgres
$ for dbname in template0 template1 postgres
  do
    /usr/bin/postgres --single $dbname < \
      /usr/share/pgsql/contrib/sepgsql.sql > /dev/null
  done

これでSE-PostgreSQLの初期設定は完了です。

最後に、TCP/IP経由での接続を受け付けるための設定を行ってサーバを起動します。

# vi /var/lib/pgsql/data/pg_hba.conf
(以下の一行を追加; 設定簡略化のため、とりあえずtrust)
host    all    all    192.168.1.42/32    trust
vi /usr/lib/systemd/system/postgresql.service
(以下の一行を編集し、pg_ctlから-iオプションを渡すようにする)
ExecStart=/usr/bin/pg_ctl start -D ${PGDATA} -s -o "-p ${PGPORT}" -w -t 300
  ↓
ExecStart=/usr/bin/pg_ctl start -D ${PGDATA} -s -o "-i -p ${PGPORT}" -w -t 300

サーバを立ち上げます。

# service postgresql start

試してみる

長かった・・・。

そして、クライアント側からTCP/IP接続でPostgreSQLサーバに接続してみます。

[kaigai@kg1 ~]$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

[kaigai@kg1 ~]$ psql -h 192.168.1.43 -U postgres postgres
psql (9.1.6)
Type "help" for help.

postgres=# SELECT sepgsql_getcon();
                    sepgsql_getcon
-------------------------------------------------------
 unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
(1 row)

ふむふむ、ちゃんとセキュリティコンテキストが切り替わっているように見えます。

それでは、クライアント側で権限を切り替えてからPostgreSQLに接続してみましょう。

[kaigai@kg1 ~]$ runcon -l s0-s0:c0,c2 bash
[kaigai@kg1 ~]$ id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0,c2
[kaigai@kg1 ~]$ psql -h 192.168.1.43 -U postgres postgres
psql (9.1.6)
Type "help" for help.

postgres=# SELECT sepgsql_getcon();
                   sepgsql_getcon
----------------------------------------------------
 unconfined_u:unconfined_r:unconfined_t:s0-s0:c0,c2
(1 row)

キタコレ!

クライアント側のOS上で変更したプロセスのセキュリティコンテキストが、PostgreSQL側に透過的に伝えられている事が分かります。

なお、以下のようにgetpeercon(3)に失敗している場合は、TCP/IPレベルでの接続には成功したものの、Labeled Networkが機能していません。設定を見直してみてください。

$ psql -h 192.168.0.43 -U postgres postgres
psql: FATAL:  SELinux: unable to get peer label: Protocol not available

まとめ

今回紹介したLabeled Networkの機能を使う事で、SE-PostgreSQLが実現する”OSとDBのアクセス制御の統合”を複数ノードから成る環境にも拡大する事ができるようになりました。
ただ、これは不特定多数からの接続に対して、接続相手のセキュリティコンテキストを受け入れるという設定ではなく、あくまでも、信頼済みのホストが提示するセキュリティコンテキストを受け入れるという形になります。したがって、”誰か分からないけど世界中からアクセスが来る”という場合に使える方法ではなく、自サイトで管理下にあるWebサーバやAPサーバからPostgreSQLサーバに接続する際にセキュリティコンテキストを伝達するための手法という事になります。

現時点では、OS標準のスクリプトがLabeled Networkに対応しておらずスクリプトを修正する必要があったり、初回接続時に(racoonの鍵交換に時間がかかって?)タイムアウトになるといった現象が見られました。
この辺は改良の余地ありなので、引き続き調査してみようと思います。