ククログ

株式会社クリアコード > ククログ > FluentdでLinuxのcapabilityを処理するには

FluentdでLinuxのcapabilityを処理するには

はじめに

クリアコードはFluentdの開発に参加しています。 Fluentdは主にLinux上やWindows Server上でのユーザーが多いです。 Fluentdの使われ方で特に多いのがLinuxで動いているサーバーのログの取得です。 筆者畑ケがFluentdでLinuxのcapabilityを扱えるようにした話をまとめてみます。 FluentdでLinuxのcapabilityを扱う機能はFluentd v1.12.0に入る予定です。

capabilityとは

Linuxにはcapabilityという権限チェックを部分的にバイパスする機能があります。 この機能は、rootまでの権限は欲しくないけれど、システムの特定のパーミッションがあるように振る舞うユーザーやプロセスが欲しい時に有用です。

例えば、Linuxのsyslogのログファイルが/var/log/syslogにあるとすると、このログに関してはadmグループに属していない通常のユーザーでは読み込めません。

$ ls -lh /var/log/syslog
-rw-r----- 1 syslog adm 60K 11月  5 16:39 /var/log/syslog
$ cat /var/log/syslog
cat: /var/log/syslog: 許可がありません

ここで、rbenvでインストールしているRubyにLinuxのcapabilityの機能の一つのCAP_DAC_READ_SEARCHを付与してみます。

$ sudo setcap cap_dac_read_search=+eip $(rbenv prefix)/bin/ruby
$ filecap $(rbenv prefix)/bin/ruby 
~/.rbenv/versions/2.6.3/bin/ruby     dac_read_search

cap_dac_read_searchのcapabilityを付与したirbで/var/log/syslogを読み込んでみます。

$ irb
irb(main):001:0> File.read("/var/log/syslog")
=> "Nov  5 09:53:11 fluentd-testing anacron[22613]: Job `cron.daily' terminated\n..."

読み込むことができました。

capabilityをRubyから扱うには

LinuxのcapabilityをRubyから扱うにはcapabilityを処理できるライブラリのバインディングを書くのが良いでしょう。

筆者畑ケはlibcap-ngのRubyバインディングを開発しました。

Gemfileに以下のように追記して、

gem 'capng_c'

bundle installをすると:

$ bundle

capng_cをインストールできます。 もしくは、gem installでインストールできます。

$ gem install capng_c

依存するコマンドやライブラリについてはcapng_cのインストール要件をチェックしてみてください。

capng_cを通してcapabilityを確認する

Gemfileに以下を追記してbundle installします。

gem 'capng_c'
$ bundle install

そして、setcap cap_dac_read_search=+eip $(rbenv prefix)/bin/rubyを行ったRubyで動作するirb上でプロセスに付いているcapabilityを確認してみましょう。

irb> require 'capng'
irb> capng = CapNG.new
irb> capng.have_capability?(:effective, :dac_read_search)
=> true
irb> capng.have_capability?(:inheritable, :dac_read_search)
=> false
irb> capng.have_capability?(:permitted, :dac_read_search)
=> true

動いているRubyのプロセスの権限は継承できないようですが、ファイル読み込みの権限がバイパスされるようです。

in_tailでcapabilityを確認する

Fluentdのin_tailプラグインでLinuxのcapabilityを扱えるようにするには、 まず、capng_cを読み込んでいてもいなくても動作するラップするクラスを定義する必要があります。 これは、Fluentdの動作対象の環境はLinuxだけではなく、WindowsやmacOSもあり、また、capng_cは動作時に必須のgemとはしないためです。

fluent/envにLinuxかどうかを判定するメソッドを生やします。

diff --git a/lib/fluent/env.rb b/lib/fluent/env.rb
index 01eba2f6..2b0bf5c8 100644
--- a/lib/fluent/env.rb
+++ b/lib/fluent/env.rb
@@ -28,4 +28,8 @@ module Fluent
   def self.windows?
     ServerEngine.windows?
   end
+
+  def self.linux?
+    /linux/ === RUBY_PLATFORM
+  end
 end

次に、capng_cをラップするクラスを作成します。

