はじめに
クリアコードは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_SEARCH
とCAP_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様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。