Inpart 1of this series, we looked at Installing mysql Server on Ubuntu 16.04 LTS. In this second part, we will be looking at configuring MySQL securely.
ConfigurationWe will start off with the most common settings by opening the default MySQL configuration file using the nano text editor.
secuser@secureserver:/# sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf UserMySQL server should never be run as the root user. To understand why this is so important, let’s take as an example MySQL user jason who has the FILE privilege. This means that jason can read/write files on the server. If MySQL server is running under the root account, it can read and write files as root , which on linux systems is the super-user.
To such an extent, MySQL should only be run as an unprivileged user which minimizes the risk of users having unauthorized access to sensitive files. By default, MySQL creates the mysql user and the MySQL server should run under that account.
user = mysql Server BindingThe bind-address option, which is currently set by default to 127.0.0.1 (localhost), tells MySQL server on which address its network socket will listen to. By using the 127.0.0.1 address, the MySQL server can only be accessed by applications which are installed on the same local environment as the MySQL server.
Warning Setting the bind-address to 0.0.0.0 (IPv4), or :: (IPv4/IPv6) is insecure since it will make the server accept TCP/IP connections on all interfaces.
A web application on the same machine can access the backend database, while another computer on the local or external network will not have access to it. This means that we restrict any direct remote access to the MySQL server.
We make sure that MySQL has the following bind-address entry in mysqld.cnf .
bind-address = 127.0.0.1 PortBy default, the MySQL server listens on port 3306 . By changing the default port, we can make the MySQL service harder to identify.
Port = 3444 #sample port numberNote The mysql service needs to be restarted for the above change to take effect
We can verify that MySQL is running on the newly configured port by running netstat and using the grep utility to filter for the mysql service.
secuser@secureserver:/# netstat ntulp | grep mysql--> Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3444 0.0.0.0:* LISTEN 865/mysqld local_infile
This option, which is enabled by default, allows a user to use LOCAL in LOAD_DATA statements, which means that it allows that user to read files on the local filesystem. Having said this, for this to work, the user must have the FILE privilege granted and can only read files they have access to.
Nevertheless, it is still very dangerous to allow MySQL users to read files as they could potentially have access sensitive data.
Note― The /etc/passwd file is commonly used in SQL injection and LFI (Local File Inclusion) exploitation examples since it is readable by all users on the system and it contains a list of the local user accounts along with information for each account.
The following is an example of saving the content of /etc/passwd into a table with the name of test .
mysql> LOAD DATA LOCAL INFILE "/etc/passwd" REPLACE INTO TABLE test FIELDS TERMINATED BY '\t' lines terminated by '\n';We can then query the table to see the file content.
mysql> SELECT * FROM test;-->
+----------------------------------------------------------------------+
| text |
+----------------------------------------------------------------------+
| root:x:0:0:root:/root:/bin/bash |
| daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin |
| bin:x:2:2:bin:/bin:/usr/sbin/nologin |
| sys:x:3:3:sys:/dev:/usr/sbin/nologin |
| sync:x:4:65534:sync:/bin:/bin/sync |
| games:x:5:60:games:/usr/games:/usr/sbin/nologin |
+----------------------------------------------------------------------+
We can disable this feature as follows in mysqld.cnf .
local_infile = 0 secure_file_privUsers who have the FILE privilege can use the LOAD_FILE() or LOAD_DATA() and SELECT INTO statements to load or write data to a file.
What secure_file_priv does, is limit the usage of these operations within a specified directory which means that the user can only work with files from within that directory.
Note Avoid specifying the secure_file_priv parameter without a value as it will have no effect The directory specified in the secure_file_priv parameter must exist (MySQL server will not create it) The MySQL data directory, any subdirectory within the data directory or any directory that is accessible by all users (e.g. /tmp), should not be specified in the secure_file_priv parameter
secure_file_priv = '/path/' Symbolic linksSymbolic links, commonly referred to as symlinks , are files that link to other files (similar to shortcuts in Microsoft windows). They are commonly used to move the data directory (the directory containing databases) to another folder or path.
Symbolic links to tables shouldn’t be allowed, especially if MySQL server is running as root (which, as we already discussed should never be the case) because under certain circumstances, users can access files outside the data directory which may lead to unauthorized access to sensitive data.
We can disable this feature as follows in mysqld.cnf .
symbolic-links = 0 default_password_lifetimeAs from MySQL 5.7.4, automatic password expiration is available in MySQL. This means that we can define the number of days a password is valid for. The default global value of default_password_lifetime is 360 , which means that a password must be changed approximately once a year.
Warning Web applications that connect to the database will no longer be able to connect to the database after their user’s password expires. Follow the instructions below to overcome this issue.
If we want passwords to expire after 90 days (3 months) we would define the following in mysqld.cnf .
default_password_lifetime = 90Otherwise, if we want passwords to never expire, we can set the following in mysqld.cnf .
default_password_lifetime = 0Alternatively, we can specify per user expiry by using ALTER_USER in a MySQL shell as follows.
mysql> ALTER USER jason'@'localhost' PASSWORD EXPIRE INTERVAL 35 DAY;If our database user’s password has expired, we will get the following message (for example’s sake, errors are being displayed to the user, however, in reality applications should properly handle and log database errors as opposed to printing them).

