Skip to content

Commit

Permalink
Obfuscate passwords in statements in QueryEvents
Browse files Browse the repository at this point in the history
patch by Sumanth Pasupuleti; reviewed by Ekaterina Dimitrova, Stefan Miklosovic and Vinay Chella for CASSANDRA-16669
  • Loading branch information
Sumanth Pasupuleti authored and smiklosovic committed Jun 15, 2021
1 parent 1579d20 commit f978bea
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
@@ -1,4 +1,5 @@
4.0-rc2
* Obfuscate passwords in statements in QueryEvents (CASSANDRA-16669)
* Fix queries on empty partitions with static data (CASSANDRA-16686)
* Keep python driver in artifacts (CASSANDRA-16700)
* Improve AuditLogging documentation and logback.xml(CASSANDRA-16682)
Expand Down
66 changes: 34 additions & 32 deletions doc/source/new/auditlogging.rst
Expand Up @@ -89,6 +89,8 @@ Audit logging does not log:

1. Configuration changes made in ``cassandra.yaml``
2. Nodetool Commands
3. Passwords mentioned as part of DCL statements. Instead everything after the appearance of the word password in DCL
statements is obfuscated as ******* as well as in failed to parse statements.

Limitations
^^^^^^^^^^^
Expand Down Expand Up @@ -449,38 +451,38 @@ The ``auditlogviewer`` tool is used to dump audit logs. Run the ``auditlogviewer
::

[ec2-user@ip-10-0-2-238 hourly]$ auditlogviewer /cassandra/audit/logs/hourly
WARN 03:12:11,124 Using Pauser.sleepy() as not enough processors, have 2, needs 8+
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427328|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE AuditLogKeyspace;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427329|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE "auditlogkeyspace"
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711446279|type :SELECT|category:QUERY|ks:auditlogkeyspace|scope:t|operation:SELECT * FROM t;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564713878834|type :DROP_TABLE|category:DDL|ks:auditlogkeyspace|scope:t|operation:DROP TABLE IF EXISTS
AuditLogKeyspace.t;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42382|timestamp:1564714618360|ty
pe:REQUEST_FAILURE|category:ERROR|operation:CREATE KEYSPACE AuditLogKeyspace
WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};; Cannot add
existing keyspace "auditlogkeyspace"
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714690968|type :DROP_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:DROP KEYSPACE AuditLogKeyspace;
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42406|timestamp:1564714708329|ty pe:CREATE_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:CREATE KEYSPACE
AuditLogKeyspace
WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};
Type: AuditLog
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714870678|type :USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE auditlogkeyspace;
[ec2-user@ip-10-0-2-238 hourly]$
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427328|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE AuditLogKeyspace;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711427329|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE "auditlogkeyspace"
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564711446279|type:SELECT|category:QUERY|ks:auditlogkeyspace|scope:t|operation:SELECT * FROM t;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564713878834|type:DROP_TABLE|category:DDL|ks:auditlogkeyspace|scope:t|operation:DROP TABLE IF EXISTS
AuditLogKeyspace.t;
Type: audit
LogMessage: user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42382|timestamp:1564714618360|type:REQUEST_FAILURE|category:ERROR|operation:CREATE KEYSPACE AuditLogKeyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};; Cannot add existing keyspace "auditlogkeyspace"
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714690968|type:DROP_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:DROP KEYSPACE AuditLogKeyspace;
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/3.91.56.164|port:42406|timestamp:1564714708329|type:CREATE_KEYSPACE|category:DDL|ks:auditlogkeyspace|operation:CREATE KEYSPACE AuditLogKeyspace WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};
Type: audit
LogMessage:
user:anonymous|host:10.0.2.238:7000|source:/127.0.0.1|port:46264|timestamp:1564714870678|type:USE_KEYSPACE|category:OTHER|ks:auditlogkeyspace|operation:USE auditlogkeyspace;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630496708|type:CREATE_ROLE|category:DCL|operation:CREATE ROLE role1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630634552|type:ALTER_ROLE|category:DCL|operation:ATLER ROLE role1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630698686|type:CREATE_ROLE|category:DCL|operation:CREATE USER user1 WITH PASSWORD*******;
Type: audit
LogMessage: user:cassandra|host:localhost/127.0.0.1:7000|source:/127.0.0.1|port:65282|timestamp:1622630747344|type:ALTER_ROLE|category:DCL|operation:ALTER USER user1 WITH PASSWORD*******;


The ``auditlogviewer`` tool usage syntax is as follows.
Expand Down
4 changes: 4 additions & 0 deletions doc/source/new/fqllogging.rst
Expand Up @@ -38,6 +38,10 @@ Some of the features of FQL are:
FQL logs all successful Cassandra Query Language (CQL) requests, both events that modify the data and those that query.
While audit logs also include CQL requests, FQL logs only the CQL request. This difference means that FQL can be used to replay or compare logs, which audit logging cannot. FQL is useful for debugging, performance benchmarking, testing and auditing CQL queries, while audit logs are useful for compliance.

