Ubuntu Weekly Recipe

第658回自作のsnapパッケージをコンテナ化する

第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にアップロードしてあります。今回はここから進めましょう。

$ git clone https://gitlab.com/mtyshibata/thorium-reader-snap.git
$ cd thorium-reader-snap
$ git checkout -b v0.1 v0.1

strictモードへの対応は大まかに次の2つの手法が存在します。

  1. devmodeのまま動かしてシステムログから必要なリソースアクセスを洗い出す
  2. strictに変更して何が動かないかを確認する

strictモードでブロックされる挙動は、devmodeでは許可されるものの各種セキュリティフレームワークのログに残るようになっています。よってそのログから何を許可すべきかを洗い出すのが最初の方法です。簡単に言うとまず次のコマンドを実行します。

$ sudo journalctl --since=now -f | grep thorium

その状態でThorium Readerを起動すると、たくさんのログが表示されるはずです。

 3月 13 16:55:45 nuc systemd[2904]: Started snap.thorium-reader-snap.thorium.940ea09d-aa26-4bf3-92d5-b7abb9eaa924.scope.
 3月 13 16:55:45 nuc systemd[2904]: app-gnome-thorium\x2dreader\x2dsnap_thorium-86679.scope: Succeeded.
 3月 13 16:55:45 nuc audit[86760]: AVC apparmor="ALLOWED" operation="open" profile="snap.thorium-reader-snap.thorium"
(以下略)

特に「audit」と表示される行がポイントです。AppArmor等によって「特別に許可された」操作がログに残るようになっているので、これらがstrictモードでも許可され続けるようにインターフェースなどの設定を行う必要があります。

ただしシンプルなツールならこの方法でもなんとかなるのですが、GUIアプリになってくると大量のメッセージが流れてしまい、あまり現実的ではありません。よって「とりあえずstrictモードにして、動かない部分を対応していく」方法をおすすめします。

ちなみに今回のパッケージは、GTKに対応したGUIアプリケーションとして作るためにSnapcraft Extensionsを利用しています。その結果、GUIアプリケーションを動かすために必要な権限は、おおよそExtensions側で用意してくれています。GUIアプリケーションをsnapパッケージ化するのであれば、Extensionsは積極的に活用していきましょう。

strictモードの有効化

まずはsnap/snapcraft.yamlからstrictモードを有効化しましょう。

grade: devel
confinement: strict

confinementdevmodeからstrictに変えるだけです。第656回の手順に従って、snapパッケージをビルド&インストールします。

$ snapcraft --debug --enable-experimental-extensions
$ sudo snap install thorium-reader-snap_0.1_amd64.snap --dangerous
thorium-reader-snap 0.1 installed

前回と異なるのはインストール時に--devmodeを付ける必要がなくなったということです。

それでは実際に起動してみましょう。GUIシェルから起動すると、起動に時間がかかっているのかエラー終了してしまったのかがわかりにくいため、最初のうちは端末から直直接コマンドを実行すると良いでしょう。

$ thorium-reader-snap.thorium
[126296:0313/173222.838179:FATAL:setuid_sandbox_host.cc(158)] The SUID
  sandbox helper binary was found, but is not configured correctly.
  Rather than run without sandboxing I'm aborting now. You need to make
  sure that /snap/thorium-reader-snap/x5/opt/Thorium/chrome-sandbox is
  owned by root and has mode 4755.
Trace/breakpoint trap (コアダンプ)

あっさりエラー終了してしまいました。

SUID sandboxに対応する

これは何のエラーかというとChromiumのLinux SUID Sandbox機能です。ThoriumはバックエンドにChromiumを利用しています。Chromiumにはセキュアなウェブブラウジングを実現するための機能の一つとして、setuid()等を利用したLinux SUID Sandbox機能が存在します。

しかしながらsnap環境の中ではいくつかのシステムコールが制限されているため、この機能は使えません。そもそもの話としてChromium側がこの機能を廃止予定なのと、snap環境の中であればsnapシステム側でセキュリティを担保できるため、機能自体を無効化するのが良さそうです。

通常snapパッケージの中で使われるChromiumバイナリはこの機能を切ってビルドすることになります。今回はビルド済みのdebパッケージのバイナリをそのままもってきているため、ランタイムに機能を無効化する必要があります。それが--no-sandboxオプションです。

試しにオプションを付けて起動してみましょう。

$ thorium-reader-snap.thorium --no-sandbox
Gtk-Message: 17:51:53.911: Failed to load module "canberra-gtk-module"
Gtk-Message: 17:51:53.912: Failed to load module "canberra-gtk-module"
中止 (コアダンプ)

