From be43311ce43e2eebfa70ac294c9f72eae1a3eb4e Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Tue, 10 Nov 2020 10:22:29 +0900 Subject: [PATCH 1/5] command: Add flunet-cap-ctl to manage Linux capability for Fluentd Ruby Signed-off-by: Hiroshi Hatake --- bin/fluent-cap-ctl | 7 ++ lib/fluent/command/cap_ctl.rb | 155 ++++++++++++++++++++++++++++++++++ test/command/test_cap_ctl.rb | 100 ++++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100755 bin/fluent-cap-ctl create mode 100644 lib/fluent/command/cap_ctl.rb create mode 100644 test/command/test_cap_ctl.rb diff --git a/bin/fluent-cap-ctl b/bin/fluent-cap-ctl new file mode 100755 index 0000000000..b71c2fcea3 --- /dev/null +++ b/bin/fluent-cap-ctl @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +# -*- coding: utf-8 -*- +here = File.dirname(__FILE__) +$LOAD_PATH << File.expand_path(File.join(here, '..', 'lib')) +require 'fluent/command/cap_ctl' + +Fluent::CapCtl.new.call \ No newline at end of file diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb new file mode 100644 index 0000000000..6d80f9f4f8 --- /dev/null +++ b/lib/fluent/command/cap_ctl.rb @@ -0,0 +1,155 @@ +# +# Fluentd +# +# 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 'optparse' +require 'fluent/log' +require 'fluent/env' +require 'fluent/capability' + +module Fluent + class CapCtl + def prepare_option_parser + @op = OptionParser.new + + @op.on('--clear-cap', '--clear-capabilities', "Clear Fluentd Ruby capability") {|s| + @opts[:clear_capabilities] = true + } + + @op.on('--add-cap', '--add-capabilities [CAPABILITITY1, CAPABILITY2, ...]', "Add capabilities into Fluentd Ruby") {|s| + @opts[:add_capabilities] = s + } + + @op.on('--drop-cap', '--drop-capabilities [CAPABILITITY1, CAPABILITY2, ...]', "Drop capabilities into Fluentd Ruby") {|s| + @opts[:drop_capabilities] = s + } + + @op.on('--get-cap', '--get-capabilities', "Get capabilities for Fluentd") {|s| + @opts[:get_capabilities] = true + } + + @op.on('-f', '--file FILE', "Specify target file to add Linux capabilities") {|s| + @opts[:target_file] = s + } + end + + def usage(msg) + puts @op.to_s + puts "error: #{msg}" if msg + exit 1 + end + + def initialize(argv = ARGV) + @opts = {} + @argv = argv + + if Fluent.linux? + begin + require 'capng' + + @capng = CapNG.new + rescue LoadError + puts "Error: capng_c is not loaded. Please install it first." + exit 1 + end + else + puts "Error: This environment is not supported." + exit 2 + end + + prepare_option_parser + end + + def call + parse_options!(@argv) + + target_file = if !!@opts[:target_file] + @opts[:target_file] + else + File.readlink("/proc/self/exe") + end + + if @opts[:clear_capabilities] + clear_capabilities(@opts, target_file) + elsif @opts[:add_capabilities] + add_capabilities(@opts, target_file) + elsif @opts[:drop_capabilities] + drop_capabilities(@opts, target_file) + end + if @opts[:get_capabilities] + get_capabilities(@opts, target_file) + end + end + + def clear_capabilities(opts, target_file) + if !!opts[:clear_capabilities] + @capng.clear(:caps) + ret = @capng.apply_caps_file(target_file) + puts "Clear capabilities #{ret ? 'done' : 'fail'}." + end + end + + def add_capabilities(opts, target_file) + if add_caps = opts[:add_capabilities] + @capng.clear(:caps) + @capng.caps_file(target_file) + capabilities = add_caps.split(/\s*,\s*/) + ret = @capng.update(:add, + CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, + capabilities) + puts "Updating #{add_caps} #{ret ? 'done' : 'fail'}." + ret = @capng.apply_caps_file(target_file) + puts "Adding #{add_caps} #{ret ? 'done' : 'fail'}." + end + end + + def drop_capabilities(opts, target_file) + if drop_caps = opts[:drop_capabilities] + @capng.clear(:caps) + @capng.caps_file(target_file) + capabilities = drop_caps.split(/\s*,\s*/) + ret = @capng.update(:drop, + CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, + capabilities) + puts "Updating #{drop_caps} #{ret ? 'done' : 'fail'}." + @capng.apply_caps_file(target_file) + puts "Dropping #{drop_caps} #{ret ? 'done' : 'fail'}." + end + end + + def get_capabilities(opts, target_file) + if opts[:get_capabilities] + @capng.caps_file(target_file) + print = CapNG::Print.new + puts "Capabilities in '#{target_file}'," + puts "Effective: #{print.caps_text(:buffer, :effective)}" + puts "Inheritable: #{print.caps_text(:buffer, :inheritable)}" + puts "Permitted: #{print.caps_text(:buffer, :permitted)}" + end + end + + def parse_options!(argv) + begin + rest = @op.parse(argv) + + if rest.length != 0 + usage nil + end + rescue + usage $!.to_s + end + end + end +end diff --git a/test/command/test_cap_ctl.rb b/test/command/test_cap_ctl.rb new file mode 100644 index 0000000000..dc11236b3d --- /dev/null +++ b/test/command/test_cap_ctl.rb @@ -0,0 +1,100 @@ +require_relative '../helper' + +require 'tempfile' +require 'fluent/command/cap_ctl' + +class TestFluentCapCtl < Test::Unit::TestCase + setup do + omit "This environment does not handle Linux capability" unless defined?(CapNG) + end + + sub_test_case "success" do + test "clear capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--clear-cap"]).call + end + expression = /\AClear capabilities .*\n/m + assert_match expression, logs + end + + test "add capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--add-cap", "dac_override"]).call + end + expression = /\AUpdating .* done.\nAdding .*\n/m + assert_match expression, logs + end + + test "drop capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--drop-cap", "chown"]).call + end + expression = /\AUpdating .* done.\nDropping .*\n/m + assert_match expression, logs + end + + test "get capability" do + logs = capture_stdout do + Fluent::CapCtl.new(["--get-cap"]).call + end + expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m + assert_match expression, logs + end + end + + sub_test_case "success with file" do + test "clear capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--clear-cap", "-f", tempfile.path]).call + end + end + expression = /\AClear capabilities .*\n/m + assert_match expression, logs + end + + test "add capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--add-cap", "dac_override", "-f", tempfile.path]).call + end + end + expression = /\AUpdating .* done.\nAdding .*\n/m + assert_match expression, logs + end + + test "drop capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--drop-cap", "chown", "-f", tempfile.path]).call + end + end + expression = /\AUpdating .* done.\nDropping .*\n/m + assert_match expression, logs + end + + test "get capability" do + logs = capture_stdout do + Tempfile.create("fluent-cap-") do |tempfile| + Fluent::CapCtl.new(["--get-cap", "-f", tempfile.path]).call + end + end + expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m + assert_match expression, logs + end + end + + sub_test_case "invalid" do + test "add capability" do + assert_raise(RuntimeError) do + Fluent::CapCtl.new(["--add-cap", "nonexitent"]).call + end + end + + test "drop capability" do + assert_raise(RuntimeError) do + Fluent::CapCtl.new(["--drop-cap", "invalid"]).call + end + end + end +end From 6e918705c42f0008ba7633db24e7198209a93661 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Mon, 16 Nov 2020 10:43:44 +0900 Subject: [PATCH 2/5] command: Show more human readable error message Signed-off-by: Hiroshi Hatake --- lib/fluent/command/cap_ctl.rb | 19 +++++++++++++++++++ test/command/test_cap_ctl.rb | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb index 6d80f9f4f8..cf5c439455 100644 --- a/lib/fluent/command/cap_ctl.rb +++ b/lib/fluent/command/cap_ctl.rb @@ -106,6 +106,7 @@ def add_capabilities(opts, target_file) @capng.clear(:caps) @capng.caps_file(target_file) capabilities = add_caps.split(/\s*,\s*/) + check_capabilities(capabilities, get_valid_capabilities) ret = @capng.update(:add, CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, capabilities) @@ -120,6 +121,7 @@ def drop_capabilities(opts, target_file) @capng.clear(:caps) @capng.caps_file(target_file) capabilities = drop_caps.split(/\s*,\s*/) + check_capabilities(capabilities, get_valid_capabilities) ret = @capng.update(:drop, CapNG::Type::EFFECTIVE | CapNG::Type::INHERITABLE | CapNG::Type::PERMITTED, capabilities) @@ -140,6 +142,23 @@ def get_capabilities(opts, target_file) end end + def get_valid_capabilities + capabilities = [] + cap = CapNG::Capability.new + cap.each do |_code, capability| + capabilities << capability + end + capabilities + end + + def check_capabilities(capabilities, valid_capabilities) + capabilities.each do |capability| + unless valid_capabilities.include?(capability) + raise ArgumentError, "'#{capability}' is not valid capability. Valid Capabilities are: #{valid_capabilities.join(", ")}" + end + end + end + def parse_options!(argv) begin rest = @op.parse(argv) diff --git a/test/command/test_cap_ctl.rb b/test/command/test_cap_ctl.rb index dc11236b3d..067880b468 100644 --- a/test/command/test_cap_ctl.rb +++ b/test/command/test_cap_ctl.rb @@ -86,13 +86,13 @@ class TestFluentCapCtl < Test::Unit::TestCase sub_test_case "invalid" do test "add capability" do - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Fluent::CapCtl.new(["--add-cap", "nonexitent"]).call end end test "drop capability" do - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Fluent::CapCtl.new(["--drop-cap", "invalid"]).call end end From 9a13c589dc84e8604052ebdb4f9d084afd6c7424 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Mon, 16 Nov 2020 16:32:44 +0900 Subject: [PATCH 3/5] command: Remove redundant -cap suffix for clearing capabilities Signed-off-by: Hiroshi Hatake --- lib/fluent/command/cap_ctl.rb | 2 +- test/command/test_cap_ctl.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb index cf5c439455..ce7b564bdd 100644 --- a/lib/fluent/command/cap_ctl.rb +++ b/lib/fluent/command/cap_ctl.rb @@ -24,7 +24,7 @@ class CapCtl def prepare_option_parser @op = OptionParser.new - @op.on('--clear-cap', '--clear-capabilities', "Clear Fluentd Ruby capability") {|s| + @op.on('--clear', "Clear Fluentd Ruby capability") {|s| @opts[:clear_capabilities] = true } diff --git a/test/command/test_cap_ctl.rb b/test/command/test_cap_ctl.rb index 067880b468..53c58271cd 100644 --- a/test/command/test_cap_ctl.rb +++ b/test/command/test_cap_ctl.rb @@ -11,7 +11,7 @@ class TestFluentCapCtl < Test::Unit::TestCase sub_test_case "success" do test "clear capability" do logs = capture_stdout do - Fluent::CapCtl.new(["--clear-cap"]).call + Fluent::CapCtl.new(["--clear"]).call end expression = /\AClear capabilities .*\n/m assert_match expression, logs From 787123d44f9f3ad0a30ea27116efd04a9a0b5dd2 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Tue, 17 Nov 2020 11:17:13 +0900 Subject: [PATCH 4/5] command: Remove redundant -cap suffixes from options Signed-off-by: Hiroshi Hatake --- lib/fluent/command/cap_ctl.rb | 6 +++--- test/command/test_cap_ctl.rb | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb index ce7b564bdd..4a3af21933 100644 --- a/lib/fluent/command/cap_ctl.rb +++ b/lib/fluent/command/cap_ctl.rb @@ -28,15 +28,15 @@ def prepare_option_parser @opts[:clear_capabilities] = true } - @op.on('--add-cap', '--add-capabilities [CAPABILITITY1, CAPABILITY2, ...]', "Add capabilities into Fluentd Ruby") {|s| + @op.on('--add [CAPABILITITY1,CAPABILITY2, ...]', "Add capabilities into Fluentd Ruby") {|s| @opts[:add_capabilities] = s } - @op.on('--drop-cap', '--drop-capabilities [CAPABILITITY1, CAPABILITY2, ...]', "Drop capabilities into Fluentd Ruby") {|s| + @op.on('--drop [CAPABILITITY1,CAPABILITY2, ...]', "Drop capabilities into Fluentd Ruby") {|s| @opts[:drop_capabilities] = s } - @op.on('--get-cap', '--get-capabilities', "Get capabilities for Fluentd") {|s| + @op.on('--get', "Get capabilities for Fluentd Ruby") {|s| @opts[:get_capabilities] = true } diff --git a/test/command/test_cap_ctl.rb b/test/command/test_cap_ctl.rb index 53c58271cd..262b8bcc05 100644 --- a/test/command/test_cap_ctl.rb +++ b/test/command/test_cap_ctl.rb @@ -19,7 +19,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "add capability" do logs = capture_stdout do - Fluent::CapCtl.new(["--add-cap", "dac_override"]).call + Fluent::CapCtl.new(["--add", "dac_override"]).call end expression = /\AUpdating .* done.\nAdding .*\n/m assert_match expression, logs @@ -27,7 +27,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "drop capability" do logs = capture_stdout do - Fluent::CapCtl.new(["--drop-cap", "chown"]).call + Fluent::CapCtl.new(["--drop", "chown"]).call end expression = /\AUpdating .* done.\nDropping .*\n/m assert_match expression, logs @@ -35,7 +35,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "get capability" do logs = capture_stdout do - Fluent::CapCtl.new(["--get-cap"]).call + Fluent::CapCtl.new(["--get"]).call end expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m assert_match expression, logs @@ -56,7 +56,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "add capability" do logs = capture_stdout do Tempfile.create("fluent-cap-") do |tempfile| - Fluent::CapCtl.new(["--add-cap", "dac_override", "-f", tempfile.path]).call + Fluent::CapCtl.new(["--add", "dac_override", "-f", tempfile.path]).call end end expression = /\AUpdating .* done.\nAdding .*\n/m @@ -66,7 +66,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "drop capability" do logs = capture_stdout do Tempfile.create("fluent-cap-") do |tempfile| - Fluent::CapCtl.new(["--drop-cap", "chown", "-f", tempfile.path]).call + Fluent::CapCtl.new(["--drop", "chown", "-f", tempfile.path]).call end end expression = /\AUpdating .* done.\nDropping .*\n/m @@ -76,7 +76,7 @@ class TestFluentCapCtl < Test::Unit::TestCase test "get capability" do logs = capture_stdout do Tempfile.create("fluent-cap-") do |tempfile| - Fluent::CapCtl.new(["--get-cap", "-f", tempfile.path]).call + Fluent::CapCtl.new(["--get", "-f", tempfile.path]).call end end expression = /\ACapabilities in .*,\nEffective: .*\nInheritable: .*\nPermitted: .*/m @@ -87,13 +87,13 @@ class TestFluentCapCtl < Test::Unit::TestCase sub_test_case "invalid" do test "add capability" do assert_raise(ArgumentError) do - Fluent::CapCtl.new(["--add-cap", "nonexitent"]).call + Fluent::CapCtl.new(["--add", "nonexitent"]).call end end test "drop capability" do assert_raise(ArgumentError) do - Fluent::CapCtl.new(["--drop-cap", "invalid"]).call + Fluent::CapCtl.new(["--drop", "invalid"]).call end end end From 8e0a1e24c7e6cb05606bb2eb0a5d20529ae1b420 Mon Sep 17 00:00:00 2001 From: Hiroshi Hatake Date: Tue, 17 Nov 2020 17:08:48 +0900 Subject: [PATCH 5/5] Fix English Signed-off-by: Hiroshi Hatake --- lib/fluent/command/cap_ctl.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fluent/command/cap_ctl.rb b/lib/fluent/command/cap_ctl.rb index 4a3af21933..6c6a817a5f 100644 --- a/lib/fluent/command/cap_ctl.rb +++ b/lib/fluent/command/cap_ctl.rb @@ -32,7 +32,7 @@ def prepare_option_parser @opts[:add_capabilities] = s } - @op.on('--drop [CAPABILITITY1,CAPABILITY2, ...]', "Drop capabilities into Fluentd Ruby") {|s| + @op.on('--drop [CAPABILITITY1,CAPABILITY2, ...]', "Drop capabilities from Fluentd Ruby") {|s| @opts[:drop_capabilities] = s }