FUSEを使ってコマンドをファイルでラップする

少し前からFUSEに興味が湧いて色々作りたいものを考えていました。年末年始にある程度動くものができたので一旦まとめてみます。

github.com

ちなみに数日前に同僚のharasouさんが cat するたびに内容が変わるファイル?を作った という記事を上げていて、内容がかなり被っているので二番煎じ感がすごいです。

何をするものか

大体GitHubのREADMEに書いていますが、以下のような設定ファイルをyamlで定義してnanafshiを使ってマウントすると、読み込んだり書き込んだりした時に内部でコマンドを実行するファイルが作成されます。

shell: /bin/bash
services:
  - name: dir
    files:
      - name: example
        read:
          command: curl http://www.example.com/

      - name: writable
        read: 
          command: |
            touch /tmp/file
            cat /tmp/file
        write: 
          async: true
          command: echo $FUSE_STDIN >> /tmp/file

このyamlを使ってマウントしてやると、設定ファイルの構造に従ってディレクトリとファイルが作成されます。

$ nanafshi -c config.yml /mnt/nanafshi

$ tree /mnt/nanafshi
/mnt/nanafshi
└── dir
    ├── example
    └── writable

以下のように作成されたファイルをcatなどで読み込むと、内部で実行したコマンドの標準出力が表示されます。また、書き込みの場合はコマンドを実行するだけですが、環境変数として入力されたデータを渡せるようになっています。

$ cat /mnt/nanafshi/dir/example # curl http://www.example.com/ の結果
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...

内部コマンドで使用できる環境変数についてはGitHubの方でまとめていますが、ファイルを開いたプロセスのPIDやUIDなどもあり、必要に応じて増やしていくつもりです。

また、Macの場合は標準では使用できませんが、FUSE for macOS をインストールすればLinuxと同様に使えます。Windowsは未検証です。

何に使うのか

設定ファイルの動的生成

はじめに紹介した記事でharasouさんも言及していますが、アプリケーションの設定ファイルのパラメータをAPIやデータベースなどから取ってくるといったことができそうです。

外部ログ基盤との連携

readとwrite両方に対応していますので、例えば bash_history のようなログファイルを以下の設定で用意してやれば、外部のログ管理基盤と直接やりとりしてログを保存/取得し、複数ホスト間で共有するといったこともできます。

services:
  - name: logs
    files:
      - name: bash_history
        read:
          # read時はGETリクエストで全ログを取得
          command: curl https://store.example.com/tokibi/bash_history
        write:
          # write時はPUTリクエストで保存
          command: curl -X PUT -H "Text:$FUSE_STDIN" https://store.example.com/tokibi/bash_history

このようなファイルはコンテナ上のログのような揮発性の高いデータの保存に対して有用になりそうです。ただ、パフォーマンス面は全く検証できていないので、本番環境で使えるような信頼性を担保するところが目標です。

コマンド単位の権限移譲

ファイル内部で実行されるコマンドは、nanafshiのプロセスからforkされて実行されるようになっています。なので、rootユーザがnanafshiを起動してマウントした場合は、ファイルアクセス時に実行されるコマンドもrootユーザの権限で実行されます。

また、ファイルアクセス時にどのようなコマンドが実行されるかは、該当のファイルをどのように参照しても(多分)見えないので、設定ファイルの所有権を厳密に設定すればファイルアクセスを行うユーザから見て内部で何が起きているか調べる手段がないという状態になります。

これらの特性を利用すれば、設定ファイルに記載したコマンドのみに限定して、その内容を隠蔽しつつ他のユーザに実行権限を委譲するといったことが可能です。例えばコンテナプロセスに対してバインドマウントでrootユーザで起動したnanafshiのファイルを提供すれば、Linuxのcapabilityなどを無視して色々なコマンドの実行権限を委譲できてしまいます。ただの脆弱性にもなりかねないので、基本的には専用のユーザなどを用意して起動することをおすすめします。

システム全体の複雑性がとても高くなるので、安易に色々やると痛い目にあいそうですね。

今後の予定

  • テスト, ログ周りちゃんとする
  • ディレクトリ, ファイルごとに所有権を設定できるようにする
  • bash組み込みのreadコマンドのような対話的なコマンドを差し込めるようにしたい
    • ファイルを開く時にMFAとか入れたい
  • read時のcache実装
  • write時のFUSE_STDIN環境変数あたりでインジェクションできそうなので対策する

まとめ

設定ファイルにコマンドを書けばいいだけなので、FUSEを使って色々と簡単に試せると思います。面白そうな使い方があったら教えてください。

あと、nanafshiという名前の由来について会社でよく聞かれるので書いておくと、コマンドをファイル(Node)に擬態させる的なイメージから、枝葉に擬態する生き物を探してナナフシが見つかったのでそのまま採用しました(枝に擬態するのでちょっと違いますが)。あとfilesystemなのでnanafushiのuを削ってfsにしています。