Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
7.0
* Support pluggable default role initialization (CASSANDRA-21546)
* Add prepared statement cache stats to nodetool info (CASSANDRA-14366)
* Don't increment client metrics on messaging service connection unpause (CASSANDRA-21491)
* Add nodetool getreplicas (CASSANDRA-17665)
* Implementation of CEP-49: Hardware-accelerated compression (CASSANDRA-20975)
* Avoid using ObjectUtils.getFirstNonNull in Schema (CASSANDRA-21394)
* Allow nodetool garbagecollect to take a user defined list of SSTables (CASSANDRA-16767)
* Add a guardrail for misprepared statements (CASSANDRA-21139)
6.0-alpha3
* Enable trickle fsync by default (CASSANDRA-21572)
* Protect uncaught_exceptions virtual table against the crash path (CASSANDRA-21578)
Expand Down
25 changes: 25 additions & 0 deletions NEWS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,31 @@ restore snapshots created with the previous major version using the
'sstableloader' tool. You can upgrade the file format of your snapshots
using the provided 'sstableupgrade' tool.

7.0
===

New features
------------

- CEP-49 - it is possible to implement custom compression providers, e.g.
integrating with a specialized hardware which makes de/compression faster.
See CASSANDRA-20975 for more information.
- It is now possible to bootstrap a cluster's initial superuser role without
creating the 'cassandra/cassandra' password, by configuring 'default_role_initializer'
in cassandra.yaml. The new MutualTlsDefaultRoleInitializer creates the role
passwordless and maps a client certificate identity onto it instead, for
deployments that authenticate over mutual TLS. PasswordDefaultRoleInitializer
remains the default and produces the historical behaviour. See CASSANDRA-21546
for more information.

Upgrading
---------

- trickle_fsync is by default set to "true" instead of "false" in cassandra.yaml (see CASSANDRA-21572)

Deprecation
-----------


6.0
===
Expand Down
20 changes: 20 additions & 0 deletions conf/cassandra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,26 @@ role_manager:
# invalid_role_disconnect_task_period: 4h
# invalid_role_disconnect_task_max_jitter: 1h

# Creates the initial role on a cluster which has no roles yet, implementing IDefaultRoleInitializer.
# Most functions of the IRoleManager require an authenticated login, so a cluster with no roles has no way
# to create the first one; this option controls how that role is bootstrapped.
#
# Defaults to PasswordDefaultRoleInitializer, which creates a 'cassandra' superuser whose password is also
# 'cassandra'. That password is a published constant, so deployments using it must rotate or drop the role
# before the native transport is reachable.
#
# MutualTlsDefaultRoleInitializer instead creates the role with no password at all and maps a client
# certificate identity onto it, so there is no credential to guess. It requires an authenticator supporting
# mutual TLS, such as MutualTlsAuthenticator or MutualTlsWithPasswordFallbackAuthenticator.
#
# default_role_initializer:
# class_name: PasswordDefaultRoleInitializer # or: MutualTlsDefaultRoleInitializer
# parameters:
# role: cassandra
# password: cassandra # PasswordDefaultRoleInitializer: plaintext password
# # password_hash: "$2a$04$wsvzFamDJPDrTwMjgfcgpO.mKc.CMEuHBFZSjhGz2Ts6.v8PUO2rC" # ...or a bcrypt hash instead
# # identity: "spiffe1" # MutualTlsDefaultRoleInitializer: cert identity to map

# Network authorization backend, implementing INetworkAuthorizer; used to restrict user
# access to certain DCs
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllNetworkAuthorizer,
Expand Down
20 changes: 20 additions & 0 deletions conf/cassandra_latest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,26 @@ role_manager:
invalid_role_disconnect_task_period: 4h
invalid_role_disconnect_task_max_jitter: 1h

# Creates the initial role on a cluster which has no roles yet, implementing IDefaultRoleInitializer.
# Most functions of the IRoleManager require an authenticated login, so a cluster with no roles has no way
# to create the first one; this option controls how that role is bootstrapped.
#
# Defaults to PasswordDefaultRoleInitializer, which creates a 'cassandra' superuser whose password is also
# 'cassandra'. That password is a published constant, so deployments using it must rotate or drop the role
# before the native transport is reachable.
#
# MutualTlsDefaultRoleInitializer instead creates the role with no password at all and maps a client
# certificate identity onto it, so there is no credential to guess. It requires an authenticator supporting
# mutual TLS, such as MutualTlsAuthenticator or MutualTlsWithPasswordFallbackAuthenticator.
#
# default_role_initializer:
# class_name: PasswordDefaultRoleInitializer # or: MutualTlsDefaultRoleInitializer
# parameters:
# role: cassandra
# password: cassandra # PasswordDefaultRoleInitializer: plaintext password
# # password_hash: "$2a$04$wsvzFamDJPDrTwMjgfcgpO.mKc.CMEuHBFZSjhGz2Ts6.v8PUO2rC" # ...or a bcrypt hash instead
# # identity: "spiffe1" # MutualTlsDefaultRoleInitializer: cert identity to map