Currently DCL statements containing passwords are logged for informational purposes but for security reasons they are not available for replay.
Replay of those statements will be unsuccessful operation because everything after the word password in a DCL statement
will be obfuscated as *******.

In performance testing, FQL appears to have little or no overhead in ``WRITE`` only workloads, and a minor overhead in ``MIXED`` workload.

Query information logged
Expand Down
2 changes: 1 addition & 1 deletion src/java/org/apache/cassandra/audit/AuditLogManager.java
Expand Up @@ -135,7 +135,7 @@ else if (e instanceof AuthenticationException)
builder.setType(AuditLogEntryType.REQUEST_FAILURE);
}

builder.appendToOperation(e.getMessage());
builder.appendToOperation(QueryEvents.instance.getObfuscator().obfuscate(e.getMessage()));

log(builder.build());
}
Expand Down
40 changes: 40 additions & 0 deletions src/java/org/apache/cassandra/cql3/PasswordObfuscator.java
@@ -0,0 +1,40 @@
/*
* 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.cql3;

/**
* Obfuscates passwords in a given string
*/
public class PasswordObfuscator
{
public static final String OBFUSCATION_TOKEN = " *******";
private static final String PASSWORD_TOKEN = "password";

public String obfuscate(String sourceString)
{
if (null == sourceString)
return null;

int passwordTokenStartIndex = sourceString.toLowerCase().indexOf(PASSWORD_TOKEN);
if (passwordTokenStartIndex < 0)
return sourceString;

return sourceString.substring(0, passwordTokenStartIndex + PASSWORD_TOKEN.length()) + OBFUSCATION_TOKEN;
}
}
33 changes: 27 additions & 6 deletions src/java/org/apache/cassandra/cql3/QueryEvents.java
Expand Up @@ -32,6 +32,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.cql3.statements.AuthenticationStatement;
import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.Message;
Expand All @@ -47,12 +48,20 @@ public class QueryEvents

private final Set<Listener> listeners = new CopyOnWriteArraySet<>();

private final PasswordObfuscator passwordObfuscator = new PasswordObfuscator();

@VisibleForTesting
public int listenerCount()
{
return listeners.size();
}

@VisibleForTesting
public PasswordObfuscator getObfuscator()
{
return passwordObfuscator;
}

