Merge branch '3' into 4

This commit is contained in:
Daniel Hensby 2017-08-14 16:50:17 +01:00
commit c0211927aa
No known key found for this signature in database
GPG Key ID: 5DE415D786BBB2FD
11 changed files with 337 additions and 19 deletions

View File

@ -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.
<div class="notice" markdown='1'>
This article assumes that you have `MySQL` and `OpenSSL` installed.
</div>
## 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.
<div class="notice" markdown='1'>
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)
</div>
:::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
<div class="warning" markdown='1'>
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.
</div>
:::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
<div class="notice" markdown='1'>
For Debian/Ubuntu instances, the configuration file is usually in `/etc/mysql/my.cnf`. Refer to your MySQL manual for more information
</div>
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
<div class="warning" markdown='1'>
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.
</div>
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`
<div class="warning" markdown='1'>
Make sure to only copy `client-key.pem`, `client-cert.pem`, and `ca-cert.pem` to avoid leaking your credentials!
</div>
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
<div class="notice" markdown='1'>
`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`.
</div>
Add or edit your `_ss_environment.php` configuration file. (See [Environment Management](/getting_started/environment_management) for more information.)
:::php
<?php
// These four define set the database connection details.
define('SS_DATABASE_CLASS', 'MySQLPDODatabase');
define('SS_DATABASE_SERVER', 'db1.example.com');
define('SS_DATABASE_USERNAME', 'dbuser');
define('SS_DATABASE_PASSWORD', '<password>');
// 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.

View File

@ -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_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_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_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 |

View File

@ -6,6 +6,9 @@ use mysqli;
use PDO; use PDO;
use Exception; use Exception;
use mysqli_result; 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. * This is a helper class for the SS installer.
@ -30,11 +33,29 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
try { try {
switch ($databaseConfig['type']) { switch ($databaseConfig['type']) {
case 'MySQLDatabase': 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['server'],
$databaseConfig['username'], $databaseConfig['username'],
$databaseConfig['password'] $databaseConfig['password']
); );
if ($conn && empty($conn->connect_errno)) { if ($conn && empty($conn->connect_errno)) {
$conn->query("SET sql_mode = 'ANSI'"); $conn->query("SET sql_mode = 'ANSI'");
return $conn; return $conn;
@ -46,10 +67,31 @@ class MySQLDatabaseConfigurationHelper implements DatabaseConfigurationHelper
} }
case 'MySQLPDODatabase': case 'MySQLPDODatabase':
// May throw a PDOException if fails // 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( $conn = @new PDO(
'mysql:host='.$databaseConfig['server'], 'mysql:host='.$databaseConfig['server'],
$databaseConfig['username'], $databaseConfig['username'],
$databaseConfig['password'] $databaseConfig['password'],
$ssl
); );
if ($conn) { if ($conn) {
$conn->query("SET sql_mode = 'ANSI'"); $conn->query("SET sql_mode = 'ANSI'");

View File

@ -138,6 +138,18 @@ if (isset($_REQUEST['db'])) {
"password" => getenv('SS_DATABASE_PASSWORD') ?: "", "password" => getenv('SS_DATABASE_PASSWORD') ?: "",
"database" => $_REQUEST['db'][$type]['database'], "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 { } else {
// Normal behaviour without the environment // Normal behaviour without the environment
$databaseConfig = $_REQUEST['db'][$type]; $databaseConfig = $_REQUEST['db'][$type];

View File

@ -440,7 +440,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase implements TestOnly
{ {
$filename = ClassLoader::inst()->getItemPath(static::class); $filename = ClassLoader::inst()->getItemPath(static::class);
if (!$filename) { 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); return dirname($filename);
} }

View File

@ -69,6 +69,13 @@ class TreeDropdownField extends FormField
'tree' 'tree'
); );
/**
* @config
* @var int
* @see {@link Hierarchy::$node_threshold_total}.
*/
private static $node_threshold_total = 30;
/** /**
* @var string * @var string
*/ */
@ -488,7 +495,12 @@ class TreeDropdownField extends FormField
} }
// Create marking set // 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 // Set filter on searched nodes
if ($this->getFilterFunction() || $this->search) { if ($this->getFilterFunction() || $this->search) {

View File

@ -3,6 +3,7 @@
namespace SilverStripe\ORM\Connect; namespace SilverStripe\ORM\Connect;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\View\Parsers\SQLFormatter; use SilverStripe\View\Parsers\SQLFormatter;
/** /**
@ -11,6 +12,8 @@ use SilverStripe\View\Parsers\SQLFormatter;
abstract class DBConnector abstract class DBConnector
{ {
use Configurable;
/** /**
* List of operations to treat as write * List of operations to treat as write
* Implicitly includes all ddl_operations * Implicitly includes all ddl_operations

View File

@ -12,6 +12,14 @@ use mysqli_stmt;
class MySQLiConnector extends DBConnector 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 * Connection to the MySQL database
* *
@ -68,22 +76,30 @@ class MySQLiConnector extends DBConnector
$connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset'); $connCharset = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_charset');
$connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation'); $connCollation = Config::inst()->get('SilverStripe\ORM\Connect\MySQLDatabase', 'connection_collation');
if (!empty($parameters['port'])) { $this->dbConn = mysqli_init();
$this->dbConn = new MySQLi(
// 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['server'],
$parameters['username'], $parameters['username'],
$parameters['password'], $parameters['password'],
$selectedDB, $selectedDB,
$parameters['port'] !empty($parameters['port']) ? $parameters['port'] : ini_get("mysqli.default_port")
); );
} else {
$this->dbConn = new MySQLi(
$parameters['server'],
$parameters['username'],
$parameters['password'],
$selectedDB
);
}
if ($this->dbConn->connect_error) { if ($this->dbConn->connect_error) {
$this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error); $this->databaseError("Couldn't connect to MySQL database | " . $this->dbConn->connect_error);

View File

@ -21,6 +21,14 @@ class PDOConnector extends DBConnector
*/ */
private static $emulate_prepare = false; 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 * The PDO connection instance
* *
@ -171,6 +179,20 @@ class PDOConnector extends DBConnector
$options = array( $options = array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES ' . $charset . ' COLLATE ' . $connCollation 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()) { if (self::is_emulate_prepare()) {
$options[PDO::ATTR_EMULATE_PREPARES] = true; $options[PDO::ATTR_EMULATE_PREPARES] = true;
} }

View File

@ -0,0 +1,5 @@
<div class="fieldholder-small field htmleditor">
<% if $Title %><label class="fieldholder-small-label" <% if $ID %>for="$ID"<% end_if %>>$Title</label><% end_if %>
$Field
<% if $RightTitle %><label class="right fieldholder-small-label" <% if $ID %>for="$ID"<% end_if %>>$RightTitle</label><% end_if %>
</div>

View File

@ -3,6 +3,7 @@
namespace SilverStripe\Security\Tests; namespace SilverStripe\Security\Tests;
use SilverStripe\Control\Controller; use SilverStripe\Control\Controller;
use SilverStripe\Control\NullHTTPRequest;
use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
@ -42,7 +43,6 @@ class MemberAuthenticatorTest extends SapphireTest
$this->defaultUsername = null; $this->defaultUsername = null;
$this->defaultPassword = null; $this->defaultPassword = null;
} }
DefaultAdminService::clearDefaultAdmin();
DefaultAdminService::setDefaultAdmin('admin', 'password'); 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 * Test that the default admin can be authenticated
*/ */