# Network authorization backend, implementing INetworkAuthorizer; used to restrict user
# access to certain DCs
# Out of the box, Cassandra provides org.apache.cassandra.auth.{AllowAllNetworkAuthorizer,
Expand Down
43 changes: 43 additions & 0 deletions doc/modules/cassandra/pages/managing/operating/security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,49 @@ information in the tables of the `system_auth` keyspace.

See also the xref:cassandra:developing/cql/security.adoc#database-roles[`CQL documentation on roles`].

[[default-role-initialization]]
=== Bootstrapping the default role

`IRoleManager` requires an authenticated login, so a freshly started cluster
with no roles yet has no way to create the first one. This bootstrap step is
pluggable and is configured by using `default_role_initializer` setting in
`cassandra.yaml`.

By default, Cassandra uses `PasswordDefaultRoleInitializer`, which creates a
`cassandra` superuser whose password is also `cassandra` - reproducing
Cassandra's historical bootstrap behaviour. Because that password is published,
well-known constant, any deployment relying on this default must rotate or disable
the `cassandra` role before the native transport is reachable by anyone other than
the operator performing this setup.

[source,yaml]
----
default_role_initializer:
class_name: MutualTlsDefaultRoleInitializer
parameters:
role: cassandra
identity: "spiffe://example.org/cassandra-admin"
----

This requires an authenticator that supports mutual TLS, such as a `MutualTlsAuthenticator`
or `MutualTlsWithPasswordFallbackAuthenticator` - Cassandra refuses to start otherwise,
since the role it would create could never be logged into.

`PasswordDefaultRoleInitializer` also accepts `role`, `password` and `password_hash`
parameters, to choose a different default role name or avoid the known default password
entirely.

[source,yaml]
----
default_role_initializer:
class_name: PasswordDefaultRoleInitializer
parameters:
role: cassandra
password_hash: "$2a$04$wsvzFamDJPDrTwMjgfcgpO.mKc.CMEuHBFZSjhGz2Ts6.v8PUO2rC"
----

See `conf/cassandra.yaml` for full list of supported parameters.

== Authentication

Authentication is pluggable in Cassandra and is configured using the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.RequestExecutionException;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.tcm.ClusterMetadata;

import static org.apache.cassandra.auth.AuthUtils.escapeCqlLiteral;

public abstract class AbstractDefaultRoleInitializer implements IDefaultRoleInitializer
{
private static final Logger logger = LoggerFactory.getLogger(AbstractDefaultRoleInitializer.class);

@Override
public final void initializeDefaultRoleIfNeeded()
{
if (ClusterMetadata.current().tokenMap.tokens().isEmpty())
throw new IllegalStateException(getClass().getSimpleName() + " skipped role setup: no known tokens in the ring");

try
{
if (!hasExistingRoles())
createDefaultRole();
}
catch (RequestExecutionException e)
{
logger.warn("{} skipped default role setup: some nodes were not ready", getClass().getSimpleName());
throw e;
}
}

@Override
public final boolean hasExistingRoles()
{
// Try looking up the configured default role first, to avoid the range query if possible.
String defaultRoleQuery = String.format("SELECT * FROM %s.%s WHERE role = '%s'", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES, escapeCqlLiteral(defaultRoleName()));
String allUsersQuery = String.format("SELECT * FROM %s.%s LIMIT 1", SchemaConstants.AUTH_KEYSPACE_NAME, AuthKeyspace.ROLES);
return !QueryProcessor.process(defaultRoleQuery, ConsistencyLevel.ONE).isEmpty()
|| !QueryProcessor.process(defaultRoleQuery, ConsistencyLevel.QUORUM).isEmpty()
|| !QueryProcessor.process(allUsersQuery, ConsistencyLevel.QUORUM).isEmpty();
}
}
37 changes: 36 additions & 1 deletion src/java/org/apache/cassandra/auth/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,23 @@ public static void applyAuth()

DatabaseDescriptor.setAuthorizer(authorizer);

// default role initializer: bootstraps the first role on a cluster which has none yet. Instantiated
// before the role manager because the role manager depends on it (see IRoleManager#defaultRoleInitializer).

IDefaultRoleInitializer defaultRoleInitializer = authInstantiate(conf.default_role_initializer,
IDefaultRoleInitializer.class,
PasswordDefaultRoleInitializer.instance);
DatabaseDescriptor.setDefaultRoleInitializer(defaultRoleInitializer);