In order to set a new password, we must login to the MySQL server using the credentials of the user whose password has expired. We will use ‘jason’ as an example.
secuser@secureserver:/# mysql -u jason -pmysql> SET PASSWORD = PASSWORD('gsu8L54eV6UBpS5A8ZbZ');
--> Query OK, 0 rows affected, 1 warning (0.00 sec)
We then edit our web application’s configuration file and set the new password.
secuser@secureserver:/# sudo nano /var/www/html/sql.php...
$con = mysqli_connect("localhost", "jason", "gsu8L54eV6UBpS5A8ZbZ") or
die("Could not connect: " . mysqli_error());
...
Once we save the changes and refresh the page, our web application is now back online.

AppArmor
AppArmor in Ubuntu is a kernel-integrated security module. Its primary job is permission control, which means that it checks if a program is running within its allowed environment.
Each program has its own profile which AppArmor loads once started. MySQL has its own profile that gets installed during MySQL Server setup.
To check whether MySQL’s profile is loaded we can run the following command.
secuser@secureserver:/# sudo apparmor_status--> apparmor module is loaded.
28 profiles are loaded.
28 profiles are in enforce mode.
# We can make sure this profile is loaded
/usr/sbin/mysqld
If the profile is not loaded, we can enable it by running the following commands.
Warning Beware when editing AppArmor’s profiles (or creating new ones) because its permissions may cause applications to malfunction. Ideally, test any changes in a staging/pre-production environment first.
secuser@secureserver:/# sudo rm /etc/apparmor.d/disable/usr.sbin.mysqldsecuser@secureserver:/# sudo apparmor_parser -r /etc/apparmor.d/usr.sbin.mysqld User accounts
In order for an application to access a database, it must first authenticate against the database server using a valid user account. As part of our MySQL server audit process, we need to review the user accounts and identify redundant accounts, or accounts that do not have a password specified. The latest versions of MySQL initially only create one account root. However, older versions create also an Anonymous account which has no password.
In order to get a list of users, we must first login to the MySQL server.
secuser@secureserver:/# mysql -u root -pEnter password:
# mysql server message
mysql>
After logging in, we can query the mysql.user table to get the user list.
mysql> SELECT User, Host, Authentication_String FROM mysql.user;-->
+------------------+-----------+----------------------------------------+
| User | Host | Authentication_String
+------------------+-----------+----------------------------------------+
| root | localhost | *4ACFE3202A5FF5CF467898FC58AAB1D615... |
| mysql.sys | localhost | *0CC75F492CB9CCCB138B7C76F73C0B5D17... |
| debian-sys-maint | localhost | *FDD4ACF2CE3B980940298A1FD2D6A5A7ED... |
| test | localhost |
+------------------+-----------+----------------------------------------+
4 rows in set (0.00 sec)
As we can see there is the root account, and the test account which has no password. We can specify a secure password for that account as follows.
mysql> SET PASSWORD FOR 'test'@'localhost' = "uYI3cU_FreFgR*UsS2B";--> Query OK, 0 rows affected (0.00 sec)
mysql> SELECT User, Host, Authentication_String FROM mysql.user WHERE User='test';
-->
+------------------+-----------+----------------------------------------+
| User | Host | Authentication_String
+------------------+-----------+----------------------------------------+
| test | localhost | *73C084DAC378FF328AD3C131ED10F6E309... |
+------------------+-----------+----------------------------------------+
1 row in set (0.00 sec)
Alternatively, we can remove the account if we don’t need it.
mysql> DROP USER 'test'@'localhost';--> Query OK, 0 rows affected (0.00 sec)
Note Use a different database user for each web applications connecting to the database. If one application gets compromised and the attacker has access to the database, they will not be able to access other databases Remember that an attacker can use an anonymous account to flood the database with a huge amount of records, consequently exhausting disk space which will eventually result in a denial of service (DoS) attack
Having a very strong password for the root user is fundamental . Additionally, we could also rename the root account username to make it harder for an attacker to guess.
mysql> RENAME USER 'root'@'localhost' TO new_username;--> Query OK, 0 rows affected (0.00 sec) User Privileges
The privileges of a database user are very often overlooked. Either the administrator does not know which permissions to grant to a user and thus grants all, or, the administrator does not carefully assign the correct permissions. Permissions are key to the overall website and server security, and could help mitigate the effects of a successful attack.
In the event of our web application getting compromised, we should have the correct mechanisms in place to minimize the damage or the chance of further privilege escalation by an attacker.
Since there are too many permissions to list, we will take a look at which permissions a common website, in our case a WordPress site, needs to have in order to function normally.
Note― With WordPress as well as other CMSs and web applications, some third-party plugins, or core updates, may need to create or alter tables, in which case, additional permissions will be required (at least temporarily). It is recommended that all privileges are granted during the installation or a core update. Additional privileges granted should then be revoked once the update is complete.
During the installation, the application may use all of the permissions in order to correctly setup the tables and insert the required data. However, after the setup is finished, a typical installation will need only the following permissions for it to function, which are only related to the DATA of the database and not STRUCTURE or ADMINISTRATION .
Privilege Description SELECT Allows a user to select data from a table INSERT Allows a user to insert data into a table UPDATE Allows a user to update the data of a table DELETE Allows a user to delete the data of a tableFor example, we can grant the above permissions on our user ‘jason’ on the ‘test’ database using the following SQL statement.
mysql> GRANT SELECT, INSERT, UPDATE, DELETE ON Test.* TO 'jason'@'localhost';--> Query OK, 0 rows affected (0.01 sec)
mysql> show grants FOR 'jason'@'localhost';
-->
+-----------------------------------------------------------------------+
| Grants for jason@localhost |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'jason'@'localhost' |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `Test`.* TO 'jason'@'localhost'
+-----------------------------------------------------------------------+
2 rows in set (0.00 sec)
Some users prefer the graphical interface of some administration tools like phpMyAdmin and MySQL Workbench, which make it easier to manage databases as well as the privileges of a user. As we can see the privileges are grouped in categories.

