diff --git a/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md b/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md new file mode 100644 index 000000000..c47e358a6 --- /dev/null +++ b/docs/en/00_Getting_Started/01_Installation/How_To/MySQL_SSL_Support.md @@ -0,0 +1,169 @@ +title: MySQL SSL Support +summary: Setting up MySQL SSL certificates to work with Silverstripe + +# MySQL SSL Support: Why do I need it? + +In a typical Silverstripe set up, you will only need to use a single host to function as the web server, email server, database server, among others. + +In some cases, however, you may be required to connect to a database on a remote host. Connecting to a remote host without SSL encryption exposes your data to [packet sniffing](http://www.linuxjournal.com/content/packet-sniffing-basics) and may compromise the security of your Silverstripe instance. + +This article demonstrates how to generate SSL certificates using MySQL and implementing them in Silverstripe. + +
+This article assumes that you have `MySQL` and `OpenSSL` installed. +
+ + +## Generating Certificates + +There are three components to an SSL certificate implementation. The first two components are the ***private key***, and the ***public certificate***, which are mathematically-generated, symetrical pieces of the puzzle that allow [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) to work. The third component is the [Certificate Authority (CA) certificate](https://en.wikipedia.org/wiki/Certificate_authority) that signs the pubic key to prove its validity. + +In the case of MySQL, we will need to generate three sets of certificates, namely: + +- the CA key and certificate +- the server key and certificate +- the client key and certificate + +We also need to sign the certificates with our generated CA. + +The commands below illustrate how to do so on your MySQL host. + +
+The following commands will work on Linux/Unix based servers. For other servers such as windows, refer to the [MySQL documentation](https://dev.mysql.com/doc/refman/5.7/en/creating-ssl-files-using-openssl.html) +
+ + + :::bash + + # Create directory + sudo mkdir ssl + cd ssl + + # Generate CA key and CA cert + sudo openssl genrsa 2048 | sudo tee -a ca-key.pem + sudo openssl req -new -x509 -nodes -days 365000 -key ca-key.pem -out ca-cert.pem + + # Generate SERVER key and server certificate signing request + # IMPORTANT: the common name of the certificate should match the domain name of your host! + sudo openssl rsa -in server-key.pem -out server-key.pem + sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout server-key.pem -out server-req.pem + + # Generate and sign SERVER certificate + sudo openssl x509 -req -in server-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem + + # Generate CLIENT key and certificate signing request + sudo openssl rsa -in client-key.pem -out client-key.pem + sudo openssl req -newkey rsa:2048 -days 365000 -nodes -keyout client-key.pem -out client-req.pem + + # Generate and sign CLIENT certificate + sudo openssl x509 -req -in client-req.pem -days 365000 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem + + # Verify validity of generated certificates + sudo openssl verify -CAfile ca-cert.pem server-cert.pem client-cert.pem + +
+After generating the certificates, make sure to set the correct permissions to prevent unauthorized access to your keys! + +It is critical that the key files (files ending in *key.pem) are kept secret. Once these files are exposed, you will need to regenerate the certificates to prevent exposing your data traffic. +
+ + :::bash + # Set permissions readonly permissions and change owner to root + sudo chown root:root *.pem + sudo chmod 440 *.pem + + # Server certificates need to be readable by mysql + sudo chgrp mysql server*.pem + sudo mv *.pem /etc/mysql/ssl + + +## Setting up MySQL to use SSL certificates + +
+For Debian/Ubuntu instances, the configuration file is usually in `/etc/mysql/my.cnf`. Refer to your MySQL manual for more information +
+ +We must edit the MySQL configuration to use the newly generated certificates. + +Edit your MySQL configuration file as follows. + + + [mysqld] + ... + ssl-ca=/etc/mysql/ca-cert.pem + ssl-cert=/etc/mysql/server-cert.pem + ssl-key=/etc/mysql/server-key.pem + + # IMPORTANT! When enabling MySQL remote connections, make sure to take adequate steps to secure your machine from unathorized access! + bind-address=0.0.0.0 + +
+Enabling remote connections to your MySQL instance introduces various security risks. Make sure to take appropriate steps to secure your instance by using a strong password, disabling MySQL root access, and using a firewall to only accept qualified hosts, for example. +
+ +Make sure to restart your MySQL instance to reflect the changes. + + :::bash + sudo service mysql restart + + +## Setting up Silverstripe to connect to MySQL + +Now that we have successfully setup the SSL your MySQL host, we now need to configure Silverstripe to use the certificates. + +### Copying SSL Certificates + +First we need to copy the client certificate files to the Silverstripe instance. You will need to copy: + +- `client-key.pem` +- `client-cert.pem` +- `ca-cert.pem` + +
+Make sure to only copy `client-key.pem`, `client-cert.pem`, and `ca-cert.pem` to avoid leaking your credentials! +
+ +On your Silverstripe instance: + + :::bash + # Secure copy over SSH via rsync command. You may use an alternative method if desired. + rsync -avP user@db1.example.com:/path/to/client/certs /path/to/secure/folder + + # Depending on your web server configuration, allow web server to read to SSL files + sudo chown -R www-data:www-data /path/to/secure/folder + sudo chmod 750 /path/to/secure/folder + sudo chmod 400 /path/to/secure/folder/* + +### Setting up _ss_environment.php to use SSL certificates + +
+`SS_DATABASE_SERVER does not accept IP-based hostnames. Also, if the domain name of the host does not match the common name you used to generate the server certificate, you will get an `SSL certificate mismatch error`. +
+ +Add or edit your `_ss_environment.php` configuration file. (See [Environment Management](/getting_started/environment_management) for more information.) + + :::php + '); + + // These define the paths to the SSL key, certificate, and CA certificate bundle. + define('SS_DATABASE_SSL_KEY', '/home/newdrafts/mysqlssltest/client-key.pem'); + define('SS_DATABASE_SSL_CERT', '/home/newdrafts/mysqlssltest/client-cert.pem'); + define('SS_DATABASE_SSL_CA', '/home/newdrafts/mysqlssltest/ca- cert.pem'); + + // When using SSL connections, you also need to supply a username and password to override the default settings + define('SS_DEFAULT_ADMIN_USERNAME', 'username'); + define('SS_DEFAULT_ADMIN_PASSWORD', 'password'); + + +When running the installer, make sure to check on the `Use _ss_environment file for configuration` option under the `Database Configuration` section to use the environment file. + +## Conclusion + +That's it! We hope that this article was able to help you configure your remote MySQL SSL secure database connection. \ No newline at end of file diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md index 05946a051..b6f25e9d5 100644 --- a/docs/en/00_Getting_Started/03_Environment_Management.md +++ b/docs/en/00_Getting_Started/03_Environment_Management.md @@ -85,3 +85,8 @@ SilverStripe core environment variables are listed here, though you're free to d | `SS_MANIFESTCACHE` | The manifest cache to use (defaults to file based caching). Must be a CacheInterface or CacheFactory class name | | `SS_IGNORE_DOT_ENV` | If set the .env file will be ignored. This is good for live to mitigate any performance implications of loading the .env file | | `SS_BASE_URL` | The url to use when it isn't determinable by other means (eg: for CLI commands) | +| `SS_CONFIGSTATICMANIFEST` | Set to `SS_ConfigStaticManifest_Reflection` to use the Silverstripe 4 Reflection config manifest (speed improvement during dev/build and ?flush) | +| `SS_DATABASE_SSL_KEY` | Absolute path to SSL key file | +| `SS_DATABASE_SSL_CERT` | Absolute path to SSL certificate file | +| `SS_DATABASE_SSL_CA` | Absolute path to SSL Certificate Authority bundle file | +| `SS_DATABASE_SSL_CIPHER` | Optional setting for custom SSL cipher | diff --git a/src/Dev/Install/MySQLDatabaseConfigurationHelper.php b/src/Dev/Install/MySQLDatabaseConfigurationHelper.php index 97c6d35d7..000eb9b70 100644 --- a/src/Dev/Install/MySQLDatabaseConfigurationHelper.php +++ b/src/Dev/Install/MySQLDatabaseConfigurationHelper.php @@ -6,6 +6,9 @@ use mysqli; use PDO; use Exception; use mysqli_result; +use SilverStripe\Core\Config\Config; +use SilverStripe\ORM\Connect\MySQLiConnector; +use SilverStripe\ORM\Connect\PDOConnector; /** * This is a helper class for the SS installer. @@ -30,11 +33,29 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper try { switch ($databaseConfig['type']) { case 'MySQLDatabase': - $conn = @new MySQLi( + $conn = mysqli_init(); + + // Set SSL parameters if they exist. All parameters are required. + if (array_key_exists('ssl_key', $databaseConfig) && + array_key_exists('ssl_cert', $databaseConfig) && + array_key_exists('ssl_ca', $databaseConfig) + ) { + $conn->ssl_set( + $databaseConfig['ssl_key'], + $databaseConfig['ssl_cert'], + $databaseConfig['ssl_ca'], + dirname($databaseConfig['ssl_ca']), + array_key_exists('ssl_cipher', $databaseConfig) + ? $databaseConfig['ssl_cipher'] + : Config::inst()->get(MySQLiConnector::class, 'ssl_cipher_default') + ); + } + @$conn->real_connect( $databaseConfig['server'], $databaseConfig['username'], $databaseConfig['password'] ); + if ($conn && empty($conn->connect_errno)) { $conn->query("SET sql_mode = 'ANSI'"); return $conn; @@ -46,10 +67,31 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper } case 'MySQLPDODatabase': // May throw a PDOException if fails + + // Set SSL parameters + $ssl = null; + + if (array_key_exists('ssl_key', $databaseConfig) && + array_key_exists('ssl_cert', $databaseConfig) + ) { + $ssl = array( + PDO::MYSQL_ATTR_SSL_KEY => $databaseConfig['ssl_key'], + PDO::MYSQL_ATTR_SSL_CERT => $databaseConfig['ssl_cert'], + ); + if (array_key_exists('ssl_ca', $databaseConfig)) { + $ssl[PDO::MYSQL_ATTR_SSL_CA] = $databaseConfig['ssl_ca']; + } + // use default cipher if not provided + $ssl[PDO::MYSQL_ATTR_SSL_CA] = array_key_exists('ssl_ca', $databaseConfig) + ? $databaseConfig['ssl_ca'] + : Config::inst()->get(PDOConnector::class, 'ssl_cipher_default'); + } + $conn = @new PDO( 'mysql:host='.$databaseConfig['server'], $databaseConfig['username'], - $databaseConfig['password'] + $databaseConfig['password'], + $ssl ); if ($conn) { $conn->query("SET sql_mode = 'ANSI'"); diff --git a/src/Dev/Install/install5.php b/src/Dev/Install/install5.php index cce0726ea..ebcfb7528 100755 --- a/src/Dev/Install/install5.php +++ b/src/Dev/Install/install5.php @@ -138,6 +138,18 @@ if (isset($_REQUEST['db'])) { "password" => getenv('SS_DATABASE_PASSWORD') ?: "", "database" => $_REQUEST['db'][$type]['database'], ); + + // Set SSL parameters if they exist + if (getenv('SS_DATABASE_SSL_KEY') && getenv('SS_DATABASE_SSL_CERT')) { + $databaseConfig['ssl_key'] = getenv('SS_DATABASE_SSL_KEY'); + $databaseConfig['ssl_cert'] = getenv('SS_DATABASE_SSL_CERT'); + } + if (getenv('SS_DATABASE_SSL_CA')) { + $databaseConfig['ssl_ca'] = getenv('SS_DATABASE_SSL_CA'); + } + if (getenv('SS_DATABASE_SSL_CIPHER')) { + $databaseConfig['ssl_ca'] = getenv('SS_DATABASE_SSL_CIPHER'); + } } else { // Normal behaviour without the environment $databaseConfig = $_REQUEST['db'][$type]; diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index a8455bf3e..6f2f0f8ad 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -440,7 +440,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly { $filename = ClassLoader::inst()->getItemPath(static::class); if (!$filename) { - throw new LogicException("getItemPath returned null for " . static::class); + throw new LogicException("getItemPath returned null for " . static::class + . ". Try adding flush=1 to the test run."); } return dirname($filename); } diff --git a/src/Forms/TreeDropdownField.php b/src/Forms/TreeDropdownField.php index ef972965d..dc6a13788 100644 --- a/src/Forms/TreeDropdownField.php +++ b/src/Forms/TreeDropdownField.php @@ -69,6 +69,13 @@ class TreeDropdownField extends FormField 'tree' ); + /** + * @config + * @var int + * @see {@link Hierarchy::$node_threshold_total}. + */ + private static $node_threshold_total = 30; + /** * @var string */ @@ -488,7 +495,12 @@ class TreeDropdownField extends FormField } // Create marking set - $markingSet = MarkedSet::create($obj, $this->getChildrenMethod(), $this->getNumChildrenMethod(), 30); + $markingSet = MarkedSet::create( + $obj, + $this->getChildrenMethod(), + $this->getNumChildrenMethod(), + $this->config()->get('node_threshold_total') + ); // Set filter on searched nodes if ($this->getFilterFunction() || $this->search) { diff --git a/src/ORM/Connect/DBConnector.php b/src/ORM/Connect/DBConnector.php index c5af38205..039a6accf 100644 --- a/src/ORM/Connect/DBConnector.php +++ b/src/ORM/Connect/DBConnector.php @@ -3,6 +3,7 @@ namespace SilverStripe\ORM\Connect; use SilverStripe\Core\Config\Config; +use SilverStripe\Core\Config\Configurable; use SilverStripe\View\Parsers\SQLFormatter; /** @@ -11,6 +12,8 @@ use SilverStripe\View\Parsers\SQLFormatter; abstract class DBConnector { + use Configurable; + /** * List of operations to treat as write * Implicitly includes all ddl_operations diff --git a/src/ORM/Connect/MySQLiConnector.php b/src/ORM/Connect/MySQLiConnector.php index 8ca85f91b..6b283be7b 100644 --- a/src/ORM/Connect/MySQLiConnector.php +++ b/src/ORM/Connect/MySQLiConnector.php @@ -12,6 +12,14 @@ use mysqli_stmt; class MySQLiConnector extends DBConnector { + /** + * Default strong SSL cipher to be used + * + * @config + * @var string + */ + private static $ssl_cipher_default = 'DHE-RSA-AES256-SHA'; + /** * Connection to the MySQL database * @@ -68,23 +76,31 @@ class MySQLiConnector extends DBConnector $connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset'); $connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation'); - if (!empty($parameters['port'])) { - $this->dbConn = new MySQLi( - $parameters['server'], - $parameters['username'], - $parameters['password'], - $selectedDB, - $parameters['port'] - ); - } else { - $this->dbConn = new MySQLi( - $parameters['server'], - $parameters['username'], - $parameters['password'], - $selectedDB + $this->dbConn = mysqli_init(); + + // Set SSL parameters if they exist. All parameters are required. + if (array_key_exists('ssl_key', $parameters) && + array_key_exists('ssl_cert', $parameters) && + array_key_exists('ssl_ca', $parameters)) { + $this->dbConn->ssl_set( + $parameters['ssl_key'], + $parameters['ssl_cert'], + $parameters['ssl_ca'], + dirname($parameters['ssl_ca']), + array_key_exists('ssl_cipher', $parameters) + ? $parameters['ssl_cipher'] + : self::config()->get('ssl_cipher_default') ); } + $this->dbConn->real_connect( + $parameters['server'], + $parameters['username'], + $parameters['password'], + $selectedDB, + !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port") + ); + if ($this->dbConn->connect_error) { $this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error); } diff --git a/src/ORM/Connect/PDOConnector.php b/src/ORM/Connect/PDOConnector.php index 5bf70f106..753957488 100644 --- a/src/ORM/Connect/PDOConnector.php +++ b/src/ORM/Connect/PDOConnector.php @@ -21,6 +21,14 @@ class PDOConnector extends DBConnector */ private static $emulate_prepare = false; + /** + * Default strong SSL cipher to be used + * + * @config + * @var string + */ + private static $ssl_cipher_default = 'DHE-RSA-AES256-SHA'; + /** * The PDO connection instance * @@ -171,6 +179,20 @@ class PDOConnector extends DBConnector $options = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation ); + + // Set SSL options if they are defined + if (array_key_exists('ssl_key', $parameters) && + array_key_exists('ssl_cert', $parameters) + ) { + $options[PDO::MYSQL_ATTR_SSL_KEY] = $parameters['ssl_key']; + $options[PDO::MYSQL_ATTR_SSL_CERT] = $parameters['ssl_cert']; + if (array_key_exists('ssl_ca', $parameters)) { + $options[PDO::MYSQL_ATTR_SSL_CA] = $parameters['ssl_ca']; + } + // use default cipher if not provided + $options[PDO::MYSQL_ATTR_SSL_CIPHER] = array_key_exists('ssl_cipher', $parameters) ? $parameters['ssl_cipher'] : self::config()->get('ssl_cipher_default'); + } + if (self::is_emulate_prepare()) { $options[PDO::ATTR_EMULATE_PREPARES] = true; } diff --git a/templates/forms/HtmlEditorField_holder_small.ss b/templates/forms/HtmlEditorField_holder_small.ss new file mode 100644 index 000000000..21eec8dc5 --- /dev/null +++ b/templates/forms/HtmlEditorField_holder_small.ss @@ -0,0 +1,5 @@ +
+ <% if $Title %><% end_if %> + $Field + <% if $RightTitle %><% end_if %> +
diff --git a/tests/php/Security/MemberAuthenticatorTest.php b/tests/php/Security/MemberAuthenticatorTest.php index d8e4c2f05..6a203681f 100644 --- a/tests/php/Security/MemberAuthenticatorTest.php +++ b/tests/php/Security/MemberAuthenticatorTest.php @@ -3,6 +3,7 @@ namespace SilverStripe\Security\Tests; use SilverStripe\Control\Controller; +use SilverStripe\Control\NullHTTPRequest; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; @@ -42,7 +43,6 @@ class MemberAuthenticatorTest extends SapphireTest $this->defaultUsername = null; $this->defaultPassword = null; } - DefaultAdminService::clearDefaultAdmin(); DefaultAdminService::setDefaultAdmin('admin', 'password'); } @@ -149,6 +149,37 @@ class MemberAuthenticatorTest extends SapphireTest ); } + public function testExpiredTempID() + { + DefaultAdminService::clearDefaultAdmin(); + + $authenticator = new CMSMemberAuthenticator(); + + // Make member with expired TempID + $member = new Member(); + $member->Email = 'test1@test.com'; + $member->PasswordEncryption = "sha1"; + $member->Password = "mypassword"; + $member->TempIDExpired = '2016-05-22 00:00:00'; + $member->write(); + Injector::inst()->get(IdentityStore::class)->logIn($member, true); + + $tempID = $member->TempIDHash; + + DBDatetime::set_mock_now('2016-05-29 00:00:00'); + + $this->assertNotEmpty($tempID); + $this->assertFalse(DefaultAdminService::hasDefaultAdmin()); + + $result = $authenticator->authenticate(array( + 'tempid' => $tempID, + 'Password' => 'notmypassword' + ), Controller::curr()->getRequest(), $validationResult); + + $this->assertNull($result); + $this->assertFalse($validationResult->isValid()); + } + /** * Test that the default admin can be authenticated */