別のところでエラー終了したものの、元のエラーメッセージは出なくなったため、効果はあったようです。新しいエラーは次で対応することにして、先に--no-sandboxを常に付加するようsnap/snapcraft.yamlを変更します。

変更するのは2箇所です。まず端末から実行するコマンドでも必ずオプションが付くように、次のように変更します。

apps:
  thorium:
    command: opt/Thorium/thorium --no-sandbox
    extensions: [gnome-3-38]
    desktop: usr/share/applications/thorium.desktop

commandフィールドが変更された箇所です。これにより端末からthorium-reader-snap.thoriumを実行したら、暗黙で--no-sandboxが付加されます。

もうひとつがデスクトップファイル側の対応です。GUIシェルから検索して起動する場合、thorium.desktopのようなデスクトップファイルのExecフィールドが参照されます。ここのフィールドはthorium-reader-snap.thoriumではなく、snapパッケージ内部のコマンド名が直接記述されていることが多いため、ここにも個別にオプションを付けなくてはならないのです。

方法はいくつかあるものの、一番手っ取り早いのがデスクトップファイルの中身を書き換えてしまうことです。具体的には次のように記述します。

parts:
  thorium-reader:
    source: https://github.com/edrlab/thorium-reader/releases/download/v1.6.0/EDRLab.ThoriumReader_1.6.0_amd64.deb
    source-type: deb
    plugin: dump
    override-build: |
      snapcraftctl build
      sed -i 's|Icon=.*|Icon=/usr/share/icons/hicolor/0x0/apps/thorium.png|g' \
        $SNAPCRAFT_PART_INSTALL/usr/share/applications/thorium.desktop
      sed -i 's|/opt/Thorium/thorium|/opt/Thorium/thorium --no-sandbox|g' \
        $SNAPCRAFT_PART_INSTALL/usr/share/applications/thorium.desktop
    stage-packages:
      - libnss3

override-buildフィールドの末尾に2行のコマンドを追加しています。やっていることはsedで置き換えている、それだけですね。

あとは先ほどと同じようにビルド&インストールすれば、SUID sandboxのエラーは出力されなくなっているはずです。

エラーログから要因を探る

新しいエラーメッセージは次の内容でした。

$ thorium-reader-snap.thorium --no-sandbox
Gtk-Message: 17:51:53.911: Failed to load module "canberra-gtk-module"
Gtk-Message: 17:51:53.912: Failed to load module "canberra-gtk-module"
中止 (コアダンプ)

「canberra」はシステムサウンドを鳴らすためのモジュールであり、このタイミングで表示されたとしてもエラー終了に直接影響するものではありません。つまり別の理由があるわけです。そこでsudo journalctl --since=now -f | grep thoriumを実行してから、別の端末で起動してみましょう。

たくさんセキュリティ関係のメッセージが表示されるはずです。そのまま貼り付けるには紙幅が足りないので、ここでは必要な情報だけを抽出します。

SECCOMP syscall=41 compat=0 ip=0x7f4b81ed490b code=0x50000

apparmor="DENIED" operation="dbus_method_call" name="org.bluez"

SECCOMP syscall=314 compat=0 ip=0x7f4b81ecc89d code=0x50000

apparmor="DENIED" operation="open" name="/etc/hosts" comm="ThreadPoolForeg" requested_mask="r" denied_mask="r"                                apparmor="DENIED" operation="open" name="/etc/host.conf" comm="ThreadPoolForeg" requested_mask="r" denied_mask="r"                            apparmor="DENIED" operation="open" name="/run/systemd/resolve/stub-resolv.conf" comm="ThreadPoolForeg" requested_mask="r" denied_mask="r"
apparmor="DENIED" operation="mknod" name="/dev/shm/.org.chromium.Chromium.iKxChM" comm="Chrome_IOThread" requested_mask="c" denied_mask="c"

seccompはsyscall番号でログを表示します。syscall番号と実際の関数の対応を調べる方法はいろいろありますが、今回はseccompパッケージに付属しているscmp_sys_resolverコマンドを使ってみましょう。

$ sudo apt install seccomp
$ scmp_sys_resolver 41
socket
$ scmp_sys_resolver 314
sched_setattr

どうやらソケットを作るのとスケジューリングの設定関連の何かが呼ばれているようです[3]⁠。AppArmor側で拒否されているのは、Bluetoothへの通信と名前解決関連の設定ファイルの読み込み、それに共有メモリのmknodですね。