diff --git a/lib/fluent/capability.rb b/lib/fluent/capability.rb
new file mode 100644
index 00000000..23f419d5
--- /dev/null
+++ b/lib/fluent/capability.rb
@@ -0,0 +1,87 @@
+#
+# Fluent
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+require "fluent/env"
+
+if Fluent.linux?
+  begin
+    require 'capng'
+  rescue LoadError
+  end
+end
+
+module Fluent
+  if defined?(CapNG)
+    class Capability
+      def initialize(target = nil, pid = nil)
+        @capng = CapNG.new(target, pid)
+      end
+
+      def usable?
+        true
+      end
+
+      def apply(select_set)
+        @capng.apply(select_set)
+      end
+
+      def clear(select_set)
+        @capng.clear(select_set)
+      end
+
+      def have_capability?(type, capability)
+        @capng.have_capability?(type, capability)
+      end
+
+      def update(action, type, capability_or_capability_array)
+        @capng.update(action, type, capability_or_capability_array)
+      end
+
+      def have_capabilities?(select_set)
+        @capng.have_capabilities?(select_set)
+      end
+    end
+  else
+    class Capability
+      def initialize(target = nil, pid = nil)
+      end
+
+      def usable?
+        false
+      end
+
+      def apply(select_set)
+        false
+      end
+
+      def clear(select_set)
+        false
+      end
+
+      def have_capability?(type, capability)
+        false
+      end
+
+      def update(action, type, capability_or_capability_array)
+        false
+      end
+
+      def have_capabilities?(select_set)
+        false
+      end
+    end
+  end
+end

このFluent::Capabilityクラスはcapng_c が正常に読み込まれた際にcapabilityの正確な情報を返しますが、 そうでない場合はスタブされた情報を返します。

in_tailでは、ファイルが読み込み可能かどうかのチェックはFile.readable?(path)で行っており、このメソッドはLinuxのcapabilityについては問い合わせません。 CAP_DAC_READ_SEARCHCAP_DAC_OVERRIDEが有効であれば、ファイルの読み込みに関する権限チェックをバイパスできるので、

diff --git a/lib/fluent/plugin/in_tail.rb b/lib/fluent/plugin/in_tail.rb
index 4c2b8a3d..632e5c7b 100644
--- a/lib/fluent/plugin/in_tail.rb
+++ b/lib/fluent/plugin/in_tail.rb
@@ -22,6 +22,7 @@ require 'fluent/event'
 require 'fluent/plugin/buffer'
 require 'fluent/plugin/parser_multiline'
 require 'fluent/variable_store'
+require 'fluent/capability'
 require 'fluent/plugin/in_tail/position_file'
 
 if Fluent.windows?
@@ -171,6 +172,7 @@ module Fluent::Plugin
       @dir_perm = system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION
       # parser is already created by parser helper
       @parser = parser_create(usage: parser_config['usage'] || @parser_configs.first.usage)
+      @capability = Fluent::Capability.new(:current_process)
     end
 
     def configure_tag
@@ -250,6 +252,11 @@ module Fluent::Plugin
       close_watcher_handles
     end
 
+    def have_read_capability?
+      @capability.have_capability?(:effective, :dac_read_search) ||
+        @capability.have_capability?(:effective, :dac_override)
+    end
+
     def expand_paths
       date = Fluent::EventTime.now
       paths = []
