From c9717b4ed53417f20a8c35747d8797d2d0ea974b Mon Sep 17 00:00:00 2001
From: Stefan Miklosovic
Date: Tue, 26 Aug 2025 16:17:24 +0200
Subject: [PATCH] Redact security-sensitive information in
system_views.settings
patch by Stefan Miklosovic; reviewed by Maxwell Guo, Francisco Guerrero for CASSANDRA-20856
---
CHANGES.txt | 4 ++
.../cassandra/config/EncryptionOptions.java | 4 ++
.../apache/cassandra/config/Properties.java | 21 ++++++++
.../org/apache/cassandra/config/Redacted.java | 42 +++++++++++++++
.../cassandra/db/virtual/SettingsTable.java | 22 ++++++++
.../db/virtual/SettingsTableTest.java | 54 +++++++++++++++++++
6 files changed, 147 insertions(+)
create mode 100644 src/java/org/apache/cassandra/config/Redacted.java
diff --git a/CHANGES.txt b/CHANGES.txt
index 6b239e6547..75f12f8f0b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,7 @@
+4.1.11
+ * Redact security-sensitive information in system_views.settings (CASSANDRA-20856)
+
+
4.1.10
* Improve CommitLogSegmentReader to skip SyncBlocks correctly in case of CRC errors (CASSANDRA-20664)
* Do not crash on first boot with data_disk_usage_max_disk_size set when data directory is not created yet (CASSANDRA-20787)
diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java
index a51e56551b..2380261dbc 100644
--- a/src/java/org/apache/cassandra/config/EncryptionOptions.java
+++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java
@@ -74,11 +74,15 @@ public class EncryptionOptions
* truststore_passwords configurations as they are in plaintext format.
*/
public final ParameterizedClass ssl_context_factory;
+ @Redacted
public final String keystore;
@Nullable
+ @Redacted
public final String keystore_password;
+ @Redacted
public final String truststore;
@Nullable
+ @Redacted
public final String truststore_password;
public final List cipher_suites;
protected String protocol;
diff --git a/src/java/org/apache/cassandra/config/Properties.java b/src/java/org/apache/cassandra/config/Properties.java
index 79852d4af6..b57b4a23d6 100644
--- a/src/java/org/apache/cassandra/config/Properties.java
+++ b/src/java/org/apache/cassandra/config/Properties.java
@@ -17,6 +17,7 @@
*/
package org.apache.cassandra.config;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayDeque;
import java.util.Collection;
@@ -192,5 +193,25 @@ public final class Properties
throw e;
}
}
+
+ /**
+ * If there is a hierarchy of settings, like
+ *
+ * {@code a.b.c.{d,e,f,g,h}}
+ *
+ * and we put e.g. {@link Redacted} on {@code c},
+ * then all {@code d,e,f,g,h} will be redacted as well automatically.
+ * This is handy for cases when we want to redact whole family of properties by one shot.
+ *
+ * @param aClass annotation to get
+ * @return found annotation of given type on root or on leaf, null when not present.
+ * @param aClass)
+ {
+ A annotation = root.getAnnotation(aClass);
+ return annotation != null ? annotation : leaf.getAnnotation(aClass);
+ }
}
}
diff --git a/src/java/org/apache/cassandra/config/Redacted.java b/src/java/org/apache/cassandra/config/Redacted.java
new file mode 100644
index 0000000000..5b832484cc
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/Redacted.java
@@ -0,0 +1,42 @@
+/*
+ * 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.cassandra.config;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation supposed to be placed on any field in Config (and its subproperties in embedded configuration classes)
+ * which will be redacted e.g. password or similar.
+ *
+ * User querying system_views.settings will be shown redacted values on such fields.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD })
+public @interface Redacted
+{
+ String REDACTED_STRING = "";
+
+ /**
+ * @return redacted value, defaults to {@code }.
+ */
+ String redactedValue() default REDACTED_STRING;
+}
diff --git a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
index 473cb88bb2..c4e7d95698 100644
--- a/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
+++ b/src/java/org/apache/cassandra/db/virtual/SettingsTable.java
@@ -26,6 +26,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.Redacted;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Loader;
import org.apache.cassandra.config.Properties;
@@ -89,6 +90,10 @@ final class SettingsTable extends AbstractVirtualTable
@VisibleForTesting
String getValue(Property prop)
{
+ Redacted maybeCredential = prop.getAnnotation(Redacted.class);
+ if (maybeCredential != null)
+ return maybeCredential.redactedValue();
+
Object value = prop.get(config);
if (value == null)
return null;
@@ -96,6 +101,23 @@ final class SettingsTable extends AbstractVirtualTable
if (value.getClass().isArray())
return Arrays.asList((Object[]) value).toString();
+ if (value instanceof Map)
+ {
+ Map map = new HashMap<>();
+ for (Map.Entry entry : ((Map) value).entrySet())
+ {
+ // this is done on best-effort basis as we do not have names in parameters
+ // inherently under control as this is what a user is responsible for
+ // when dealing with custom implementations
+ if (entry.getKey().endsWith("_password") || entry.getKey().equals("password"))
+ map.put(entry.getKey(), Redacted.REDACTED_STRING);
+ else
+ map.put(entry.getKey(), entry.getValue());
+ }
+
+ return map.toString();
+ }
+
return value.toString();
}
diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
index 32b85e24cc..7cf9aef1d8 100644
--- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
+++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java
@@ -18,8 +18,11 @@
package org.apache.cassandra.db.virtual;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
@@ -31,13 +34,18 @@ import org.junit.Test;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import org.apache.cassandra.config.Config;
+import org.apache.cassandra.config.Redacted;
+import org.apache.cassandra.config.DefaultLoader;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption;
import org.apache.cassandra.config.ParameterizedClass;
+import org.apache.cassandra.config.TransparentDataEncryptionOptions;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.security.SSLFactory;
import org.yaml.snakeyaml.introspector.Property;
+import static java.util.stream.Collectors.toMap;
+
public class SettingsTableTest extends CQLTester
{
private static final String KS_NAME = "vts";
@@ -63,6 +71,16 @@ public class SettingsTableTest extends CQLTester
config.commitlog_sync_group_window = new DurationSpec.IntMillisecondsBound(0);
config.credentials_update_interval = null;
config.data_file_directories = new String[] {"/my/data/directory", "/another/data/directory"};
+
+ Map params = new LinkedHashMap<>();
+ params.put("keystore_password", "password");
+ params.put("key_password", "password");
+ params.put("keystore", "conf/.keystore");
+ config.transparent_data_encryption_options = new TransparentDataEncryptionOptions(false,
+ "AES/CBC/PKCS5Padding",
+ "alias",
+ new ParameterizedClass("SomeClass",
+ params));
table = new SettingsTable(KS_NAME, config);
VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace(KS_NAME, ImmutableList.of(table)));
disablePreparedReuseForTest();
@@ -299,4 +317,40 @@ public class SettingsTableTest extends CQLTester
config.transparent_data_encryption_options.iv_length = 7;
check(pre + "iv_length", "7");
}
+
+ @Test
+ public void testRedaction() throws Throwable
+ {
+ assertValue("transparent_data_encryption_options.key_provider.parameters",
+ String.format("{keystore_password=%s, keystore=conf/.keystore, key_password=%s}",
+ Redacted.REDACTED_STRING,
+ Redacted.REDACTED_STRING));
+
+ Set> entries = new DefaultLoader().flatten(Config.class)
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue().getAnnotation(Redacted.class) != null)
+ .collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (e, r) -> e, TreeMap::new))
+ .entrySet();
+
+ Assert.assertFalse(entries.isEmpty());
+
+ for (Map.Entry entry : entries)
+ {
+ logger.info("redacted {}", entry.getKey());
+ assertValue(entry.getKey(), entry.getValue().getAnnotation(Redacted.class).redactedValue());
+ }
+ }
+
+ private void assertValue(String settingName, String expectedValue) throws Throwable
+ {
+ List all = executeNet(String.format("SELECT * from vts.settings WHERE name = '%s'", settingName)).all();
+ Assert.assertFalse(all.isEmpty());
+ Row row = all.get(0);
+ String name = row.getString("name");
+ String value = row.getString("value");
+
+ Assert.assertEquals(settingName, name);
+ Assert.assertEquals(expectedValue, value);
+ }
}