From e7f789f7d80e7a92b5c96abea36e137ec3c3762c Mon Sep 17 00:00:00 2001 From: Kentaro Hayashi Date: Wed, 16 Sep 2026 06:13:54 +0000 Subject: [PATCH] out_opensearch,in_opensearch: mask hosts in the configuration dump Before: `hosts` accepts "https://user:password@host" and `endpoint.url` can carry a user and a password, but neither was marked secret. Fluentd logs the whole configuration after configure and masks only secret parameters, so those passwords were written to the log in plain text. After: `hosts` in both plugins and `endpoint.url` are secret, so the dump shows "xxxxxx" for them. The host list no longer appears in the startup log. Co-Authored-By: Claude Signed-off-by: Kentaro Hayashi --- lib/fluent/plugin/in_opensearch.rb | 4 +++- lib/fluent/plugin/out_opensearch.rb | 7 ++++-- test/plugin/test_in_opensearch.rb | 15 +++++++++++++ test/plugin/test_out_opensearch.rb | 35 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/fluent/plugin/in_opensearch.rb b/lib/fluent/plugin/in_opensearch.rb index 17fb391..d50fa78 100644 --- a/lib/fluent/plugin/in_opensearch.rb +++ b/lib/fluent/plugin/in_opensearch.rb @@ -51,7 +51,9 @@ class UnrecoverableRequestFailure < Fluent::UnrecoverableError; end config_param :password, :string, :default => nil, :secret => true config_param :path, :string, :default => nil config_param :scheme, :enum, :list => [:https, :http], :default => :http - config_param :hosts, :string, :default => nil + # `hosts` accepts "https://user:password@host", so it is masked in the + # configuration dump like `password` is. + config_param :hosts, :string, :default => nil, :secret => true config_param :index_name, :string, :default => "fluentd" config_param :parse_timestamp, :bool, :default => false config_param :timestamp_key_format, :string, :default => nil diff --git a/lib/fluent/plugin/out_opensearch.rb b/lib/fluent/plugin/out_opensearch.rb index 2ddcca4..8eed4d8 100644 --- a/lib/fluent/plugin/out_opensearch.rb +++ b/lib/fluent/plugin/out_opensearch.rb @@ -101,7 +101,9 @@ def initialize(retry_stream) config_param :password, :string, :default => nil, :secret => true config_param :path, :string, :default => nil config_param :scheme, :enum, :list => [:https, :http], :default => :http - config_param :hosts, :string, :default => nil + # `hosts` accepts "https://user:password@host" and "%{user}:%{password}@", + # so it is masked in the configuration dump like `password` is. + config_param :hosts, :string, :default => nil, :secret => true config_param :target_index_key, :string, :default => nil config_param :time_key_format, :string, :default => nil config_param :time_precision, :integer, :default => 9 @@ -185,7 +187,8 @@ def initialize(retry_stream) config_section :endpoint, multi: false do config_param :region, :string - config_param :url do |c| + # The URL can carry a user and a password, so mask it in the dump. + config_param :url, secret: true do |c| c.chomp("/") end config_param :access_key_id, :string, :default => "" diff --git a/test/plugin/test_in_opensearch.rb b/test/plugin/test_in_opensearch.rb index 1b75c25..ed15dd9 100644 --- a/test/plugin/test_in_opensearch.rb +++ b/test/plugin/test_in_opensearch.rb @@ -322,6 +322,21 @@ def test_hosts_list assert_equal 'raw.opensearch', instance.tag end + def test_hosts_is_masked_in_the_configuration_dump + config = Fluent::Config::Element.new( + 'ROOT', '', { + '@type' => 'opensearch', + 'hosts' => 'https://john:passw0rd@host1:443/elastic/', + 'tag' => 'raw.opensearch', + 'check_connection' => 'false', + }, []) + driver(config) + + dump = config.to_masked_element.to_s + assert_false dump.include?('passw0rd') + assert_true dump.include?('hosts xxxxxx') + end + def test_hosts_list_with_escape_placeholders config = %{ hosts https://%{j+hn}:%{passw@rd}@host1:443/elastic/,http://host2 diff --git a/test/plugin/test_out_opensearch.rb b/test/plugin/test_out_opensearch.rb index 8717655..4c8b5a4 100644 --- a/test/plugin/test_out_opensearch.rb +++ b/test/plugin/test_out_opensearch.rb @@ -366,6 +366,41 @@ def test_configure assert_equal :es, instance.endpoint.aws_service_name end + test 'hosts is masked in the configuration dump' do + config = Fluent::Config::Element.new( + 'ROOT', '', { + '@type' => 'opensearch', + 'hosts' => 'https://john:passw0rd@host1:443/elastic/', + }, [ + Fluent::Config::Element.new('buffer', 'tag', {}, []) + ]) + driver(config) + + dump = config.to_masked_element.to_s + assert_false dump.include?('passw0rd') + assert_true dump.include?('hosts xxxxxx') + end + + test 'endpoint url is masked in the configuration dump' do + config = Fluent::Config::Element.new( + 'ROOT', '', { + '@type' => 'opensearch', + }, [ + Fluent::Config::Element.new('endpoint', '', { + 'url' => "https://john:passw0rd@search-opensearch.aws.example.com/", + 'region' => "local", + 'access_key_id' => 'YOUR_AWESOME_KEY', + 'secret_access_key' => 'YOUR_AWESOME_SECRET', + }, []), + Fluent::Config::Element.new('buffer', 'tag', {}, []) + ]) + driver(config) + + dump = config.to_masked_element.to_s + assert_false dump.include?('passw0rd') + assert_true dump.include?('url xxxxxx') + end + test 'aws_credentials returns credential provider with access key' do config = Fluent::Config::Element.new( 'ROOT', '', {