@@ -263,7 +270,7 @@ module Fluent::Plugin
           paths += Dir.glob(path).select { |p|
             begin
               is_file = !File.directory?(p)
-              if File.readable?(p) && is_file
+              if (File.readable?(p) || have_read_capability?) && is_file
                 if @limit_recently_modified && File.mtime(p) < (date.to_time - @limit_recently_modified)
                   false
                 else

という変更をin_tailに加えます。 この変更により、glob(*)で指定したファイルパターンの時もcapabilityまでチェックしてOKだったらエラーにならず、tailing対象のパスに加えます。

Linuxのcapabilityを見るようにしたin_tailの動作確認

cap_dac_read_searchを付与したRubyでFluentdを動かすと、パーミッション640のファイルを扱えるようになります。

例として/var/log/syslogを確認してみます:

$ ls -lh /var/log/syslog
-rw-r----- 1 syslog adm 29K Nov  5 14:35 /var/log/syslog

このファイルは通常ユーザーでは読めません。

$ cat /var/log/syslog
cat: /var/log/syslog: 許可がありません

cap_dac_read_searchをRubyの実行ファイルに付けます。 Fluentdが新たに提供するLinuxのcapabilityを操作するfluent-cap-ctlコマンドを使用します:

$ sudo fluent-cap-ctl --add dac_override [-f /path/to/bin/ruby]
Updating dac_override done.
Adding dac_override done.
$ fluent-cap-ctl --get -f /path/to/bin/ruby
Capabilities in '/path/to/bin/ruby',
Effective:   dac_override, dac_read_search
Inheritable: dac_override, dac_read_search
Permitted:   dac_override, dac_read_search

ここでfluent-cap-ctlコマンドを利用したdac_read_search capabilityの付与はsetcap cap_dac_read_search=+eip /path/to/bin/rubyと同義です。 fluent-cap-ctlコマンドは-f fileオプションでファイルを指定しない場合には、 /proc/self/exeをreadlinkして動かしているRubyの実行ファイルへ指定したcapabilityを自動で付与します。

そして以下のFluentdの設定ファイルを用意します:

<source>
  @type tail
  path /var/log/sysl*g
  pos_file /var/run/fluentd/log/syslog_test_with_capability.pos
  tag test
  rotate_wait 5
  read_from_head true
  refresh_interval 60
  <parse>
    @type syslog
  </parse>
</source>

<match test>
  @type stdout
</match>

positionファイルを配置するディレクトリを作成し、パーミッションを調整します:

$ sudo mkdir /var/run/fluentd
$ sudo chown `whoami` /var/run/fluentd

これで、通常ユーザーでcap_dac_read_searchの付いたRubyを使ってFluentdを実行すると:

$ bundle exec fluentd -c in_tail_camouflage_permission.conf
2020-11-05 14:47:57 +0900 [info]: parsing config file is succeeded path="example/in_tail.conf"
2020-11-05 14:47:57 +0900 [info]: gem 'fluentd' version '1.12.0'
2020-11-05 14:47:57 +0900 [info]: gem 'fluent-plugin-systemd' version '1.0.2'
2020-11-05 14:47:57 +0900 [info]: using configuration file: <ROOT>
  <source>
    @type tail
    path "/var/log/syslog"
    pos_file "/var/run/fluentd/log/syslog_test_with_capability.pos"
    tag "test"
    rotate_wait 5
    read_from_head true
    refresh_interval 60
    <parse>
      @type "syslog"
      unmatched_lines
    </parse>
  </source>
  <match test>
    @type stdout
  </match>
</ROOT2
2020-11-05 14:47:57 +0900 [info]: starting fluentd-1.12.0 pid=12109 ruby="2.6.3"
2020-11-05 14:47:57 +0900 [info]: spawn command to main:  cmdline=["/home/fluentd/.rbenv/versions/2.6.3/bin/ruby", "-rbundler/setup", "-Eascii-8bit:ascii-8bit", "/home/fluentd/work/fluentd/vendor/bundle/ruby/2.6.0/bin/fluentd", "-c", "example/in_tail.conf", "--under-supervisor"]
2020-11-05 14:47:58 +0900 [info]: adding match pattern="test" type="stdout"
2020-11-05 14:47:58 +0900 [info]: adding source type="tail"
2020-11-05 14:47:58 +0900 [info]: #0 starting fluentd worker pid=12143 ppid=12109 worker=0
2020-11-05 14:47:58 +0900 [info]: #0 following tail of /var/log/syslog
2020-11-05 09:53:11.000000000 +0900 test: {"host":"fluentd-testing","ident":"anacron","pid":"22613","message":"Job `cron.daily' terminated"}
2020-11-05 09:53:11.000000000 +0900 test: {"host":"fluentd-testing","ident":"anacron","pid":"22613","message":"Normal exit (1 job run)"}
2020-11-05 09:55:01.000000000 +0900 test: {"host":"fluentd-testing","ident":"CRON","pid":"24610","message":"(root) CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)"}

Fluentdは許可がありません、というエラーを吐かなくなります。 このことから、in_tailで通常ユーザーが読めないファイルをLinuxのcapabilityを見てあげることによって、 権限チェックをバイパスして通常ユーザーが読めないファイルを読めるようにできることがわかります。

まとめ

FluentdでLinuxのcapabilityを扱えるようにした作業で行ったことを解説しました。 Linux capabilityをFluentdに同梱されるコマンドのfluent-cap-ctlにて変更したり削除したりすることも併せてサポートしました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。