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 as published by
7# the Free Software Foundation; version 2 of the License.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335  USA
17
18config=".my.cnf.$$"
19command=".mysql.$$"
20output=".my.output.$$"
21
22trap "interrupt" 1 2 3 6 15
23
24rootpass=""
25echo_n=
26echo_c=
27basedir=
28defaults_file=
29defaults_extra_file=
30no_defaults=
31
32parse_arg()
33{
34  echo "$1" | sed -e 's/^[^=]*=//'
35}
36
37parse_arguments()
38{
39  # We only need to pass arguments through to the server if we don't
40  # handle them here.  So, we collect unrecognized options (passed on
41  # the command line) into the args variable.
42  pick_args=
43  if test "$1" = PICK-ARGS-FROM-ARGV
44  then
45    pick_args=1
46    shift
47  fi
48
49  for arg
50  do
51    case "$arg" in
52      --basedir=*) basedir=`parse_arg "$arg"` ;;
53      --defaults-file=*) defaults_file="$arg" ;;
54      --defaults-extra-file=*) defaults_extra_file="$arg" ;;
55      --no-defaults) no_defaults="$arg" ;;
56      *)
57        if test -n "$pick_args"
58        then
59          # This sed command makes sure that any special chars are quoted,
60          # so the arg gets passed exactly to the server.
61          # XXX: This is broken; true fix requires using eval and proper
62          # quoting of every single arg ($basedir, $ldata, etc.)
63          #args="$args "`echo "$arg" | sed -e 's,\([^a-zA-Z0-9_.-]\),\\\\\1,g'`
64          args="$args $arg"
65        fi
66        ;;
67    esac
68  done
69}
70
71# Try to find a specific file within --basedir which can either be a binary
72# release or installed source directory and return the path.
73find_in_basedir()
74{
75  return_dir=0
76  found=0
77  case "$1" in
78    --dir)
79      return_dir=1; shift
80      ;;
81  esac
82
83  file=$1; shift
84
85  for dir in "$@"
86  do
87    if test -f "$basedir/$dir/$file"
88    then
89      found=1
90      if test $return_dir -eq 1
91      then
92        echo "$basedir/$dir"
93      else
94        echo "$basedir/$dir/$file"
95      fi
96      break
97    fi
98  done
99
100  if test $found -eq 0
101  then
102      # Test if command is in PATH
103      $file --no-defaults --version > /dev/null 2>&1
104      status=$?
105      if test $status -eq 0
106      then
107        echo $file
108      fi
109  fi
110}
111
112cannot_find_file()
113{
114  echo
115  echo "FATAL ERROR: Could not find $1"
116
117  shift
118  if test $# -ne 0
119  then
120    echo
121    echo "The following directories were searched:"
122    echo
123    for dir in "$@"
124    do
125      echo "    $dir"
126    done
127  fi
128
129  echo
130  echo "If you compiled from source, you need to run 'make install' to"
131  echo "copy the software into the correct location ready for operation."
132  echo
133  echo "If you are using a binary release, you must either be at the top"
134  echo "level of the extracted archive, or pass the --basedir option"
135  echo "pointing to that location."
136  echo
137}
138
139# Ok, let's go.  We first need to parse arguments which are required by
140# my_print_defaults so that we can execute it first, then later re-parse
141# the command line to add any extra bits that we need.
142parse_arguments PICK-ARGS-FROM-ARGV "$@"
143
144#
145# We can now find my_print_defaults.  This script supports:
146#
147#   --srcdir=path pointing to compiled source tree
148#   --basedir=path pointing to installed binary location
149#
150# or default to compiled-in locations.
151#
152
153if test -n "$basedir"
154then
155  print_defaults=`find_in_basedir my_print_defaults bin extra`
156  echo "print: $print_defaults"
157  if test -z "$print_defaults"
158  then
159    cannot_find_file my_print_defaults $basedir/bin $basedir/extra
160    exit 1
161  fi
162  mysql_command=`find_in_basedir mysql bin`
163  if test -z "$mysql_command"
164  then
165    cannot_find_file mysql $basedir/bin
166    exit 1
167  fi
168else
169  print_defaults="@bindir@/my_print_defaults"
170  mysql_command="@bindir@/mysql"
171fi
172
173if test ! -x "$print_defaults"
174then
175  cannot_find_file "$print_defaults"
176  exit 1
177fi
178
179if test ! -x "$mysql_command"
180then
181  cannot_find_file "$mysql_command"
182  exit 1
183fi
184
185# Now we can get arguments from the group [client] and [client-server]
186# in the my.cfg file, then re-run to merge with command line arguments.
187parse_arguments `$print_defaults $defaults_file $defaults_extra_file $no_defaults client client-server client-mariadb`
188parse_arguments PICK-ARGS-FROM-ARGV "$@"
189
190set_echo_compat() {
191    case `echo "testing\c"`,`echo -n testing` in
192	*c*,-n*) echo_n=   echo_c=     ;;
193	*c*,*)   echo_n=-n echo_c=     ;;
194	*)       echo_n=   echo_c='\c' ;;
195    esac
196}
197
198validate_reply () {
199    ret=0
200    if [ -z "$1" ]; then
201	reply=y
202	return $ret
203    fi
204    case $1 in
205        y|Y|yes|Yes|YES) reply=y ;;
206        n|N|no|No|NO)    reply=n ;;
207        *) ret=1 ;;
208    esac
209    return $ret
210}
211
212prepare() {
213    touch $config $command
214    chmod 600 $config $command
215}
216
217do_query() {
218    echo "$1" >$command
219    #sed 's,^,> ,' < $command  # Debugging
220    $mysql_command --defaults-file=$config $defaults_extra_file $no_defaults $args <$command >$output
221    return $?
222}
223
224# Simple escape mechanism (\-escape any ' and \), suitable for two contexts:
225# - single-quoted SQL strings
226# - single-quoted option values on the right hand side of = in my.cnf
227#
228# These two contexts don't handle escapes identically.  SQL strings allow
229# quoting any character (\C => C, for any C), but my.cnf parsing allows
230# quoting only \, ' or ".  For example, password='a\b' quotes a 3-character
231# string in my.cnf, but a 2-character string in SQL.
232#
233# This simple escape works correctly in both places.
234basic_single_escape () {
235    # The quoting on this sed command is a bit complex.  Single-quoted strings
236    # don't allow *any* escape mechanism, so they cannot contain a single
237    # quote.  The string sed gets (as argv[1]) is:  s/\(['\]\)/\\\1/g
238    #
239    # Inside a character class, \ and ' are not special, so the ['\] character
240    # class is balanced and contains two characters.
241    echo "$1" | sed 's/\(['"'"'\]\)/\\\1/g'
242}
243
244#
245# create a simple my.cnf file to be able to pass the root password to the mysql
246# client without putting it on the command line
247#
248make_config() {
249    echo "# mysql_secure_installation config file" >$config
250    echo "[mysql]" >>$config
251    echo "user=root" >>$config
252    esc_pass=`basic_single_escape "$rootpass"`
253    echo "password='$esc_pass'" >>$config
254    #sed 's,^,> ,' < $config  # Debugging
255
256    if test -n "$defaults_file"
257    then
258        dfile=`parse_arg "$defaults_file"`
259        cat "$dfile" >>$config
260    fi
261}
262
263get_root_password() {
264    status=1
265    while [ $status -eq 1 ]; do
266	stty -echo
267	echo $echo_n "Enter current password for root (enter for none): $echo_c"
268	read password
269	echo
270	stty echo
271	if [ "x$password" = "x" ]; then
272	    emptypass=1
273	else
274	    emptypass=0
275	fi
276	rootpass=$password
277	make_config
278	do_query "show create user root@localhost"
279	status=$?
280    done
281    if grep -q unix_socket $output; then
282      emptypass=0
283    fi
284    echo "OK, successfully used password, moving on..."
285    echo
286}
287
288set_root_password() {
289    stty -echo
290    echo $echo_n "New password: $echo_c"
291    read password1
292    echo
293    echo $echo_n "Re-enter new password: $echo_c"
294    read password2
295    echo
296    stty echo
297
298    if [ "$password1" != "$password2" ]; then
299	echo "Sorry, passwords do not match."
300	echo
301	return 1
302    fi
303
304    if [ "$password1" = "" ]; then
305	echo "Sorry, you can't use an empty password here."
306	echo
307	return 1
308    fi
309
310    esc_pass=`basic_single_escape "$password1"`
311    do_query "UPDATE mysql.global_priv SET priv=json_set(priv, '$.plugin', 'mysql_native_password', '$.authentication_string', PASSWORD('$esc_pass')) WHERE User='root';"
312    if [ $? -eq 0 ]; then
313	echo "Password updated successfully!"
314	echo "Reloading privilege tables.."
315	reload_privilege_tables
316	if [ $? -eq 1 ]; then
317		clean_and_exit
318	fi
319	echo
320	rootpass=$password1
321	make_config
322    else
323	echo "Password update failed!"
324	clean_and_exit
325    fi
326
327    return 0
328}
329
330remove_anonymous_users() {
331    do_query "DELETE FROM mysql.global_priv WHERE User='';"
332    if [ $? -eq 0 ]; then
333	echo " ... Success!"
334    else
335	echo " ... Failed!"
336	clean_and_exit
337    fi
338
339    return 0
340}
341
342remove_remote_root() {
343    do_query "DELETE FROM mysql.global_priv WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');"
344    if [ $? -eq 0 ]; then
345	echo " ... Success!"
346    else
347	echo " ... Failed!"
348    fi
349}
350
351remove_test_database() {
352    echo " - Dropping test database..."
353    do_query "DROP DATABASE IF EXISTS test;"
354    if [ $? -eq 0 ]; then
355	echo " ... Success!"
356    else
357	echo " ... Failed!  Not critical, keep moving..."
358    fi
359
360    echo " - Removing privileges on test database..."
361    do_query "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'"
362    if [ $? -eq 0 ]; then
363	echo " ... Success!"
364    else
365	echo " ... Failed!  Not critical, keep moving..."
366    fi
367
368    return 0
369}
370
371reload_privilege_tables() {
372    do_query "FLUSH PRIVILEGES;"
373    if [ $? -eq 0 ]; then
374	echo " ... Success!"
375	return 0
376    else
377	echo " ... Failed!"
378	return 1
379    fi
380}
381
382interrupt() {
383    echo
384    echo "Aborting!"
385    echo
386    cleanup
387    stty echo
388    exit 1
389}
390
391cleanup() {
392    echo "Cleaning up..."
393    rm -f $config $command $output
394}
395
396# Remove the files before exiting.
397clean_and_exit() {
398	cleanup
399	exit 1
400}
401
402# The actual script starts here
403
404prepare
405set_echo_compat
406
407echo
408echo "NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB"
409echo "      SERVERS IN PRODUCTION USE!  PLEASE READ EACH STEP CAREFULLY!"
410echo
411echo "In order to log into MariaDB to secure it, we'll need the current"
412echo "password for the root user. If you've just installed MariaDB, and"
413echo "haven't set the root password yet, you should just press enter here."
414echo
415
416get_root_password
417
418
419#
420# Set the root password
421#
422
423echo "Setting the root password or using the unix_socket ensures that nobody"
424echo "can log into the MariaDB root user without the proper authorisation."
425echo
426
427while true ; do
428    if [ $emptypass -eq 1 ]; then
429	echo $echo_n "Enable unix_socket authentication? [Y/n] $echo_c"
430    else
431	echo "You already have your root account protected, so you can safely answer 'n'."
432	echo
433	echo $echo_n "Switch to unix_socket authentication [Y/n] $echo_c"
434    fi
435    read reply
436    validate_reply $reply && break
437done
438
439if [ "$reply" = "n" ]; then
440  echo " ... skipping."
441else
442  emptypass=0
443  do_query "UPDATE mysql.global_priv SET priv=json_set(priv, '$.password_last_changed', UNIX_TIMESTAMP(), '$.plugin', 'mysql_native_password', '$.authentication_string', 'invalid', '$.auth_or', json_array(json_object(), json_object('plugin', 'unix_socket'))) WHERE User='root';"
444  if [ $? -eq 0 ]; then
445   echo "Enabled successfully!"
446   echo "Reloading privilege tables.."
447   reload_privilege_tables
448   if [ $? -eq 1 ]; then
449     clean_and_exit
450   fi
451   echo
452  else
453   echo "Failed!"
454   clean_and_exit
455  fi
456fi
457echo
458
459while true ; do
460    if [ $emptypass -eq 1 ]; then
461	echo $echo_n "Set root password? [Y/n] $echo_c"
462    else
463	echo "You already have your root account protected, so you can safely answer 'n'."
464	echo
465	echo $echo_n "Change the root password? [Y/n] $echo_c"
466    fi
467    read reply
468    validate_reply $reply && break
469done
470
471if [ "$reply" = "n" ]; then
472    echo " ... skipping."
473else
474    status=1
475    while [ $status -eq 1 ]; do
476	set_root_password
477	status=$?
478    done
479fi
480echo
481
482
483#
484# Remove anonymous users
485#
486
487echo "By default, a MariaDB installation has an anonymous user, allowing anyone"
488echo "to log into MariaDB without having to have a user account created for"
489echo "them.  This is intended only for testing, and to make the installation"
490echo "go a bit smoother.  You should remove them before moving into a"
491echo "production environment."
492echo
493
494while true ; do
495    echo $echo_n "Remove anonymous users? [Y/n] $echo_c"
496    read reply
497    validate_reply $reply && break
498done
499if [ "$reply" = "n" ]; then
500    echo " ... skipping."
501else
502    remove_anonymous_users
503fi
504echo
505
506
507#
508# Disallow remote root login
509#
510
511echo "Normally, root should only be allowed to connect from 'localhost'.  This"
512echo "ensures that someone cannot guess at the root password from the network."
513echo
514while true ; do
515    echo $echo_n "Disallow root login remotely? [Y/n] $echo_c"
516    read reply
517    validate_reply $reply && break
518done
519if [ "$reply" = "n" ]; then
520    echo " ... skipping."
521else
522    remove_remote_root
523fi
524echo
525
526
527#
528# Remove test database
529#
530
531echo "By default, MariaDB comes with a database named 'test' that anyone can"
532echo "access.  This is also intended only for testing, and should be removed"
533echo "before moving into a production environment."
534echo
535
536while true ; do
537    echo $echo_n "Remove test database and access to it? [Y/n] $echo_c"
538    read reply
539    validate_reply $reply && break
540done
541
542if [ "$reply" = "n" ]; then
543    echo " ... skipping."
544else
545    remove_test_database
546fi
547echo
548
549
550#
551# Reload privilege tables
552#
553
554echo "Reloading the privilege tables will ensure that all changes made so far"
555echo "will take effect immediately."
556echo
557
558while true ; do
559    echo $echo_n "Reload privilege tables now? [Y/n] $echo_c"
560    read reply
561    validate_reply $reply && break
562done
563
564if [ "$reply" = "n" ]; then
565    echo " ... skipping."
566else
567    reload_privilege_tables
568fi
569echo
570
571cleanup
572
573echo
574echo "All done!  If you've completed all of the above steps, your MariaDB"
575echo "installation should now be secure."
576echo
577echo "Thanks for using MariaDB!"
578