public void registerListener(Listener listener)
{
listeners.add(listener);
Expand All @@ -72,8 +81,9 @@ public void notifyQuerySuccess(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.querySuccess(statement, query, options, state, queryTime, response);
listener.querySuccess(statement, possiblyObfuscatedQuery, options, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -90,8 +100,9 @@ public void notifyQueryFailure(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.queryFailure(statement, query, options, state, cause);
listener.queryFailure(statement, possiblyObfuscatedQuery, options, state, cause);
}
catch (Throwable t)
{
Expand All @@ -109,8 +120,9 @@ public void notifyExecuteSuccess(CQLStatement statement,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.executeSuccess(statement, query, options, state, queryTime, response);
listener.executeSuccess(statement, possiblyObfuscatedQuery, options, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -128,8 +140,9 @@ public void notifyExecuteFailure(QueryHandler.Prepared prepared,
String query = prepared != null ? prepared.rawCQLStatement : null;
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.executeFailure(statement, query, options, state, cause);
listener.executeFailure(statement, possiblyObfuscatedQuery, options, state, cause);
}
catch (Throwable t)
{
Expand Down Expand Up @@ -204,8 +217,9 @@ public void notifyPrepareSuccess(Supplier<QueryHandler.Prepared> preparedProvide
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(prepared.statement, query) : query;
for (Listener listener : listeners)
listener.prepareSuccess(prepared.statement, query, state, queryTime, response);
listener.prepareSuccess(prepared.statement, possiblyObfuscatedQuery, state, queryTime, response);
}
catch (Throwable t)
{
Expand All @@ -225,8 +239,9 @@ public void notifyPrepareFailure(@Nullable CQLStatement statement, String query,
{
try
{
final String possiblyObfuscatedQuery = listeners.size() > 0 ? possiblyObfuscateQuery(statement, query) : query;
for (Listener listener : listeners)
listener.prepareFailure(statement, query, state, cause);
listener.prepareFailure(statement, possiblyObfuscatedQuery, state, cause);
}
catch (Throwable t)
{
Expand All @@ -235,6 +250,12 @@ public void notifyPrepareFailure(@Nullable CQLStatement statement, String query,
}
}

private String possiblyObfuscateQuery(CQLStatement statement, String query)
{
// Statement might be null as side-effect of failed parsing, originates from QueryMessage#execute
return null == statement || statement instanceof AuthenticationStatement ? passwordObfuscator.obfuscate(query) : query;
}

public boolean hasListeners()
{
return !listeners.isEmpty();
Expand Down
37 changes: 29 additions & 8 deletions test/unit/org/apache/cassandra/audit/AuditLoggerAuthTest.java
Expand Up @@ -32,12 +32,14 @@
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.exceptions.AuthenticationException;
import com.datastax.driver.core.exceptions.SyntaxError;
import com.datastax.driver.core.exceptions.UnauthorizedException;
import org.apache.cassandra.OrderedJUnit4ClassRunner;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.OverrideConfigurationLoader;
import org.apache.cassandra.config.ParameterizedClass;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.PasswordObfuscator;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.EmbeddedCassandraService;

Expand Down Expand Up @@ -80,8 +82,8 @@ public static void setup() throws Exception
embedded.start();

executeWithCredentials(
Arrays.asList(getCreateRoleCql(TEST_USER, true, false),
getCreateRoleCql("testuser_nologin", false, false),
Arrays.asList(getCreateRoleCql(TEST_USER, true, false, false),
getCreateRoleCql("testuser_nologin", false, false, false),
"CREATE KEYSPACE testks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}",
"CREATE TABLE testks.table1 (key text PRIMARY KEY, col1 int, col2 int)"),
"cassandra", "cassandra", null);
Expand Down Expand Up @@ -127,6 +129,21 @@ public void testCqlCreateRoleAuditing()
createTestRole();
}

@Test
public void testCqlCreateRoleSyntaxError()
{
String createTestRoleCQL = String.format("CREATE ROLE %s WITH LOGIN = %s ANDSUPERUSER = %s AND PASSWORD",
TEST_ROLE, true, false) + CASS_PW;
String createTestRoleCQLExpected = String.format("CREATE ROLE %s WITH LOGIN = %s ANDSUPERUSER = %s AND PASSWORD",
TEST_ROLE, true, false) + PasswordObfuscator.OBFUSCATION_TOKEN;

executeWithCredentials(Arrays.asList(createTestRoleCQL), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
assertTrue(getInMemAuditLogger().size() > 0);
AuditLogEntry logEntry = getInMemAuditLogger().poll();
assertLogEntry(logEntry, AuditLogEntryType.REQUEST_FAILURE, createTestRoleCQLExpected, CASS_USER);
assertEquals(0, getInMemAuditLogger().size());
}

@Test
public void testCqlALTERRoleAuditing()
{
Expand All @@ -135,7 +152,7 @@ public void testCqlALTERRoleAuditing()
executeWithCredentials(Arrays.asList(cql), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
assertTrue(getInMemAuditLogger().size() > 0);
AuditLogEntry logEntry = getInMemAuditLogger().poll();
assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, cql, CASS_USER);
assertLogEntry(logEntry, AuditLogEntryType.ALTER_ROLE, "ALTER ROLE " + TEST_ROLE + " WITH PASSWORD" + PasswordObfuscator.OBFUSCATION_TOKEN, CASS_USER);
assertEquals(0, getInMemAuditLogger().size());
}

Expand Down Expand Up @@ -231,6 +248,10 @@ private static void executeWithCredentials(List<String> queries, String username
{
//no-op, taken care by caller
}
catch (SyntaxError se)
{
// no-op, taken care of by caller
}
}

if (expectedType != null)
Expand Down Expand Up @@ -274,19 +295,19 @@ private static void assertSource(AuditLogEntry logEntry, String username)
assertEquals(username, logEntry.getUser());
}

private static String getCreateRoleCql(String role, boolean login, boolean superUser)
private static String getCreateRoleCql(String role, boolean login, boolean superUser, boolean isPasswordObfuscated)
{
return String.format("CREATE ROLE IF NOT EXISTS %s WITH LOGIN = %s AND SUPERUSER = %s AND PASSWORD = '%s'",
role, login, superUser, TEST_PW);
String baseQueryString = String.format("CREATE ROLE IF NOT EXISTS %s WITH LOGIN = %s AND SUPERUSER = %s AND PASSWORD", role, login, superUser);
return isPasswordObfuscated ? baseQueryString + PasswordObfuscator.OBFUSCATION_TOKEN : baseQueryString + String.format(" = '%s'", TEST_PW);
}

private static void createTestRole()
{
String createTestRoleCQL = getCreateRoleCql(TEST_ROLE, true, false);
String createTestRoleCQL = getCreateRoleCql(TEST_ROLE, true, false, false);
executeWithCredentials(Arrays.asList(createTestRoleCQL), CASS_USER, CASS_PW, AuditLogEntryType.LOGIN_SUCCESS);
assertTrue(getInMemAuditLogger().size() > 0);
AuditLogEntry logEntry = getInMemAuditLogger().poll();
assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, createTestRoleCQL, CASS_USER);
assertLogEntry(logEntry, AuditLogEntryType.CREATE_ROLE, getCreateRoleCql(TEST_ROLE, true, false, true), CASS_USER);
assertEquals(0, getInMemAuditLogger().size());
}
}

0 comments on commit f978bea

Please sign in to comment.