1#!/bin/sh
2
3# Copyright (c) 2002, 2016, Oracle and/or its affiliates. All rights reserved.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License, version 2.0,
7# as published by the Free Software Foundation.
8#
9# This program is also distributed with certain software (including
10# but not limited to OpenSSL) that is licensed under separate terms,
11# as designated in a particular file or component or in included license
12# documentation.  The authors of MySQL hereby grant you an additional
13# permission to link the program and your derivative works with the
14# separately licensed software that they have included with MySQL.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License, version 2.0, for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
24
25config=".my.cnf.$$"
26command=".mysql.$$"
27mysql_client=""
28
29trap "interrupt" 1 2 3 6 15
30
31rootpass=""
32echo_n=
33echo_c=
34
35set_echo_compat() {
36    case `echo "testing\c"`,`echo -n testing` in
37	*c*,-n*) echo_n=   echo_c=     ;;
38	*c*,*)   echo_n=-n echo_c=     ;;
39	*)       echo_n=   echo_c='\c' ;;
40    esac
41}
42
43validate_reply () {
44    ret=0
45    if [ -z "$1" ]; then
46	reply=y
47	return $ret
48    fi
49    case $1 in
50        y|Y|yes|Yes|YES) reply=y ;;
51        n|N|no|No|NO)    reply=n ;;
52        *) ret=1 ;;
53    esac
54    return $ret
55}
56
57prepare() {
58    touch $config $command
59    chmod 600 $config $command
60}
61
62find_mysql_client()
63{
64  for n in ./bin/mysql mysql
65  do
66    $n --no-defaults --help > /dev/null 2>&1
67    status=$?
68    if test $status -eq 0
69    then
70      mysql_client=$n
71      return
72    fi
73  done
74  echo "Can't find a 'mysql' client in PATH or ./bin"
75  exit 1
76}
77
78do_query() {
79    echo "$1" >$command
80    #sed 's,^,> ,' < $command  # Debugging
81    $mysql_client --defaults-file=$config <$command
82    return $?
83}
84
85# Simple escape mechanism (\-escape any ' and \), suitable for two contexts:
86# - single-quoted SQL strings
87# - single-quoted option values on the right hand side of = in my.cnf
88#
89# These two contexts don't handle escapes identically.  SQL strings allow
90# quoting any character (\C => C, for any C), but my.cnf parsing allows
91# quoting only \, ' or ".  For example, password='a\b' quotes a 3-character
92# string in my.cnf, but a 2-character string in SQL.
93#
94# This simple escape works correctly in both places.
95basic_single_escape () {
96    # The quoting on this sed command is a bit complex.  Single-quoted strings
97    # don't allow *any* escape mechanism, so they cannot contain a single
98    # quote.  The string sed gets (as argv[1]) is:  s/\(['\]\)/\\\1/g
99    #
100    # Inside a character class, \ and ' are not special, so the ['\] character
101    # class is balanced and contains two characters.
102    echo "$1" | sed 's/\(['"'"'\]\)/\\\1/g'
103}
104
105make_config() {
106    echo "# mysql_secure_installation config file" >$config
107    echo "[mysql]" >>$config
108    echo "user=root" >>$config
109    esc_pass=`basic_single_escape "$rootpass"`
110    echo "password='$esc_pass'" >>$config
111    #sed 's,^,> ,' < $config  # Debugging
112}
113
114get_root_password() {
115    status=1
116    while [ $status -eq 1 ]; do
117	stty -echo
118	echo $echo_n "Enter current password for root (enter for none): $echo_c"
119	read password
120	echo
121	stty echo
122	if [ "x$password" = "x" ]; then
123	    hadpass=0
124	else
125	    hadpass=1
126	fi
127	rootpass=$password
128	make_config
129	do_query ""
130	status=$?
131    done
132    echo "OK, successfully used password, moving on..."
133    echo
134}
135
136set_root_password() {
137    stty -echo
138    echo $echo_n "New password: $echo_c"
139    read password1
140    echo
141    echo $echo_n "Re-enter new password: $echo_c"
142    read password2
143    echo
144    stty echo
145
146    if [ "$password1" != "$password2" ]; then
147	echo "Sorry, passwords do not match."
148	echo
149	return 1
150    fi
151
152    if [ "$password1" = "" ]; then
153	echo "Sorry, you can't use an empty password here."
154	echo
155	return 1
156    fi
157
158    esc_pass=`basic_single_escape "$password1"`
159
160    # attempt to lift the password expiration flag for root first
161    do_query "SET PASSWORD=PASSWORD('$esc_pass');"
162    if [ $? -ne 0 ]; then
163	echo "root password update failed!"
164	clean_and_exit
165    fi
166
167    # now since the password has changed, lets use the new one.
168    rootpass=$password1
169    make_config
170
171    # set password for all root users
172    # do the old password
173    do_query "SET @@old_passwords=1; UPDATE mysql.user SET Password=PASSWORD('$esc_pass'), password_expired='N' WHERE User='root' and plugin = 'mysql_old_password';"
174    if [ $? -ne 0 ]; then
175	echo "old password update failed!"
176	clean_and_exit
177    fi
178
179    # do the native password
180    do_query "SET @@old_passwords=0; UPDATE mysql.user SET Password=PASSWORD('$esc_pass'), password_expired='N' WHERE User='root' and plugin in ('', 'mysql_native_password');"
181    if [ $? -ne 0 ]; then
182	echo "native password update failed!"
183	clean_and_exit
184    fi
185
186    # do the sha256 password
187    do_query "SET @@old_passwords=2; UPDATE mysql.user SET authentication_string=PASSWORD('$esc_pass'), password_expired='N' WHERE User='root' and plugin = 'sha256_password';"
188    if [ $? -ne 0 ]; then
189	echo "sha256 password update failed!"
190	clean_and_exit
191    fi
192
193    echo "Password updated successfully!"
194    echo "Reloading privilege tables.."
195    reload_privilege_tables
196    if [ $? -eq 1 ]; then
197            clean_and_exit
198    fi
199    echo
200
201    return 0
202}
203
204remove_anonymous_users() {
205    do_query "DELETE FROM mysql.user WHERE User='';"
206    if [ $? -eq 0 ]; then
207	echo " ... Success!"
208    else
209	echo " ... Failed!"
210	clean_and_exit
211    fi
212
213    return 0
214}
215
216remove_remote_root() {
217    do_query "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"
218    if [ $? -eq 0 ]; then
219	echo " ... Success!"
220    else
221	echo " ... Failed!"
222    fi
223}
224
225remove_test_database() {
226    echo " - Dropping test database..."
227    do_query "DROP DATABASE test;"
228    if [ $? -eq 0 ]; then
229	echo " ... Success!"
230    else
231	echo " ... Failed!  Not critical, keep moving..."
232    fi
233
234    echo " - Removing privileges on test database..."
235    do_query "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'"
236    if [ $? -eq 0 ]; then
237	echo " ... Success!"
238    else
239	echo " ... Failed!  Not critical, keep moving..."
240    fi
241
242    return 0
243}
244
245reload_privilege_tables() {
246    do_query "FLUSH PRIVILEGES;"
247    if [ $? -eq 0 ]; then
248	echo " ... Success!"
249	return 0
250    else
251	echo " ... Failed!"
252	return 1
253    fi
254}
255
256interrupt() {
257    echo
258    echo "Aborting!"
259    echo
260    cleanup
261    stty echo
262    exit 1
263}
264
265cleanup() {
266    echo "Cleaning up..."
267    rm -f $config $command
268}
269
270# Remove the files before exiting.
271clean_and_exit() {
272	cleanup
273	exit 1
274}
275
276# The actual script starts here
277
278prepare
279find_mysql_client
280set_echo_compat
281
282echo
283echo
284echo
285echo
286echo "NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL"
287echo "      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!"
288echo
289echo
290
291echo "In order to log into MySQL to secure it, we'll need the current"
292echo "password for the root user.  If you've just installed MySQL, and"
293echo "you haven't set the root password yet, the password will be blank,"
294echo "so you should just press enter here."
295echo
296
297get_root_password
298
299
300#
301# Set the root password
302#
303
304echo "Setting the root password ensures that nobody can log into the MySQL"
305echo "root user without the proper authorisation."
306echo
307
308while true ; do
309    if [ $hadpass -eq 0 ]; then
310	echo $echo_n "Set root password? [Y/n] $echo_c"
311    else
312	echo "You already have a root password set, so you can safely answer 'n'."
313	echo
314	echo $echo_n "Change the root password? [Y/n] $echo_c"
315    fi
316    read reply
317    validate_reply $reply && break
318done
319
320if [ "$reply" = "n" ]; then
321    echo " ... skipping."
322else
323    status=1
324    while [ $status -eq 1 ]; do
325	set_root_password
326	status=$?
327    done
328fi
329echo
330
331
332#
333# Remove anonymous users
334#
335
336echo "By default, a MySQL installation has an anonymous user, allowing anyone"
337echo "to log into MySQL without having to have a user account created for"
338echo "them.  This is intended only for testing, and to make the installation"
339echo "go a bit smoother.  You should remove them before moving into a"
340echo "production environment."
341echo
342
343while true ; do
344    echo $echo_n "Remove anonymous users? [Y/n] $echo_c"
345    read reply
346    validate_reply $reply && break
347done
348if [ "$reply" = "n" ]; then
349    echo " ... skipping."
350else
351    remove_anonymous_users
352fi
353echo
354
355
356#
357# Disallow remote root login
358#
359
360echo "Normally, root should only be allowed to connect from 'localhost'.  This"
361echo "ensures that someone cannot guess at the root password from the network."
362echo
363while true ; do
364    echo $echo_n "Disallow root login remotely? [Y/n] $echo_c"
365    read reply
366    validate_reply $reply && break
367done
368if [ "$reply" = "n" ]; then
369    echo " ... skipping."
370else
371    remove_remote_root
372fi
373echo
374
375
376#
377# Remove test database
378#
379
380echo "By default, MySQL comes with a database named 'test' that anyone can"
381echo "access.  This is also intended only for testing, and should be removed"
382echo "before moving into a production environment."
383echo
384
385while true ; do
386    echo $echo_n "Remove test database and access to it? [Y/n] $echo_c"
387    read reply
388    validate_reply $reply && break
389done
390
391if [ "$reply" = "n" ]; then
392    echo " ... skipping."
393else
394    remove_test_database
395fi
396echo
397
398
399#
400# Reload privilege tables
401#
402
403echo "Reloading the privilege tables will ensure that all changes made so far"
404echo "will take effect immediately."
405echo
406
407while true ; do
408    echo $echo_n "Reload privilege tables now? [Y/n] $echo_c"
409    read reply
410    validate_reply $reply && break
411done
412
413if [ "$reply" = "n" ]; then
414    echo " ... skipping."
415else
416    reload_privilege_tables
417fi
418echo
419
420cleanup
421
422echo
423echo
424echo
425echo "All done!  If you've completed all of the above steps, your MySQL"
426echo "installation should now be secure."
427echo
428echo "Thanks for using MySQL!"
429echo
430echo
431
432
433