Status
Current state: Under Discussion
Discussion thread: https://lists.apache.org/thread/1hs27lx2pw9lmp7rw499vn0m7vl2bgt1
JIRA:
Released: not released yet
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
Motivation
At the time of writing this CEP, Cassandra lacks a mechanism how to prevent a user from creating a password which does not follow a certain security policy. As of now, a password might be anything. While passwords for users might be generated as part of company processes and they should adhere to organisational password complexity policies, it does not mean that it is enforced from Cassandra itself. More to it, once a password is set, even though it might follow some security guidelines upon its creation, it might be changed afterwards to a password which is less secure by "alter role" statements. Last but not least, it is a good practice to change passwords every now and then to prevent unauthorised access when credentials are leaked unknowingly. However, upon changing a password, it should not be possible to reuse them - each new password for a user should be unique from the recent ones in order to not "recycle" them.
Secondly, if Cassandra asks for passwords to be of a certain form, it is up to a user to come up with such password. This might be quite tedious task to do - to make Cassandra happy - hence it would be beneficial if Cassandra had a way to generate passwords which would pass its self-imposed security policy so users / operators do not need to do that on their own.
We believe that all these problems are solvable and they are unnecessary and the complexity of these problems might be greatly reduced or completely bypassed.
Audience
This CEP is for users and operators of a cluster as well as for security departments and security officers.
Goals
- Introduce a way how to enforce password strength upon role creation or role alteration.
- Implement a reference implementation of a password validator which adheres to a recommended password strength policy, to be used for Cassandra users out of the box.
- Emit a warning (and proceed) or just reject "create role" and "alter role" statements when provided password does not meet certain security level, based on user configuration of Cassandra.
- To be able to implement a custom password validator with its own policy, whatever it might be, and provide a modular/pluggable mechanism to do so.
- To not be able to reuse same passwords which were previously set (up to some number of them, configurable), even they are otherwise valid.
- Provide a way for Cassandra to generate a password which would pass the subsequent validation for use by the user.
Non-Goals
It is not a goal to impose any restrictions on already existing passwords which were created before this CEP is introduced. In other words, we are not going to, for example, reject a user from logging in when his password is weak because it was already created before we had a chance to prevent that from happening.
It is also not a goal to pro-actively inform a user that his password is too old and it should be regenerated. This might be implemented as a third party, standalone extension, but it is not to be implemented as part of this CEP.
Implementation and proposed changes
Implementation-wise, we believe that the best approach is to leverage already existing framework in place - Guardrails. Guardrails are an excellent way how to achieve this functionality but we identified that it needs to be updated a little bit to fully support the proposed features.
While Guardrails are great to implement warning / error emitting upon an invalid password creation on alter or create role statements, they lack one very important functionality - plug-ability. Plug-ability is crucial because even if Cassandra provided a reference implementation of a password validator (discussed further), we can not possibly accommodate every possible security office of companies which just happen to have a different policy on what a characteristics a strong password should have. The requirements varies when it comes to what a valid password is, there does not exist one definition of what a strong password is. Hence, there might exist organisations for which the password policy varies in such a way that the out-of-the-box solution Cassandra provides is simply not enough. Rather than trying to come up with a one-size-fits-all validator, we want to implement a default one and if users do not find it sufficient, they can comfortably implement their own.
We believe that the default implementation will provide a validator which follows the basic and well known security standards, however, we can not (in principle) cover every possible requirement there is. We want to be able to provide a way how to implement specific validators which would differ from the default one in unpredictable ways.
Currently, every guardrail is configured by a single property in cassandra.yaml. We think that at the time of Guardrails design, it was seemed to be just sufficient to have a flat configuration approach, however, when it comes to a guardrail as password validator, this is simply not enough. The existing Guardrail design simply does not consider that guards themselves might be configurable. We identify this as a drawback.
Hence, we want to introduce custom guardrails .
CustomGuardrail is a guardrail extending Guardrail, which internally validates an arbitrary value based on its value validator which is an instance of a class which name is taken from cassandra.yaml from the respective guardrail configuration snippet.
A CustomValidator is a concrete implementation of ValueValidator abstract class which is meant to be called in guard method of a CustomGuardrail.
Plug-ability of CustomGuardrail
While we want to use Guardrails as a framework to implement the password validation guard, we also propose to add a mechanism to implement guardrails which would be pluggable.
Under plug-ability, we reference to a well known mechanism Cassandra already uses quite intensively - implementing an interface / extending an abstract class and putting such class on the class path so Cassandra will react on it and use it in runtime.
In the configuration (cassandra.yaml), this would be introduced:
# Guardrail to warn or fail when setting / altering a password.
password_validator:
# Implementation class of a validator. When not in form of FQCN, the
# package name org.apache.cassandra.db.guardrails.validators is prepended.
class_name: DefaultPasswordValidator
.... other configuration parameters of this guardrail
Secondly, we propose the introduction of CustomGuardrail class which just extends Guardrail. The main feature of a CustomGuardrail is that it is possible to dynamically plug-in a validator of values such guardrail is supposed to validate.
It is important to realise that we want to make CustomGuardrail generic enough to implement any other guardrail which would solve similar problems we discussed above. There is nothing specific about password validation in CustomGuardrail. It is validator-agnostic. Only the concrete implementation of CustomGuardrail would start to validate values in some specific way.
It is also important to note that already existing guardrails would not be changed in any way.
We believe that with flat guardrails configuration, it is not possible to achieve the desired plug-ability and organisational compliance for an operator, hence user experience would suffer. If every password guardrail option was in a flat structure as it is now, people wanting to implement their own password validators (or any guardrails for that matter) would have to cope with all the configuration properties which suddenly became obsolete and redundant and which are being activated even it is not needed (when not commented out). Not to mention a solution with that many configuration options would be extremely difficult to maintain.
We acknowledge the fact that password validator as guardrail which is supposed to be configurable / pluggable is rather a corner case scenario to cover, however, we do not think that by the introduction of this feature we are abusing the current implementation. The very opposite is true. Guardrail framework proved to be a solid foundation on top of which we want to base our solution.
Activation of password guardrail
When the above snippet for password_validator is commented out, it means that no password validator will be in place. Actually, there would be still password validation in effect, but such validation would consider every password to be valid.
When the snippet above is commented out, it is equal to this configuration. NoOpValidator is validator which does nothing.
password_validator:
class_name: org.apache.cassandra.db.guardrails.validator.NoOpValidator
The very same mechanism would be in place for every other CustomGuardrail.
If the class_name parameter is not present, NoOpValidator will be used. If class_name is present but it is not able to be instantiated as an object of such class (or class is missing on the class path), an exception will be thrown upon start of a node, not in runtime.
ValueValidator
The skeleton of abstract ValueValidator would look like this (omitting other implementation details)
public abstract class ValueValidator<VALUE>
{
private final CustomGuardrailConfig config;
public ValueValidator(CustomGuardrailConfig config)
{
this.config = config;
}
public abstract Optional<String> shouldWarn(@Nonnull List<VALUE> oldValues, VALUE newValue);
public abstract Optional<String> shouldFail(@Nonnull List<VALUE> oldValues, VALUE newValue);
public abstract void validateParameters() throws ConfigurationException;
@Nonnull
public CustomGuardrailConfig getParameters()
{
return config;
}
}
shouldWarn and shoudFail return Optional<String>. If Optional is empty, it means that validator considers newValue to be valid in such a way that it should not emit any warnings. If Optional is not empty, it should contain an informational message telling why validator thinks that newValue is not valid and deemed to emit a warning. The same logic holds for shouldFail method.
ValueValidator is not an interface but an abstract class because we want implementors to provide a class into which we inject configuration for it so a validator can configure itself before validation is in action.
CustomGuardrailConfig extends HashMap.
CustomGuardrail
CustomGuardrail extends Guardrail and instantiates a ValueValidator on its instantiation, in the constructor:
public class CustomGuardrail<VALUE> extends Guardrail
{
public CustomGuardrail(String name, CustomGuardrailConfig config)
{
super(name);
this.validator = new AtomicReference<>(ValueValidator.getValidator(name, config));
}
... other methods
}
The implementation of the guard method of CustomGuardrail looks like this:
/**
* @param oldValues the list of previous values
* @param newValue value to validate by the validator of this guardrail
* @param state client's state
*/
public void guard(@Nullable List<VALUE> oldValues, VALUE newValue, ClientState state)
{
if (!enabled(state))
return;
ValueValidator<VALUE> currentValidator = validator.get();
Optional<String> failMessage = currentValidator.shouldFail(oldValues, newValue);
if (failMessage.isPresent())
fail(failMessage.get(), state);
else
currentValidator.shouldWarn(oldValues, newValue).ifPresent(this::warn);
}
"validator" variable is field in CustomGuardrail class. "validator" is AtomicReference.
Reconfiguration of custom guardrails
Guardrails which are already in place are possible to be reconfigured on runtime via JMX. Hence, similarly, custom guardrails should be reconfigurable as well. Our CustomGuardrail implementation enables reconfiguration of their validators in runtime.
When it comes to password validator, while custom guardrails are reconfigurable as such, we do not want to have it reconfigurable. The reconfiguration of password validators in runtime seems to be not desirable, the primary reason for this is we want to prevent rogue actors to lower the security standards intentionally. The enablement of reconfigurability is not up to an implementation of a custom guardrail itself. It is rather done by not exposing a respective JMX method on GuardrailsMBean which acts as way to achieve that. If implementors of any other custom guardrail need to have it reconfigurable in runtime, they are welcomed to do so by providing respective JMX methods on GuardrailsMBean and by calling "reconfigure" method on CustomGuardrail like this:
This is method of CustomGuardrail class:
/**
* Reconfigures this custom guardrail. After the successful finish of this method, every
* new call to this guardrail will use new configuration for its validator.
* <p>
* New configuration is merged into the old one. Values for the keys in the old configuration
* are replaced by the values of the same key in the new configuration.
*
* @param newConfig if null or the configuration is an empty map, no reconfiguration happens.
*/
void reconfigure(@Nullable Map<String, Object> newConfig)
{
if (newConfig == null || newConfig.isEmpty())
return;
Map<String, Object> mergedMap = new HashMap<>(validator.get().getParameters());
mergedMap.putAll(newConfig);
ValueValidator<VALUE> newValidator = ValueValidator.getValidator(name, new CustomGuardrailConfig(mergedMap));
validator.set(newValidator);
}
We want to be able to reconfigure a validator in such a way that we might override only parameters which we want to change and leave other configuration parameters intact. ValueValidator.getValidator is a static method which instantiates a validator by reflection taking the value of class_name parameter. "validator" variable is field in CustomGuardrail class. "validator" is AtomicReference. By this way, we can just switch one validator for another in runtime. Once switched, every subsequent validation will use new validator.
Validation of a new value against the previous values
In Goals section, we proposed that a password validator should reject passwords which were already used in the past. Before modeling the solution to this problem, we think that it is necessary to be able to keep some kind of a state. One may argue that by holding or knowing previous values, a validator suddenly became stateful. We do not think this is true, the guardrail class itself is not stateful. It just happens to (optionally) retrieve previous values it validated.
Naturally, when a password is changed and we want to prevent to set a new password which would be the same as the previous n passwords, logically, we need to somehow keep track of them and we need it to survive node's reboot.
If a value is valid, it is put into the history. In order to evaluate if a new value (new password) is valid or not (equal or not to one of the previously set ones), we need to get all the previous values which were valid and reject or pass the current validation based on these values.
This is highly specific to a custom guard rail. Not every guardrail needs to use this functionality. We propose that CustomGuardrail would have a method called "save", which does nothing by default and it is up to an implementor to fill the details if necessary. An implementor also has to explicitly call this method, it is not called by Guardrails internals automatically.
/**
* Persists a state after this guardrail is invoked. This is guardrail-specific
* as some guardrails do not need to persist anything but other do. The most typical
* usecase would be to persist old value, so it is available upon next guardrail invocation when a new value is
* being validated. The default implementation does not do anything. This method is meant to be called after
* guard method has finished, either errorneously or not.
*
* @param state client's state
* @param args abritrary arguments a caller can specify to use upon persistence
*/
public void save(ClientState state, Object... args)
{
}
Similarly, we need to have some way to retrieve old values and provide them to shouldWarn and shouldFail methods of a ValueValidator:
/**
* Returns historical values this validator successfully validated. The actual values to be returned
* are up to implementator of a guardrail to resolve. By default, this method returns an empty, immutable list.
*
* @param args arguments necessary for the retrieval of historical values
* @return historical values this validator already successfully validated
*/
public List<VALUE> retrieveHistoricalValues(Object... args)
{
return Collections.emptyList();
}
Validation of a new password against previous passwords
We propose to introduce a helper table into system_auth keyspace called previous_passwords with the following schema:
CREATE TABLE system_auth.previous_passwords (
role text,
created timeuuid,
salted_hash text,
PRIMARY KEY (role, created)
) WITH CLUSTERING ORDER BY (created ASC)
After I changed the password 5 times for a role 'stefan', there would be these entries in that table:
cassandra@cqlsh> select * from system_auth.previous_passwords ;
role | created | salted_hash
---------+--------------------------------------+--------------------------------------------------------------
stefan | 2da578c0-343a-11ed-a1df-0114d34213ee | $2a$10$JldE3bAeus9HAX0bSibYHerXpBYqqygmOoAtSUJyUqA./99JXykqy
stefan | 32aba290-343a-11ed-a1df-0114d34213ee | $2a$10$0kNxuXxoZWyjIBvBYw7j2uYl.ZBOcht9JQFTg/F18.VgNkczX7iH2
stefan | 34cb2730-343a-11ed-a1df-0114d34213ee | $2a$10$43kebtKvoBeojrCFzCRUDO0aDajpE6bO7PbpoLqFmj.LRtrhEgoOW
stefan | 36465580-343a-11ed-a1df-0114d34213ee | $2a$10$wvrzqbpyt2MJE4otxyqsveaIJIUhr8AolmTEnwVLWiFzNZNKGYC/6
stefan | 4d04c860-343a-11ed-a1df-0114d34213ee | $2a$10$Tvk6Yws9ydbcz.WfZZrx3.pmF6rhbpOq09WzLtHkhNgBSLs0v7Ihi
(5 rows)
When a user is about to change a password for a role 'stefan' for the sixth time, two things happen:
1) it will validate that the password is valid as such
2) It will check the proposed password against previously set ones (the current password included). The password from the request is hashed and if it is same as the one from previously set passwords (or same as the current one), the alter statement will fail. When it is not equal to any of them, it passes.
3) When the number of rows for a particular role is bigger than a configured value (by default 5), it will remove the oldest password hash. If the number of rows is lower than the limit, it will add new entry. Hence, in effect, a user can re-use a password only after he used 5 other passwords previously. We may increase this to e.g. 10, 20 or any number for that matter.
The logical extension of this approach would be that a user should not able to change his password too often, if we find it necessary. For example, if a user wants to indeed have the very same password as he had, all it takes is to create 5 other passwords and then set the sixth one which is equal to the first one. We believe that this is hardly going to happen in practice. Setting the history to e.g. 20 would discourage people to set 20 different passwords (which all needs to be valid according to complex rules) just to be able to use the same one. If we were about to make this even harder to bypass, we may say that password can be changed once per day, for example (anytime for a superuser). Since we have "created" column which is of type timeuuid, we would check this table and see if there was some password already set that day or not and fail the request eventually. This is not the part of the initial implementation.
The proposed table above has such a schema that it would be possible to ask a question like "what was the last time a user changed his password?" and notify a user that he should regenerate it.
Validation of already hashed passwords
We are not going to validate already hashed passwords which are specified in HASHED PASSWORD clause in CQL. However, it is still possible to validate that such hash was not used previously so historical validation would still work (because previous_passwords table holds hashes as well). We can not validate hashed passwords on any characteristics because it is not a plain text password.
As another option, for preventing a user to create weak hashed passwords to bypass the password strength validator, we propose to introduce a guardrail which would prevent hashed passwords to be set when a custom (and no non-op) password guardrail is activated.
Implementation of DefaultPasswordValidator
The default implementation of a password validator will consider a password to be valid under these conditions:
- it has to be at least 12 characters long (configurable) to not emit any warnings
- it has to be longer than 8 characters (configurable) to not emit any failures
- it can not contain illegal sequences of a specific length (configurable), for example, if there is a password like "abcdefgh", that password is invalid
- it can not contain whitespaces
- it has to fulfil n out of these 4 characteristics, number of characters per characteristic is again configurable both for warning and failure thresholds
- contains upper case characters
- contains lower case characters
- contains digits
- contains special characters (only ascii chars)
Guardrails act around two thresholds - warning and failure. Failure rejects the statement, warning just emits a warning into cqlsh but it passes. The same logic is applied here. For example:
- if a password is longer than 12 characters, it does not emit a warning nor a failure
- if a password is shorter than 12 but longer than 8 characters, it emits a warning.
- if a password is shorter than 8 characters, it will emit failure.
For characteristics (lower case, upper case ...), it has to fulfil at least 3 characteristics to not emit error and it has to fulfil all 4 characteristics to not emit warning.
Incorporating password guardrail into AuthenticationStatements
We want to plug the password validation mechanism into three authentication statements. The logic involved when it comes to passwords which are already hashed by using ... HASHED PASSWORD = 'hash' is not shown here for brevity. Please look at the reference implementation to see it all.
CreateRoleStatement
we fail (or emit warning) on this statement when password is not valid and we save the password when it is valid into system_auth.previous_passwords table.
public ResultMessage execute(ClientState state)
{
opts.getPassword().ifPresent(password -> Guardrails.password.guard(password, state));
// current code, here create a role when validation of password passes
// after role is created, save the password into previous_passwords table
opts.getPassword().ifPresent(password -> Guardrails.password.save(state,
role.getRoleName()),
getSaltedHash(role.getRoleName())));
return null;
}
AlterRoleStatement
We fail altering of a role when alter statement contains a password which is not valid. If we happen to have a history of previously set passwords, we retrieve all passwords for a role to be altered and we validate a password to be set against all the previous ones. If a new password is not among the old ones, we proceed, otherwise we fail.
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
opts.getPassword().ifPresent(password -> {
if (Guardrails.password.isValidatingAgainstHistoricalValues())
Guardrails.password.guard(Guardrails.password.retrieveHistoricalValues(role.getRoleName()), password, state);
else
Guardrails.password.guard(password, state);
});
/// the current code is here
opts.getPassword().ifPresent(password -> Guardrails.password.save(state,
role.getRoleName(),
getSaltedHash(role.getRoleName())));
return null;
}
DropRoleStatement
Upon dropping of a role, we need to clear the history of passwords that role had.
public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
{
// current code is here
QueryProcessor.execute(format("DELETE FROM %s.%s WHERE role = ?", AUTH_KEYSPACE_NAME, PREVIOUS_PASSWORDS),
authWriteConsistencyLevel(),
role.getRoleName());
return null;
}
Configuration parameters of reference implementation
this is meant to be all commented out on commit so NoOpValidator will be used.
# Guardrail to warn or fail when setting / altering a password.
password_validator:
# Implementation class of a validator. When not in form of FQCN, the
# package name org.apache.cassandra.db.guardrails.validators is prepended.
class_name: DefaultPasswordValidator
# If set to true, alter role statements which are changing passwords
# will be checking if that password was not already used in the past.
# Password validation fails if it was used already. Defaults to false.
validate_against_historical_values: true
# Maximum previous passwords to validate a password to be altered against
# If a password to validate is found among the last 5, the validation will fail.
max_historical_values: 5
# There are four characteristics:
# upper-case, lower-case, special character and digit.
# If this value is set e.g. to 3, a password has to consist of 3 out of 4 characteristics.
# For example, it has to contain at least 2 upper-case characters, 2 lower-case, and 2 digits to pass,
# but it does not have to contain any special characters.
# if number of characteristics found in the password is less than 4, it will emit warning
# if number of characteristics found in the password is less than 3, it will emit failure
min_characteristics_warn: 4
min_characteristics_fail: 3
# If password is shorter than this value, the validator will emit a warning.
min_length_warn: 12
# If a password is shorter than this value, the validator will emit a failure.
min_length_fail: 8
# If a password does not contain at least n upper-case characters, the validator will emit a warning.
min_upper_case_chars_warn: 2
# If a password does not contain at least n upper-case characters, the validator will emit a failure.
min_upper_case_chars_fail: 1
# If a password does not contain at least n lower-case characters, the validator will emit a warning.
min_lower_case_chars_warn: 2
# If a password does not contain at least n lower-case characters, the validator will emit a failure.
min_lower_case_chars_fail: 1
# If a password does not contain at least n digits, the validator will emit a warning.
min_digits_chars_warn: 2
# If a password does not contain at least n digits, the validator will emit a failure.
min_digits_chars_fail: 1
# If a password does not contain at least n special characters, the validator will emit a warning.
min_special_chars_warn: 2
# If a password does not contain at least n special characters, the validator will emit a failure.
min_special_chars_fail: 1
# illegal sequences that will fail when they are >= 5 characters long
# illegal sequences might be either alphabetical (form 'abcde'),
# numerical (form '34567'), or US qwery (form 'asdfg').
illegal_sequence_length: 5
Generation of valid passwords
To create a password which would pass given validator is quite tedious task. Even more so when we want to regenerate them often and at the same time we do not want to reuse them. For that reason, it would be very handy to have some tooling which is able to generate them. We identified two ways how to do this.
There is already hash_password command line tools via which it would be possible to generate passwords based on a configuration in cassandra.yaml. It would work like this:
./tools/bin$ ./hash_password -g
OP0i:73.oH
./tools/bin$
In a nutshell, it would translate to this:
CustomGuardrailConfig config = DatabaseDescriptor.getGuardrailsConfig().getPasswordValidatorConfig();
ValueGenerator<String> generator = ValueGenerator.getGenerator("password", config);
String generatedPassword = generator.generate();
System.out.print(generatedPassword);
System.out.flush();
While practically possible, we do not think that this is a good idea. The tool is called "hash_password" and we are not hashing anything. We either rename this tool to something more appropriate or we create brand new tool just for password generation.
Secondly, passwords to be generated will be used in CQL shell directly most of the time when an administrator is about to create a role or when a user is about to change his password. It would be more appropriate if there was some way to generate password directly in CQLSH. We propose these modifications of create and alter authentication statements:
cqlsh> CREATE ROLE stefan WITH LOGIN = true AND GENERATED PASSWORD;
password
------------
Op0i:73.oH
(1 rows)
cqlsh> ALTER ROLE stefan WITH GENERATED PASSWORD;
password
------------
Lr5l:73&oM
(1 rows)
If a user wants to generate a password only, we suggest we add this new CQL statement:
cqlsh> GENERATE PASSWORD;
password
------------
Lr5l:73&oM
(1 rows)
There might be as well a version of generation command which would return hashed password as well even though its practical usage is questionable:
cqlsh> GENERATE HASHED PASSWORD;
password | salted_hash
------------+-------------------------
Lr5l:73&oM | where will be hash
(1 rows)
We will not include the last two commands into the initial implementation.
The advantage of this approach:
- We do not need to figure out the password to pass the validator on our own which can be quite tedious and time consuming task
- We do not need to hash any passwords and we do not need to use WITH HASHED PASSWORD clause
- We are not moving passwords nor hashes from a source to CQL shell, we do not copy it (if we don't create it directly in cqlsh for the first time)
- Passwords will not be visible in CQLSH history, only command will be there, but the password itself will never be disclosed in the history logs
- This also simplifies audit logging and obfuscation of the logs because they never appear anywhere.
Examples of validation
cassandra@cqlsh> alter role jackson8 WITH LOGIN = true and PASSWORD = 'B$a2$sd4d';
Warnings :
Guardrail password violated: Password was set, however it might not be strong enough according to the configured password strength policy. To resolve this warning, the following has to be done: Password must be 10 or more characters in length. Password must contain 2 or more uppercase characters. Password matches 3 of 4 character rules, but 4 are required.
cassandra@cqlsh> alter role jackson8 WITH LOGIN = true and PASSWORD = 'B$a2$sd4d';
InvalidRequest: Error from server: code=2200 [Invalid query] message="Guardrail password violated: Password was not set as it violated configured password strength policy. To resolve this error, the following has to be done: Password matches one of 5 previous passwords. "
cassandra@cqlsh> alter role jackson8 WITH LOGIN = true and PASSWORD = 'B$a2$sd4T';
Warnings :
Guardrail password violated: Password was set, however it might not be strong enough according to the configured password strength policy. To resolve this warning, the following has to be done: Password must be 12 or more characters in length.
cassandra@cqlsh> alter role jackson8 WITH LOGIN = true and PASSWORD = 'B$a2';
InvalidRequest: Error from server: code=2200 [Invalid query] message="Guardrail password violated: Password was not set as it violated configured password strength policy. To resolve this error, the following has to be done: Password must be 8 or more characters in length. "
Reference implementation
The reference implementation can be found in this branch: https://github.com/instaclustr/cassandra/tree/CEP-24-with-generator
There is working validation as well as generation implemented. The code contains both hash_password modifications as well as the changes for CQL statements (CREATE / ALTER role ... WITH GENERATED PASSWORD)
Compatibility, Deprecation, and Migration Plan
- Password validation can be turned on / off on demand (by default off but this may be further discussed)
- We do not need any migration tools.
It is questionable whether we should make password validator enabled by default. Currently, the default password for Cassandra is "cassandra" and that is hardly secure so we would have to make an exception for Cassandra user. However, we might turn the password validation on by default. All existing users, even they passwords would not pass the validator anymore, would be able to log in. It would affect only the alter statement or the creation of a new role which is acceptable.
Selection of password validation and generation library
For the default password validator and generation library we found only the Passay library. It is lightweight, fairly extensible, flexible and it not only validates passwords but it also generates them. It is able to implement custom rules and it provides quite exhaustive set of in-built rules to use if found appropriate which is crucial for further modifications and extensibility.
Passay is dual licensed under LGPL and Apache Licence 2.0. It is still actively developed. It has optional bindings to bouncycastle crypto library which is MIT. We do not need anything from bouncycastle to implement this CEP.
Passay was selected via many recommendations and online documentation, it seems to be the most popular password utility library in the Java ecosystem.
Existing solutions to password validation problem in other databases
MySQL takes very similar approach to ours. The validation of a password is done by installing validation plugin.
They are not validating hashed passwords, for exactly the same reasons as we do not: "Passwords specified as hashed values are not checked because the original password value is not available for checking." We are going even further here by checking historical hashes as well.
Additionally, they have three "complexity levels", LOW, MEDIUM and STRONG.
It is configurable by system properties. If not set, validation is practically avoided, same as in our implementation. The configuration is happening in the table: https://dev.mysql.com/doc/refman/5.6/en/validate-password-options-variables.html We do not think that having custom validators plays well with having its configuration in a table as we do not know what properties there might be.
The approach they took is that a password to be set might be checked and it returns a number from 0 to 100 (100 is the strongest password). We do not think this approach is any better than what we want to do. We have basically two levels - warning or error.
https://dev.mysql.com/doc/refman/5.6/en/validate-password.html
We consider our solution to be superior because MySQL plugin is not capable of (automatic) password generation based on password strength policies in CQLSH, there is not any historical validation and that "plugin" is not "pluggable" at all. We are also questioning the approach they took in general. Checking a password's strength before using it seems to be a redundant step which might be completely skipped when passwords are possible to be generated automatically by Cassandra itself upon role creation or alternation. We think that assignement of a score to a password is a result of them not being able to generate them.
Test Plan
This CEP is meant to be tested by standard way of JUnit tests.
Password Complexity
Password complexity is a very subjective topic, many organisations apply their own password (or passphrase) requirements for their users to follow. Due to many competing standards and opinions on the subject, we decided to used the US based National Institute of Standards and Technology (NIST) Special Publication 800-63B (https://pages.nist.gov/800-63-3/sp800-63b.html - See Appendix A) as a reference to develop this default password policy. NIST is highly regarded in the information security sector (as of writing), and as such it seemed reasonable to develop around their recommendation.