// role manager

IRoleManager roleManager = authInstantiate(conf.role_manager, IRoleManager.class, CassandraRoleManager.class);

if (authenticator instanceof PasswordAuthenticator && !(roleManager instanceof CassandraRoleManager))
throw new ConfigurationException(authenticator.getClass().getName() + " requires " + CassandraRoleManager.class.getName(), false);

validateDefaultRoleInitializerSupportsRoleManager(conf.default_role_initializer, defaultRoleInitializer, roleManager);

DatabaseDescriptor.setRoleManager(roleManager);

// authenticator
Expand Down Expand Up @@ -136,18 +146,33 @@ public static void applyAuth()
authenticator.validateConfiguration();
authorizer.validateConfiguration();
roleManager.validateConfiguration();
defaultRoleInitializer.validateConfiguration();
networkAuthorizer.validateConfiguration();
cidrAuthorizer.validateConfiguration();
DatabaseDescriptor.getInternodeAuthenticator().validateConfiguration();
}

private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, Class<? extends T> defaultCls) {
@VisibleForTesting
static void validateDefaultRoleInitializerSupportsRoleManager(ParameterizedClass configuredInitializer,
IDefaultRoleInitializer defaultRoleInitializer,
IRoleManager roleManager)
{
boolean explicitlyConfigured = configuredInitializer != null && configuredInitializer.class_name != null;
if (explicitlyConfigured && !defaultRoleInitializer.supportsRoleManager(roleManager))
throw new ConfigurationException(defaultRoleInitializer.getClass().getName() + " does not support " + roleManager.getClass().getName(), false);
}

private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, Class<? extends T> defaultCls)
{
if (authCls != null && authCls.class_name != null)
{
String authPackage = AuthConfig.class.getPackage().getName();
return ParameterizedClass.newInstance(authCls, List.of("", authPackage), expectedType);
}

if (defaultCls == null)
return null;

// for now, this has to stay and can not be replaced by ParameterizedClass.newInstance as above
// due to that failing for simulator dtests. See CASSANDRA-20450 for more information.
try
Expand All @@ -159,4 +184,14 @@ private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expect
throw new ConfigurationException("Failed to instantiate " + defaultCls.getName(), e);
}
}

private static <T> T authInstantiate(ParameterizedClass authCls, Class<T> expectedType, T defaultInstance)
{
if (authCls != null && authCls.class_name != null)
{
String authPackage = AuthConfig.class.getPackage().getName();
return ParameterizedClass.newInstance(authCls, List.of("", authPackage), expectedType);
}
return defaultInstance;
}
}
72 changes: 72 additions & 0 deletions src/java/org/apache/cassandra/auth/AuthUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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.auth;

import org.apache.commons.lang3.StringUtils;
import org.mindrot.jbcrypt.BCrypt;

import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.ConsistencyLevel;

import static org.apache.cassandra.auth.PasswordDefaultRoleInitializer.DEFAULT_SUPERUSER_NAME;

public class AuthUtils
{
static final ConsistencyLevel DEFAULT_SUPERUSER_CONSISTENCY_LEVEL = ConsistencyLevel.QUORUM;

private AuthUtils() {}

public static String hashpw(String password)
{
return BCrypt.hashpw(password, PasswordSaltSupplier.get());
}

/**
* Escapes a value for safe interpolation into a single-quoted CQL string literal by doubling any
* single quotes it contains (e.g. {@code o'brien} becomes {@code o''brien}). Used when building auth
* DDL/DML from role, identity and resource names via {@link String#format}.
*
* @param name the raw value to escape; {@code null} is returned unchanged
* @return the value with every {@code '} doubled
*/
public static String escapeCqlLiteral(String name)
{
return StringUtils.replace(name, "'", "''");
}

/** Allows selective overriding of the consistency level for specific roles. */
public static ConsistencyLevel consistencyForRoleWrite(String role)
{
return defaultRoleName().equals(role) ? DEFAULT_SUPERUSER_CONSISTENCY_LEVEL : CassandraAuthorizer.authWriteConsistencyLevel();
}

public static ConsistencyLevel consistencyForRoleRead(String role)
{
return defaultRoleName().equals(role) ? DEFAULT_SUPERUSER_CONSISTENCY_LEVEL : CassandraAuthorizer.authReadConsistencyLevel();
}

private static String defaultRoleName()
{
IRoleManager roleManager = DatabaseDescriptor.getRoleManager();
IDefaultRoleInitializer initializer = roleManager != null
? roleManager.defaultRoleInitializer()
: DatabaseDescriptor.getDefaultRoleInitializer();
return initializer != null ? initializer.defaultRoleName() : DEFAULT_SUPERUSER_NAME;
}
}
Loading