Paul DuBois
paul@kitebird.com
Document revision: 1.01
Last update: 2003-01-25
A MySQL installation should be made as secure as possible to protect
databases and other information maintained by the MySQL server
from unauthorized access. This article describes potential problem
areas about which you should be concerned as a MySQL administrator,
and provides guidelines for dealing with them. The issues covered
here fall into the following broad categories, which include both
local and remote exploits:
Unix commands shown with a # prompt should be run as
root. Commands shown with a % prompt may be
run as an ordinary user. SQL statements shown with a mysql>
prompt are displayed as you would enter them using the mysql
command-line client program.
General Security Considerations
This section outlines aspects of a suggested MySQL security strategy.
Subsequent sections provide specific instructions to help you
carry it out. In general, security principles you should consider
adopting are as follows:
The instructions in this section apply to Unix systems. If you're
running Windows, you can skip ahead to "Securing MySQL Accounts."
The procedure described here shows how to run the MySQL server
using an unprivileged (non-root) login account and how
to protect the server's data directory so that it can be accessed
directly only by that account. You'll need to run the commands
shown here as root, because they involve login account
creation, as well as file ownership and mode changes.
# mysqladmin -p -u root shutdownOmit the -p option if you have not yet set up a password for the MySQL root account.
# chown -R mysqlusr /usr/local/mysql/data # chmod -R go-rwx /usr/local/mysql/dataIf your MySQL installation follows the usual convention of storing individual database directories directly under the data directory, these commands suffice to secure it. However, if your data directory contains symbolic links that point to databases located elsewhere and your versions of chown and chmod don't follow symlinks, you'll need a different approach. Either follow the links yourself and run the appropriate chown and chmod commands in the directories where they lead, or else change the ownership and mode of the data directory contents using the following commands rather than those just shown:
# find /usr/local/mysql/data -follow -print \
| xargs chown mysqlusr
# find /usr/local/mysql/data -follow -print \
| xargs chmod go-rwx
Whichever pair of commands you use to lock down the data directory
so that it can be accessed only by the mysqlusr account,
you may have a problem if the data directory contains the socket
file that the server uses to listen for local connections. In
that case, other people on the server host won't be able to connect
to the server through the socket. To remedy this difficulty after
restricting access to the data directory as described above, you
can use the following command; it opens up the data directory
enough that client programs can access the socket file:# chmod go+rx /usr/local/mysql/dataUnfortunately, making the directory readable to other local login accounts also allows them to see what files and directories it contains. A better solution, if you install MySQL from source, is to recompile the MySQL distribution to relocate the socket file outside the data directory. Use the --with-unix-socket-path option when you run the distribution's configure script. Relocating the socket file by specifying a different pathname for it in an option file is an incomplete solution because it fails to affect clients that do not read option files.
# mysqld_safe --user=mysqlusr &However, you can avoid specifying the user name on the command line if you add a user line to the [mysqld] group in the /etc/my.cnf option file:
[mysqld] user=mysqlusrThis approach has the advantage that it works both for mysqld_safe and for mysql.server, the two scripts most commonly used to start the MySQL server at system boot time. This means that if you put the user value in /etc/my.cnf, no changes will be necessary to your system's startup procedure.
If you run multiple MySQL servers on a single host, you should
repeat the procedure just described for each server and its data
directory. However, if you want to use different accounts to run
different servers, you cannot put a user line in the
[mysqld] group of the /etc/my.cnf file--it will
be used for all servers. Use one of the following methods instead:
Option files are a potential point of compromise because they
contain information used to control the server's operation or
to store client connection parameters such as MySQL user names
and passwords. Option files can be global, server-specific, or
user-specific. The precautions you take to secure a given file
depend on its scope and purpose.
Global Option Files
/etc/my.cnf is a global option file used both by the MySQL
server and by client programs. It should be protected against
unauthorized modification (to prevent someone from changing it
to tell the server to start up as root, for example),
but must also be publicly readable so that client programs can
access it. To make this file owned by and modifiable only by root
but readable to anyone, use these commands:
# chown root /etc/my.cnf # chmod 644 /etc/my.cnfBecause this file is world-readable, it should not be used to specify password values.
A MySQL server can have its own private option file named my.cnf
located in the server's data directory. This file should be owned
by and accessible only to the account used to run the server.
If you've followed the procedure outlined earlier for securing
your data directory, these access constraints should be satisfied
already. To set the ownership and mode explicitly (assuming the
data directory is /usr/local/mysql/data), use these commands:
# chown mysqlusr /usr/local/mysql/data/my.cnf # chmod 600 /usr/local/mysql/data/my.cnfIn a multiple-server installation, you'd need to repeat these commands, substituting the appropriate user name and option file pathname, of course.
On multiple-user systems, each login account may have its own
option file. The file is named .my.cnf and is located in
the account's home directory. Personal option files are commonly
used to store MySQL user names and passwords, so each one should
be accessible only to its owner to prevent other users from reading
its contents. A user who has a .my.cnf file can (and should)
make it private by executing this command:
% chmod 600 .my.cnfAlthough this precaution can be left up to the discretion of individual MySQL users, an administrator who prefers a more proactive approach can run a program daily from cron that looks for .my.cnf files to make sure that each one has the proper ownership and a restrictive access mode. A sample Perl script that does this may be found in the Appendix at the end of this article.
The MySQL installation procedure creates a database named mysql
containing the grant tables that the server uses to determine
which MySQL accounts can do what. In particular, the user
table in the mysql database lists all valid accounts
and indicates which global privileges they have, if any. This
section provides some guidelines that you can use to evaluate
existing accounts, and that you should keep in mind when creating
new accounts. These guidelines apply to servers running on any
platform.
A general set of principles for securing the MySQL accounts listed
in your grant tables is as follows:
Anonymous MySQL accounts allow clients to connect to the server
without specifying a user name. Under Windows, the default accounts
may even have full access to any database managed by the server.
To remove anonymous accounts, connect to the server as the MySQL
root user to access the mysql database, then
issue the following statements:
mysql> DELETE FROM user WHERE User="; mysql> FLUSH PRIVILEGES;The DELETE statement removes accounts that have an empty value in the User column of the user table that lists MySQL accounts, and FLUSH PRIVILEGES tells the server to reload the grant tables so the changes take effect.
You may also want to check the other grant tables in the mysql
database for rows that have empty User values. Those
rows can be removed, too.
Make Sure MySQL Accounts Have Passwords
MySQL accounts that do not have passwords should either be removed
or assigned passwords. To find such accounts, look in the user
table of the mysql database using the following query:
mysql> SELECT * FROM user WHERE Password=";The accounts corresponding to any rows returned by this query are insecure and should be assigned passwords. To do this, you must specify both the user name and hostname associated with an account. Suppose the values in the User and Host columns for an account having no password are user_name and host_name. The statement to assign a password of new_pass to that account looks like this:
mysql> UPDATE user SET Password=PASSWORD('new_pass')
-> WHERE User='user_name' AND Host='host_name';
You can also use a SET PASSWORD statement:
mysql> SET PASSWORD FOR 'user_name'@'host_name'=PASSWORD('new_pass');
If you assign passwords with UPDATE, issue a FLUSH
PRIVILEGES statement afterward to tell the server to
reload the grant tables so the changes take effect.
A MySQL account is identified by both a user name and a hostname,
which are found in the User and Host columns
of the user table. The User value is the name
that a client must supply when connecting to the server. The Host
value indicates the host or hosts from which the user is allowed
to connect. If this is a literal hostname, the account is limited
to connections only from that host. If the hostname is a pattern
such as %.xyz.com or % that contains the '%'
wildcard character, the user can connect from any host in the
xyz.com domain or from any host at all.
From a security standpoint, literal Host values are best
and % is the worst. Accounts that have Host
values containing wildcards are more susceptible to attack than
accounts with literal Host values, because attackers
can attempt to connect from a broader range of machines. For example,
if an account has User and Host values of root
and %, it means that you can connect as root
from anywhere if you know the password. An attacker must guess
the password, but may attempt to do so by connecting from any
host. By contrast, if the host name is localhost, the
attacker can attempt to connect as the root user only
after first gaining access to the server host.
To find existing accounts that contain the '%' wildcard
character anywhere in the Host column of the user
table, use the following query:
mysql> SELECT * FROM user WHERE Host LIKE '%\%%';If you discover a hostname value that is overly broad (for example, very few accounts really require a value of % in the Host column), change it with UPDATE to something more restrictive. Then check the other grant tables for rows with the corresponding User and Host values and change the Host columns of any such rows as well. If you do modify any accounts, issue a FLUSH PRIVILEGES statement afterward to tell the server to reload the grant tables so the changes take effect.
When you create new accounts (with the GRANT statement),
avoid host values that contain wildcards, or at least constrain
them so they are only as broad as necessary. Hostname patterns
can be convenient for setting up an account that can be accessed
by a given user from a set of machines, but you shouldn't use
them unless you really need to. In particular, resist the temptation
to simply create all accounts with % as the hostname.
Don't Grant Global Privileges Unnecessarily
Global privileges are granted using *.* in the ON
clause of the GRANT statement:
GRANT privileges ON *.* TO ...However, in most cases, global privileges should be allowed only for the MySQL root user, and possibly for other accounts used by people that you trust. Global privileges should otherwise not be granted because they allow users to perform operations that may be dangerous. For example, the FILE privilege allows a user to read and write files on the database server (which includes stealing databases), the PROCESS privilege allows currently executing queries to be monitored, and the SHUTDOWN privilege allows a user to shut down the server.
If you discover an existing account with global privileges that
it does not need, you can remove them using the REVOKE
statement. To take away the FILE privilege from an account
with User and Host values of user_name
and host_name in the user table, the
REVOKE statement looks like this:
mysql> REVOKE FILE ON *.* FROM 'user_name'@'host_name';To revoke all global privileges, use this statement instead:
mysql> REVOKE ALL ON *.* FROM 'user_name'@'host_name';There is one exception to REVOKE ALL, which is that the GRANT OPTION privilege must be revoked explicitly:
mysql> REVOKE GRANT OPTION ON *.* FROM 'user_name'@'host_name';
Provide your MySQL users with the following guidelines to help
them avoid unnecessary exposure of their passwords when connecting
to the server:
The following script, chkmycnf.pl, can be used to look
for user-specific .my.cnf option files in the home directory
of each account listed in the /etc/passwd file. It makes
sure that the file is owned by the proper account and that the
mode disallows access to everyone but the file owner. It must
be run as root so that it can make any file ownership
of access mode changes that are necessary. Typical use is as a
daily cron job run from root's crontab
file.
#! /usr/bin/perl -w
# chkmycnf.pl - check user-specific .my.cnf files and make sure the
# ownership and modes are correct. Each file should be owned by the
# user in whose home directory the file is found. The mode should
# have the group and other permissions turned off. The script must
# be run as root.
use strict;
@ARGV = ("/etc/passwd");
while (<>)
{
my ($uid, $home) = (split (/:/, $_))[2,5];
my $cnf_file = "$home/.my.cnf";
next unless -f $cnf_file; # does account have .my.cnf file?
if ((stat ($cnf_file))[4] != $uid)
{
warn "Changing ownership of $cnf_file to $uid\n";
chown ($uid, (stat ($cnf_file))[5], $cnf_file);
}
my $mode = (stat ($cnf_file))[2];
if ($mode & 077) # test "group" and "other" access bits
{
warn sprintf ("Changing mode of %s from %o to %o\n",
$cnf_file, $mode, $mode & ~077);
chmod ($mode & ~077, $cnf_file);
}
}
exit (0);
The original version of this article was written for NuSphere
Corporation. The current version is an updated revision of the
original.