第656回ではChromeベースのEPUBリーダーをsnapパッケージ化してみました。今回はsnapパッケージ作成における最大のハードルと言える「strict confinement」の対応を行いましょう。ここまで対応できれば、「snapパッケージ完全に理解した」と言えますし、Snap Storeにも公開できるようになります[1]。
snapパッケージにおけるセキュリティモデル
snapパッケージではアプリケーションを「コンテナ化」することで、ホストシステムから隔離した環境で実行できる仕組みを構築しています。その「コンテナ化」を実現するために、次のような機能を利用しています。
- 古くから存在するディレクトリのパーミッションなどの任意アクセス制御(DAC:Discretionary Access Controls)
- AppArmorを活用した強制アクセス制御(MAC:Mandatory Access Control)
- seccompを利用したシステムコールの利用制限
- cgroupを利用したハードウェアリソースへのアクセス制御
つまりいわゆる「コンテナ管理システム」のようなことを実現しているわけです。これにより個々のsnapパッケージは、簡単にはホストシステムを参照できず、ユーザーはパッケージごとにどのリソースにアクセスするかを柔軟に設定できます。
どれくらい隔離するかは、パッケージ側でもビルド時に設定可能です。それがsnapcraft.yaml
に記述する「confinement(封じ込め)」キーワードです。snapパッケージでは次の3つから選択できます。
-
devmode
-
パッケージの開発時にのみ利用する、まったく隔離せずアクセスログのみを残すモード
-
strict
-
システムからは完全に隔離された状態で、アクセスが必要な場合は個別に許可しなければならないモード
-
classic
-
従来のdebパッケージのように、システムへのアクセスを許容するモード
このうちclassicは特殊なモードです。作ろうとしているsnapパッケージが、たとえばホストシステム上のホームディレクトリーや(/media
にマウントされた)外部ストレージ以外にアクセスしなければならないケース、snapが実装しているリソース管理ではカバーしきれないケースなどに利用されます。classicなsnapパッケージは、snapによる隔離環境の外に存在することになるため、パッケージを公開するためには十分な理由とストアの管理者によるレビュー対応を求められます。
つまりsnapパッケージを作るなら、基本的にdevmodeで作って、その後strictに対応するのが一般的な流れです。
strictなパッケージが、たとえばインターネットに通信したい場合、そのsnapパッケージに「ネットワーク接続への許可」を与える必要があります。このような個別のリソースへのアクセス権限を、snapでは「interface(インターフェース)」と呼んでいます。
ネットワーク接続したいならnetworkインターフェース、カメラデバイスにアクセスしたいならcameraインターフェース、ホームディレクトリにアクセスしたいならhomeインターフェースなどなど、多種多様なインターフェースが存在します。
インターフェースは「Plug(プラグ)」と「Slot(スロット)」に分割されます。プラグはsnapパッケージ側からのリソースアクセス要求であり、スロットはその要求を受けるsnapシステム側のインターフェースです[2]。プラグをスロットに接続することではじめてコネクションが確立し、snapパッケージはリソースを利用できるようになります。
言い換えるとsnapシステムに存在しないスロットに対するプラグは作れません。もしスロットが存在しないリソースを使いたいなら、新規にスロットの作成を提案するか、classicパッケージとして作るかの二択になります。
「strictに対応する」ということは、そのsnapパッケージが必要とするリソースを洗い出し、実装済みのインターフェースの中から使用するスロットをリストアップする作業なのです。
Thorium Readerパッケージをstrictモードに変更する
第656回に引き続き「Thorium Reader」のパッケージングを例に具体的な手順を説明します。
前回までの設定でdevmodeでなら動作することを確認しました。その時点でのコードはGitLabにアップロードしてあります。今回はここから進めましょう。
strictモードへの対応は大まかに次の2つの手法が存在します。
- devmodeのまま動かしてシステムログから必要なリソースアクセスを洗い出す
- strictに変更して何が動かないかを確認する
strictモードでブロックされる挙動は、devmodeでは許可されるものの各種セキュリティフレームワークのログに残るようになっています。よってそのログから何を許可すべきかを洗い出すのが最初の方法です。簡単に言うとまず次のコマンドを実行します。
その状態でThorium Readerを起動すると、たくさんのログが表示されるはずです。
特に「audit」と表示される行がポイントです。AppArmor等によって「特別に許可された」操作がログに残るようになっているので、これらがstrictモードでも許可され続けるようにインターフェースなどの設定を行う必要があります。
ただしシンプルなツールならこの方法でもなんとかなるのですが、GUIアプリになってくると大量のメッセージが流れてしまい、あまり現実的ではありません。よって「とりあえずstrictモードにして、動かない部分を対応していく」方法をおすすめします。
ちなみに今回のパッケージは、GTKに対応したGUIアプリケーションとして作るためにSnapcraft Extensionsを利用しています。その結果、GUIアプリケーションを動かすために必要な権限は、おおよそExtensions側で用意してくれています。GUIアプリケーションをsnapパッケージ化するのであれば、Extensionsは積極的に活用していきましょう。
strictモードの有効化
まずはsnap/snapcraft.yaml
からstrictモードを有効化しましょう。
confinement
をdevmode
からstrict
に変えるだけです。第656回の手順に従って、snapパッケージをビルド&インストールします。
前回と異なるのはインストール時に「--devmode
」を付ける必要がなくなったということです。
それでは実際に起動してみましょう。GUIシェルから起動すると、起動に時間がかかっているのかエラー終了してしまったのかがわかりにくいため、最初のうちは端末から直直接コマンドを実行すると良いでしょう。
あっさりエラー終了してしまいました。
SUID sandboxに対応する
これは何のエラーかというとChromiumのLinux SUID Sandbox機能です。ThoriumはバックエンドにChromiumを利用しています。Chromiumにはセキュアなウェブブラウジングを実現するための機能の一つとして、setuid()
等を利用したLinux SUID Sandbox機能が存在します。
しかしながらsnap環境の中ではいくつかのシステムコールが制限されているため、この機能は使えません。そもそもの話としてChromium側がこの機能を廃止予定なのと、snap環境の中であればsnapシステム側でセキュリティを担保できるため、機能自体を無効化するのが良さそうです。
通常snapパッケージの中で使われるChromiumバイナリはこの機能を切ってビルドすることになります。今回はビルド済みのdebパッケージのバイナリをそのままもってきているため、ランタイムに機能を無効化する必要があります。それが「--no-sandbox
」オプションです。
試しにオプションを付けて起動してみましょう。
別のところでエラー終了したものの、元のエラーメッセージは出なくなったため、効果はあったようです。新しいエラーは次で対応することにして、先に「--no-sandbox
」を常に付加するようsnap/snapcraft.yaml
を変更します。
変更するのは2箇所です。まず端末から実行するコマンドでも必ずオプションが付くように、次のように変更します。
command
フィールドが変更された箇所です。これにより端末から「thorium-reader-snap.thorium
」を実行したら、暗黙で「--no-sandbox
」が付加されます。
もうひとつがデスクトップファイル側の対応です。GUIシェルから検索して起動する場合、thorium.desktop
のようなデスクトップファイルのExec
フィールドが参照されます。ここのフィールドはthorium-reader-snap.thorium
ではなく、snapパッケージ内部のコマンド名が直接記述されていることが多いため、ここにも個別にオプションを付けなくてはならないのです。
方法はいくつかあるものの、一番手っ取り早いのがデスクトップファイルの中身を書き換えてしまうことです。具体的には次のように記述します。
override-build
フィールドの末尾に2行のコマンドを追加しています。やっていることはsed
で置き換えている、それだけですね。
あとは先ほどと同じようにビルド&インストールすれば、SUID sandboxのエラーは出力されなくなっているはずです。
エラーログから要因を探る
新しいエラーメッセージは次の内容でした。
「canberra」はシステムサウンドを鳴らすためのモジュールであり、このタイミングで表示されたとしてもエラー終了に直接影響するものではありません。つまり別の理由があるわけです。そこで「sudo journalctl --since=now -f | grep thorium
」を実行してから、別の端末で起動してみましょう。
たくさんセキュリティ関係のメッセージが表示されるはずです。そのまま貼り付けるには紙幅が足りないので、ここでは必要な情報だけを抽出します。
seccompはsyscall番号でログを表示します。syscall番号と実際の関数の対応を調べる方法はいろいろありますが、今回はseccompパッケージに付属しているscmp_sys_resolver
コマンドを使ってみましょう。
どうやらソケットを作るのとスケジューリングの設定関連の何かが呼ばれているようです[3]。AppArmor側で拒否されているのは、Bluetoothへの通信と名前解決関連の設定ファイルの読み込み、それに共有メモリのmknod
ですね。
Bluetooth関連はPCによって実装されているかまちまちであるため、必須の機能ではないはずです。よってこれは後回ししても良さそうです。
ネットワーク通信関連はnetworkインターフェースを利用することで許可できます。snapパッケージは何も設定しないとネットワーク通信の権限がありません。networkインターフェースを追加することではじめて、接続できるようになるのです。その他のインターフェースは「サポートされているインターフェースリスト」を参照してください。
試しにインターフェースを追加してみましょう。前述したようにsnapパッケージ側のインターフェースは「プラグ」と呼ばれています。よって次のように記述します。
「このsnapパッケージはnetworkプラグを利用します」と宣言しているわけです。あとはインストール先のsnapシステムが許可すればネットワーク通信を利用できます。ビルド&インストールしたら次のように確認してみましょう。
networkプラグがスロットに追加されていますね。ちなみに他のインターフェースはgnomeエクステンションが自動的に追加したものです。
インターフェースは「自動的に接続されるもの」と「ユーザーが明示的に許可してはじめて接続されるもの」の2種類が存在します。networkは前者で、cameraなどは後者です。
手動で接続許可するものは「Ubuntu Software」の個別のアプリケーションのページにある「Permission」から設定可能です。またCLIからだと次のように実施します。
networkが繋がった状態でThoriumを起動し直してみましょう。引き続き起動しない状態ではあるものの、ログを見るとネットワーク関連のメッセージがなくなっていることがわかります。方向性は間違っていないようです。
browser-supportの有効化
残っているのはBluetoothとmknod
です。Bluetoothはbluezインターフェースが該当しそうですが、後者がわかりませんね。こういうときは検索してみましょう。おすすめはGoogleでforum.snapcraft.ioを検索する方法です。今回の例だと「mknod /dev/shm site:forum.snapcraft.io」となります。
検索してみると「そのものずばりなやりとりが見つかります」。いわく「browser-supprotインターフェースを使いましょう」とのことです。このインターフェースはChromiumやFirefoxなどモダンなウェブブラウザーをsnapパッケージ化するにあたって必要な機能を一通りサポートするためのインターフェースのようです。今回のThoriumはバックエンドにChromiumを使っているのでちょうど良さそうですね。ちなみにbrowser-supportインターフェースのページには前述の「--no-sandbox
」に関する話も掲載されています。
bluezとbrowser-supportを有効化しましょう。
インストールしたあとはまずインターフェースの状態を確認してみます。
期待通りに設定が行われていますね。bluezのみ接続されていません。必要なら次の方法で接続してください。
もう一度Thorium Readerを起動してみましょう。今度は無事に起動できるようになっているはずです。
残りのログを確認する
無事に起動するようになったので、最低限のstrictモード対応は完了となります。このあとは個別に機能を使ってみながら、動かない機能に対して対応していくフェーズとなります。
まずは普通に動くようになった状態で、もう一度起動ログを見直してみましょう。
後半のメッセージはElectron側の通知メッセージなので、気にしなくてもかまいません。必要ならThorium Reader側で対応することになるでしょう。最初のメッセージは、AMD GPUを動かしている環境ではlibdrmが/usr/share/libdrm/amdgpu.ids
を読み込もうとします。snapパッケージの中にはこのファイルがないため、エラーメッセージが出ています。
実際のところ読めなくても実害はないものの、気になるようならlibdrm-commonパッケージをstage-packages
に追加しておくと良いでしょう。
layoutはsnapパッケージ内部のファイルレイアウトのマッピングをコントロールする設定です。今回のThorium Readerのようにビルド済みバイナリを使う場合、バイナリの中で「/usr/share/libdrm
」のようにフルパスで記述されたディレクトリパスは「ホストのパス」を参照しようとします。当然のことながら、strictモードではこれは許容されません。そこで「/usr/share/libdrm
」をsnapパッケージの特定の場所に向けるための仕組みがこのlayoutなのです。
layoutは大変強力な機能なので、「そういうものがある」と頭の片隅に残しておくことをおすすめします。
もうひとつ確認すべきなのがjorunalctl側のログです。普通に動くようになったため、前回終了していたところ以降のメッセージも表示されるようになりました。おおよそ次の3種類で弾かれているようです。
sched_setaffinity()
sched_setattr()
org.freedesktop.DBus.ListNames
最初の2つはChromiumによるプロセス優先度管理のために必要な機能です。実はChromiumでこの機能を使うためには、browser-supportでallow-sandboxをtrueにする必要があります。しかしながらallow-sandboxをtrueにすると、Snap Storeでパッケージを公開するさいにマニュアルレビューが必要になります。つまり管理者に明示的な許可を得なくてはなりません。
今回はそこまですることのものでもないため、この件については対応しないことにしましょう。
後者については、system-observeインターフェースで実現可能です。ただしこれも必要性については微妙なところなので、plugsには記載するものの、自動接続は考えなくて良いでしょう。
ホームディレクトリを読み書きできるようにする
snapパッケージはそのまままだとsnapパッケージディレクトリ(/snap/パッケージ名/
)やランタイムディレクトリ(/var/snap/パッケージ名/
)、各ユーザーのsnapディレクトリ($HOME/snap/パッケージ名/
)の中しか読み書きできません。
今回のように「別途取得したEPUBファイルを取り込む」場合、ウィンドウにドラッグアンドドロップするだけでなく、ファイルブラウザーからホームディレクトリにあるファイルを選択できたほうが便利です。
これを実現できるのがhomeインターフェースとなります。ちなみにhomeインターフェースは隠しディレクトリは参照できません。設定によって参照できるようになるものの、その場合は自動接続の対象外となります。
システムディレクトリを参照できるもう一つのインターフェースがremovable-mediaインターフェースです。こちらはより大容量のデータ置き場を使用したいsnapパッケージに向いているインターフェースとなります。
今回設定したプラグをまとめると次のような結果になります。
これでstrictモードでも、Thorium Readerの最低限必要な機能が動くことは確認できました。今回は明示的には書いていませんが、strictモードにしたあとは改めて「日本語入力できるか」「URLをクリックしてウェブブラウザーが起動するか」なども確認しておくと良いでしょう。
ここまでのsnap/snapcraft.yaml
の全体像はこちらのリポジトリから参照できます。
次回はSnap Storeに公開する話です。