Bluetooth関連はPCによって実装されているかまちまちであるため、必須の機能ではないはずです。よってこれは後回ししても良さそうです。

ネットワーク通信関連はnetworkインターフェースを利用することで許可できます。snapパッケージは何も設定しないとネットワーク通信の権限がありません。networkインターフェースを追加することではじめて、接続できるようになるのです。その他のインターフェースはサポートされているインターフェースリストを参照してください。

試しにインターフェースを追加してみましょう。前述したようにsnapパッケージ側のインターフェースは「プラグ」と呼ばれています。よって次のように記述します。

apps:
  thorium:
    command: opt/Thorium/thorium --no-sandbox
    extensions: [gnome-3-38]
    plugs:
      - network
    desktop: usr/share/applications/thorium.desktop

「このsnapパッケージはnetworkプラグを利用します」と宣言しているわけです。あとはインストール先のsnapシステムが許可すればネットワーク通信を利用できます。ビルド&インストールしたら次のように確認してみましょう。

$ snap connections thorium-reader-snap
Interface                 Plug                                 Slot                             Notes
(中略)
network                   thorium-reader-snap:network          :network                         -
(後略)

networkプラグがスロットに追加されていますね。ちなみに他のインターフェースはgnomeエクステンションが自動的に追加したものです。

インターフェースは「自動的に接続されるもの」「ユーザーが明示的に許可してはじめて接続されるもの」の2種類が存在します。networkは前者で、cameraなどは後者です。

手動で接続許可するものは「Ubuntu Software」の個別のアプリケーションのページにある「Permission」から設定可能です。またCLIからだと次のように実施します。

$ sudo snap connect thorium-reader-snap:<インターフェース名>

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を有効化しましょう。

apps:
  thorium:
    command: opt/Thorium/thorium --no-sandbox
    extensions: [gnome-3-38]
    plugs:
      - bluez
      - browser-support
      - network
    desktop: usr/share/applications/thorium.desktop

インストールしたあとはまずインターフェースの状態を確認してみます。

$ snap connections thorium-reader-snap
Interface                 Plug                                 Slot                             Notes
bluez                     thorium-reader-snap:bluez            -                                -
browser-support           thorium-reader-snap:browser-support  :browser-support                 -
(中略)
network                   thorium-reader-snap:network          :network                         -
(後略)

期待通りに設定が行われていますね。bluezのみ接続されていません。必要なら次の方法で接続してください。

$ sudo snap connect thorium-reader-snap:bluez

もう一度Thorium Readerを起動してみましょう。今度は無事に起動できるようになっているはずです。

残りのログを確認する

無事に起動するようになったので、最低限のstrictモード対応は完了となります。このあとは個別に機能を使ってみながら、動かない機能に対して対応していくフェーズとなります。

まずは普通に動くようになった状態で、もう一度起動ログを見直してみましょう。

/usr/share/libdrm/amdgpu.ids: No such file or directory
(node:257885) electron: The default of contextIsolation is deprecated
  and will be changing from false to true in a future release of Electron.
  See https://github.com/electron/electron/issues/23506 for more information

後半のメッセージはElectron側の通知メッセージなので、気にしなくてもかまいません。必要ならThorium Reader側で対応することになるでしょう。最初のメッセージは、AMD GPUを動かしている環境ではlibdrmが/usr/share/libdrm/amdgpu.idsを読み込もうとします。snapパッケージの中にはこのファイルがないため、エラーメッセージが出ています。

実際のところ読めなくても実害はないものの、気になるようならlibdrm-commonパッケージをstage-packagesに追加しておくと良いでしょう。

layout:
  /usr/share/libdrm:
    bind: $SNAP/usr/share/libdrm

parts:
  (中略)
    stage-packages:
      - libdrm-common
      - libnss3

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パッケージに向いているインターフェースとなります。

今回設定したプラグをまとめると次のような結果になります。

apps:
  thorium:
    command: opt/Thorium/thorium --no-sandbox
    extensions: [gnome-3-38]
    plugs:
      - bluez
      - browser-support
      - home
      - network
      - system-observe
    desktop: usr/share/applications/thorium.desktop

これでstrictモードでも、Thorium Readerの最低限必要な機能が動くことは確認できました。今回は明示的には書いていませんが、strictモードにしたあとは改めて「日本語入力できるか」⁠URLをクリックしてウェブブラウザーが起動するか」なども確認しておくと良いでしょう。

ここまでのsnap/snapcraft.yamlの全体像はこちらのリポジトリから参照できます

次回はSnap Storeに公開する話です。

おすすめ記事

記事・ニュース一覧