Databases
After reviewing the users, we must now review the databases to identify unneeded databases.
mysql> show databases;-->
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sys |
| test |
+--------------------+
5 rows in set (0.00 sec)
Older versions of MySQL server initially create a test database which is accessible by any user and should be removed along with any other database that is not being used. In the case that we need to keep the test database, we should remove access rights for all users.
We can remove access to the test database by executing the following SQL commands.
mysql> DELETE FROM mysql.db WHERE Db = 'test';--> Query OK, 0 rows affected (0.02 sec)
mysql> FLUSH PRIVILEGES;
--> Query OK, 0 rows affected (0.00 sec)
Now, no users should be able to access that database. If we want to completely remove the database, we can do so by executing the following SQL statement.
Note― If we decide to remove a database we must first delete access to it . If we only drop the database, then the users will still have access to that database if recreated in the future
mysql> DROP DATABASE test;--> Query OK, 0 rows affected (0.00 sec) Command History
Like Bash, MySQL has its own history log stored in the ~/.mysql_history file. Any commands send to the MySQL server via shell will be logged into this file. As you may have already figured out, it will likely contain information about permissions, database names, usernames and passwords which we may have used in queries.
secuser@secureserver:~# cat ~/.mysql_history--> use\040wp;
select\040*\040FROM\040wp_users;
select\040*\040FROM\040wp_users\040WHERE\040user_pass='1hF71jf91f7919Hf271HHFbA24';
exit
We can clear this file using the following Bash command.
secuser@secureserver:~# cat /dev/null > ~/.mysql_history