This is an automated email from the ASF dual-hosted git repository.

fresh-borzoni pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 3b4b8e10e [security] Redact sensitive config values in startup logs 
(#3486)
3b4b8e10e is described below

commit 3b4b8e10ec2705f8171a5f4cb31e72a52a79627a
Author: litiliu <[email protected]>
AuthorDate: Tue Jun 30 06:28:37 2026 +0800

    [security] Redact sensitive config values in startup logs (#3486)
    
    * [config] Redact sensitive values in startup config logs
    
    * [client] Reject client-side S3 credential options
    
    * [ci] Trigger CI rerun
    
    ---------
    
    Co-authored-by: litiliu <[email protected]>
---
 .../apache/fluss/config/ConfigurationUtils.java    |  73 ++++++++++++++
 .../apache/fluss/config/GlobalConfiguration.java   |   2 +-
 .../org/apache/fluss/config/ConfigurationTest.java |  77 +++++++++++++++
 .../fluss/config/GlobalConfigurationTest.java      | 105 +++++++++++++++++++++
 .../apache/fluss/server/DynamicServerConfig.java   |  15 ++-
 .../apache/fluss/server/zk/ZooKeeperClient.java    |   3 +-
 6 files changed, 268 insertions(+), 7 deletions(-)

diff --git 
a/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java 
b/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
index ad800aa8f..4b4ae09fc 100644
--- a/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
+++ b/fluss-common/src/main/java/org/apache/fluss/config/ConfigurationUtils.java
@@ -17,6 +17,7 @@
 
 package org.apache.fluss.config;
 
+import org.apache.fluss.utils.CollectionUtils;
 import org.apache.fluss.utils.TimeUtils;
 
 import javax.annotation.Nonnull;
@@ -38,6 +39,37 @@ import static 
org.apache.fluss.config.StructuredOptionsSplitter.escapeWithSingle
 
 /** Utility class for {@link Configuration} related helper functions. */
 public class ConfigurationUtils {
+
+    private static final String[] SENSITIVE_KEY_PARTS = {
+        "password",
+        "secret",
+        "fs.azure.account.key",
+        "apikey",
+        "api-key",
+        "auth-params",
+        "service-key",
+        "token",
+        "basic-auth",
+        "jaas.config",
+        "http-headers",
+        "private.key",
+        "private-key",
+        "access.key",
+        "access-key",
+        "access_key",
+        "accesskey"
+    };
+
+    // Exact allowlist for non-secret options that match broad sensitive key 
parts.
+    private static final Set<String> NON_SENSITIVE_KEYS =
+            Arrays.stream(
+                            new String[] {
+                                
ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_RETRY_BACKOFF.key(),
+                                
ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_TIME_RATIO.key()
+                            })
+                    .map(key -> key.toLowerCase(Locale.ROOT))
+                    .collect(Collectors.toSet());
+
     // 
--------------------------------------------------------------------------------------------
     //  Type conversion
     // 
--------------------------------------------------------------------------------------------
@@ -81,6 +113,47 @@ public class ConfigurationUtils {
         throw new IllegalArgumentException("Unsupported type: " + clazz);
     }
 
+    /**
+     * Returns a log-safe representation for a configuration value.
+     *
+     * @param key configuration key
+     * @param value configuration value
+     * @return {@link Password#HIDDEN_CONTENT} for sensitive values, otherwise 
the original value
+     */
+    public static Object hideSensitiveValue(String key, Object value) {
+        if (value == null) {
+            return null;
+        }
+        return value instanceof Password || isSensitiveKey(key) ? 
Password.HIDDEN_CONTENT : value;
+    }
+
+    /**
+     * Returns a copy of the given configuration map with sensitive values 
hidden.
+     *
+     * @param values configuration key/value pairs
+     * @return a new map containing log-safe values
+     */
+    public static Map<String, String> hideSensitiveValues(Map<String, String> 
values) {
+        Map<String, String> hiddenValues =
+                CollectionUtils.newHashMapWithExpectedSize(values.size());
+        values.forEach(
+                (key, value) -> hiddenValues.put(key, (String) 
hideSensitiveValue(key, value)));
+        return hiddenValues;
+    }
+
+    static boolean isSensitiveKey(String key) {
+        String lowerKey = key.toLowerCase(Locale.ROOT);
+        if (NON_SENSITIVE_KEYS.contains(lowerKey)) {
+            return false;
+        }
+        for (String sensitiveKeyPart : SENSITIVE_KEY_PARTS) {
+            if (lowerKey.contains(sensitiveKeyPart)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @SuppressWarnings("unchecked")
     static <T> T convertToList(Object rawValue, Class<?> atomicClass) {
         if (rawValue instanceof List) {
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/config/GlobalConfiguration.java 
b/fluss-common/src/main/java/org/apache/fluss/config/GlobalConfiguration.java
index fa7b6da91..b3d0412bf 100644
--- 
a/fluss-common/src/main/java/org/apache/fluss/config/GlobalConfiguration.java
+++ 
b/fluss-common/src/main/java/org/apache/fluss/config/GlobalConfiguration.java
@@ -126,7 +126,7 @@ public class GlobalConfiguration {
                                 "{} configuration property: {}={}",
                                 prefix,
                                 key,
-                                value instanceof Password ? 
Password.HIDDEN_CONTENT : value));
+                                ConfigurationUtils.hideSensitiveValue(key, 
value)));
     }
 
     /**
diff --git 
a/fluss-common/src/test/java/org/apache/fluss/config/ConfigurationTest.java 
b/fluss-common/src/test/java/org/apache/fluss/config/ConfigurationTest.java
index 77dfec168..d08099948 100644
--- a/fluss-common/src/test/java/org/apache/fluss/config/ConfigurationTest.java
+++ b/fluss-common/src/test/java/org/apache/fluss/config/ConfigurationTest.java
@@ -23,6 +23,7 @@ import java.time.Duration;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.stream.Collectors;
@@ -58,6 +59,11 @@ public class ConfigurationTest {
     private static final ConfigOption<Password> SECRET_OPTION =
             ConfigBuilder.key("secret").passwordType().noDefaultValue();
 
+    private static final String GS_SERVICE_ACCOUNT_PRIVATE_KEY =
+            "fs.gs.auth.service.account.private.key";
+    private static final String GS_SERVICE_ACCOUNT_PRIVATE_KEY_HYPHEN =
+            "fs.gs.auth.service.account.private-key";
+
     private static final Map<String, String> PROPERTIES_MAP = new HashMap<>();
 
     static {
@@ -456,6 +462,77 @@ public class ConfigurationTest {
         assertThat(cfg.get(SECRET_OPTION).value()).isEqualTo(secretValue);
     }
 
+    @Test
+    void testSensitiveKeyParts() {
+        List<String> sensitiveKeys =
+                Arrays.asList(
+                        "client.security.sasl.password",
+                        "fs.s3a.secret.key",
+                        "fs.azure.account.key.account.blob.core.windows.net",
+                        "connector.apikey",
+                        "connector.api-key",
+                        "connector.auth-params",
+                        "connector.service-key",
+                        "session.token",
+                        "security.basic-auth",
+                        "security.sasl.plain.jaas.config",
+                        "client.http-headers",
+                        GS_SERVICE_ACCOUNT_PRIVATE_KEY,
+                        GS_SERVICE_ACCOUNT_PRIVATE_KEY_HYPHEN,
+                        "fs.s3a.access.key",
+                        "s3.access-key",
+                        "fs.oss.accessKeyId",
+                        "datalake.lance.access_key_id");
+
+        for (String key : sensitiveKeys) {
+            
assertThat(ConfigurationUtils.isSensitiveKey(key)).as(key).isTrue();
+            
assertThat(ConfigurationUtils.isSensitiveKey(key.toUpperCase(Locale.ROOT)))
+                    .as(key)
+                    .isTrue();
+        }
+
+        
assertThat(ConfigurationUtils.isSensitiveKey("client.request.timeout")).isFalse();
+        
assertThat(ConfigurationUtils.isSensitiveKey("remote.data.dir")).isFalse();
+
+        List<String> nonSensitiveKeys =
+                Arrays.asList(
+                        
ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_RETRY_BACKOFF.key(),
+                        
ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_TIME_RATIO.key());
+        for (String key : nonSensitiveKeys) {
+            
assertThat(ConfigurationUtils.isSensitiveKey(key)).as(key).isFalse();
+            
assertThat(ConfigurationUtils.isSensitiveKey(key.toUpperCase(Locale.ROOT)))
+                    .as(key)
+                    .isFalse();
+        }
+    }
+
+    @Test
+    void testHideSensitiveValue() {
+        assertThat(ConfigurationUtils.hideSensitiveValue("fs.s3a.access.key", 
"ak"))
+                .isEqualTo(Password.HIDDEN_CONTENT);
+        
assertThat(ConfigurationUtils.hideSensitiveValue("client.security.sasl.password",
 "pwd"))
+                .isEqualTo(Password.HIDDEN_CONTENT);
+        assertThat(ConfigurationUtils.hideSensitiveValue("plain.key", new 
Password("pwd")))
+                .isEqualTo(Password.HIDDEN_CONTENT);
+        assertThat(ConfigurationUtils.hideSensitiveValue("plain.key", 
"value")).isEqualTo("value");
+    }
+
+    @Test
+    void testHideSensitiveValues() {
+        Map<String, String> values = new HashMap<>();
+        values.put("fs.s3a.access.key", "access-key");
+        values.put(GS_SERVICE_ACCOUNT_PRIVATE_KEY, "private-key");
+        
values.put(ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_RETRY_BACKOFF.key(), 
"10 s");
+        values.put("plain.key", "value");
+
+        assertThat(ConfigurationUtils.hideSensitiveValues(values))
+                .containsEntry("fs.s3a.access.key", Password.HIDDEN_CONTENT)
+                .containsEntry(GS_SERVICE_ACCOUNT_PRIVATE_KEY, 
Password.HIDDEN_CONTENT)
+                .containsEntry(
+                        
ConfigOptions.FILESYSTEM_SECURITY_TOKEN_RENEWAL_RETRY_BACKOFF.key(), "10 s")
+                .containsEntry("plain.key", "value");
+    }
+
     @Test
     void testPasswordParserErrorDoesNotLeakSensitiveData() {
         assertThat(SECRET_OPTION.isSensitive()).isTrue();
diff --git 
a/fluss-common/src/test/java/org/apache/fluss/config/GlobalConfigurationTest.java
 
b/fluss-common/src/test/java/org/apache/fluss/config/GlobalConfigurationTest.java
new file mode 100644
index 000000000..3baedb881
--- /dev/null
+++ 
b/fluss-common/src/test/java/org/apache/fluss/config/GlobalConfigurationTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+package org.apache.fluss.config;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link GlobalConfiguration}. */
+public class GlobalConfigurationTest {
+
+    @Test
+    void testSensitiveConfigurationValuesAreHiddenInLogs(@TempDir Path 
tempDir) throws Exception {
+        Files.write(
+                tempDir.resolve(GlobalConfiguration.FLUSS_CONF_FILENAME),
+                Arrays.asList(
+                        "fs.s3a.access.key: s3-access-key",
+                        "fs.gs.auth.service.account.private.key: 
gs-private-key",
+                        "fs.oss.accessKeyId: oss-access-key",
+                        "client.security.sasl.password: sasl-password",
+                        "client.filesystem.security.token.renewal.backoff: 10 
s",
+                        "client.request.timeout: 30 s"),
+                StandardCharsets.UTF_8);
+
+        Logger logger = (Logger) 
LogManager.getLogger(GlobalConfiguration.class);
+        Level previousLevel = logger.getLevel();
+        boolean previousAdditive = logger.isAdditive();
+        TestAppender appender = new TestAppender();
+        appender.start();
+        logger.addAppender(appender);
+        logger.setLevel(Level.INFO);
+        logger.setAdditive(false);
+
+        try {
+            GlobalConfiguration.loadConfiguration(tempDir.toString(), null);
+        } finally {
+            logger.removeAppender(appender);
+            logger.setLevel(previousLevel);
+            logger.setAdditive(previousAdditive);
+            appender.stop();
+        }
+
+        String logs = String.join("\n", appender.getMessages());
+        assertThat(logs)
+                .contains("Loading configuration property: 
fs.s3a.access.key=******")
+                .contains(
+                        "Loading configuration property: "
+                                + 
"fs.gs.auth.service.account.private.key=******")
+                .contains("Loading configuration property: 
fs.oss.accessKeyId=******")
+                .contains("Loading configuration property: 
client.security.sasl.password=******")
+                .contains(
+                        "Loading configuration property: "
+                                + 
"client.filesystem.security.token.renewal.backoff=10 s")
+                .contains("Loading configuration property: 
client.request.timeout=30 s")
+                .doesNotContain(
+                        "s3-access-key", "gs-private-key", "oss-access-key", 
"sasl-password");
+    }
+
+    private static class TestAppender extends AbstractAppender {
+
+        private final List<String> messages = new ArrayList<>();
+
+        TestAppender() {
+            super("test-appender", null, null, true, Property.EMPTY_ARRAY);
+        }
+
+        @Override
+        public void append(LogEvent event) {
+            messages.add(event.getMessage().getFormattedMessage());
+        }
+
+        private List<String> getMessages() {
+            return messages;
+        }
+    }
+}
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java 
b/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
index dee0b1fad..df82f606d 100644
--- 
a/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
+++ 
b/fluss-server/src/main/java/org/apache/fluss/server/DynamicServerConfig.java
@@ -21,6 +21,7 @@ import org.apache.fluss.annotation.Internal;
 import org.apache.fluss.config.ConfigOption;
 import org.apache.fluss.config.ConfigOptions;
 import org.apache.fluss.config.Configuration;
+import org.apache.fluss.config.ConfigurationUtils;
 import org.apache.fluss.config.cluster.ConfigValidator;
 import org.apache.fluss.config.cluster.ServerReconfigurable;
 import org.apache.fluss.exception.ConfigException;
@@ -168,7 +169,9 @@ class DynamicServerConfig {
 
         // Early return if no effective changes
         if (effectiveChanges.isEmpty()) {
-            LOG.info("No effective config changes detected for: {}", 
newDynamicConfigs);
+            LOG.info(
+                    "No effective config changes detected for: {}",
+                    ConfigurationUtils.hideSensitiveValues(newDynamicConfigs));
             return;
         }
 
@@ -181,7 +184,9 @@ class DynamicServerConfig {
 
         // Update internal state
         updateInternalState(newConfig, newConfigMap, newDynamicConfigs);
-        LOG.info("Dynamic configs changed: {}", effectiveChanges);
+        LOG.info(
+                "Dynamic configs changed: {}",
+                ConfigurationUtils.hideSensitiveValues(effectiveChanges));
     }
 
     /**
@@ -287,8 +292,8 @@ class DynamicServerConfig {
             LOG.error(
                     "Config validation failed for '{}': {} -> {}. {}",
                     configKey,
-                    oldValue,
-                    newValue,
+                    ConfigurationUtils.hideSensitiveValue(configKey, oldValue),
+                    ConfigurationUtils.hideSensitiveValue(configKey, newValue),
                     e.getMessage());
             if (skipErrorConfig) {
                 skippedConfigs.add(configKey);
@@ -366,7 +371,7 @@ class DynamicServerConfig {
                 throw new ConfigException(
                         String.format(
                                 "Cannot parse '%s' as %s for config '%s': %s",
-                                newValueStr,
+                                
ConfigurationUtils.hideSensitiveValue(configKey, newValueStr),
                                 configOption.isList()
                                         ? "List<" + 
configOption.getClazz().getSimpleName() + ">"
                                         : 
configOption.getClazz().getSimpleName(),
diff --git 
a/fluss-server/src/main/java/org/apache/fluss/server/zk/ZooKeeperClient.java 
b/fluss-server/src/main/java/org/apache/fluss/server/zk/ZooKeeperClient.java
index 533cca6ff..4c6837fb9 100644
--- a/fluss-server/src/main/java/org/apache/fluss/server/zk/ZooKeeperClient.java
+++ b/fluss-server/src/main/java/org/apache/fluss/server/zk/ZooKeeperClient.java
@@ -21,6 +21,7 @@ import org.apache.fluss.annotation.Internal;
 import org.apache.fluss.annotation.VisibleForTesting;
 import org.apache.fluss.config.ConfigOptions;
 import org.apache.fluss.config.Configuration;
+import org.apache.fluss.config.ConfigurationUtils;
 import org.apache.fluss.config.FlussConfigUtils;
 import org.apache.fluss.exception.CoordinatorEpochFencedException;
 import org.apache.fluss.metadata.DatabaseSummary;
@@ -1583,7 +1584,7 @@ public class ZooKeeperClient implements AutoCloseable {
                     .forPath(path, ConfigEntityZNode.encode(configs));
         }
 
-        LOG.info("upsert entity configs {}", configs);
+        LOG.info("upsert entity configs {}", 
ConfigurationUtils.hideSensitiveValues(configs));
         insertConfigChangeNotification();
     }
 

Reply via email to