Bash Tips: Passing Passwords
Bash offers any number of simplifications, especially when it comes to pesky individual steps in installation routines for larger software packages. Some caution is advisable, though, if you need to handle database and application passwords, which can easily be compromised.
From the administrator’s point of view, installing web applications always follows the same pattern: download and unpack the package, prepare the database, then introduce the two components to one another, typically in the form of a configuration file. To discover what entries you need – in what syntax and in which file – you traditionally read the README, but the installation can be faster for an administrator (and often less prone to error) if the developers include a shell script that takes the administrator by the hand and guides them through the settings.
This is the approach that the community surrounding Magento, a popular eCommence platform for PHP, takes. The website provides an installer in the form of a Bash script. The script (see an excerpt in Listing 1) follows a simple pattern; it queries whether the required MySQL database exists, asks for various connection information for the database, then downloads the required packages with wget. It then sets up a couple of permissions and feeds a schema to the database so you can launch the web application.
Listing 1: Magento Installer
01 #!/bin/bash 02 03 clear 04 05 echo "To install Magento, you will need a blank database ready with a user assigned to it." 06 07 echo -n "Database Host (usually localhost): " 08 read dbhost 09 10 echo -n "Database Name: " 11 read dbname 12 13 echo -n "Database User: " 14 read dbuser 15 16 echo -n "Database Password: " 17 read dbpass 18 19 echo -n "Store URL: " 20 read url 21 22 echo -n "Admin Username: " 23 read adminuser 24 25 echo -n "Admin Password: " 26 read adminpass 27 [...] 28 echo "Downloading and extracting packages ..." 29 30 wget http://www.magentocommerce.com/downloads/assets/22.214.171.124/magento-126.96.36.199.tar.gz 31 wget http://www.magentocommerce.com/downloads/assets/1.2.0/magento-sample-data-1.2.0.tar.gz 32 tar -zxvf magento-188.8.131.52.tar.gz 33 tar -zxvf magento-sample-data-1.2.0.tar.gz 34 [...] 35 echo "Importing sample products ..." 36 mysql -h $dbhost -u $dbuser -p$dbpass $dbname < data.sql 37 [...] 38 echo "Installing Magento ..." 39 40 php-cli -f install.php -- \ 41 --license_agreement_accepted "yes" \ 42 --db_host "$dbhost" \ 43 --db_name "$dbname" \ 44 --db_user "$dbuser" \ 45 --db_pass "$dbpass" \ 46 [...] 47 echo "Finished installing Magento"
Security-conscious administrators will have noticed a couple of details. In line 16, the script uses echo -n "password: **" to create a prompt that doesn’t cause a line break. The -n option keeps the cursor to the right of the output. The script then prompts for the database password. Some lines farther down, this pattern is repeated for the administrator account.
If you work in an open plan office or suspect that somebody is shoulder surfing, you will not be very happy to see the password entry echoed onto the screen. The Bash read command normally repeats all of your input on the standard output.
At this point, it could be useful to think about how these characters actually get where they are. If a user is sitting directly in front of the computer, the physical keyboard passes the key presses to the kernel. The kernel passes them to an X server, assuming an X11 interface is running, and the X server converts them into X events and sends them to the correct window; this will be a terminal emulation such as xterm or konsole.
Inside the terminal program you will typically have a shell like Bash running. The two programs are connected by a pipe that presents a pseudo-terminal device such as /dev/pts/0 on the terminal side and the standard input on the shell side. If a Bash user needs a file for this, they can use /proc/self/fd/0. Two other pipes, numbered 1 and 2, combine the two sides for the standard output and the standard error output in the same way (see Figure 1).
The terminal program is also responsible for showing the keys you press in the terminal, not in the shell. To try this out, you can issue the command
in the shell. The command then uses ioctl() to send a change to the terminal driver, which will not display any keys you press from now on. However, the shell will continue to process commands and create the normal output. To restore the normal state, you just need to enter the complementary command:
This means you can disable the echo in shell scripts and then reenable it after entering the password. To avoid the need to use this external command, Bash also has a built-in function that works in a similar way. The -s option in read also tells the shell to suppress the echo.
Bash has another couple of useful options here: for example, the -n <number> key defines the number of characters that read should read. You don’t even need a newline for this, which the shell always otherwise requires to terminate input. If you use a combination of read -n1 x, Bash will wait for a single key press and store this in the variable x.
Sometimes – in wild arcade shooters, for example – programmers only want to wait a limited time for input. The option for handling this is -t <seconds>. After the specified time, Bash processes the subsequent command. Then you need to check the exit status of the command with $? to discover whether or not you have any input. Listing 2 implements proof of concept in the form of a small race into space in retro design. The user can guide a small spaceship through an asteroid belt using the G and H keys, leaving a tail of exhaust gas in its wake.
Listing 2: Space Race in Bash
01 #!/bin/bash 02 03 pos=40 04 width=80 05 06 while true; do 07 read -t1 -s -n1 x 08 case "$x" in 09 g) pos=$(($pos - 1)) ;; 10 h) pos=$(($pos + 1)) ;; 11 esac 12 for ((i=0; i < $width; i++)) 13 do 14 if [ $i -eq $pos ]; then 15 echo -n 'V' 16 elif [ $(($RANDOM % 9)) -eq 0 ]; then 17 echo -n '*' 18 else 19 echo -n ' ' 20 fi 21 done 22 echo 23 done
The Magento installer, now improved by adding read -s, has another problem. In line 36 of Listing 1, the script calls MySQL to initialize the database and uses the -p$dbpass option to pass the password it queried previously into the database. Other users logged in to the same computer at run time can see these details in the clear in the process list by typing ps auxwww.
This explains why passwords should be banned from command-line arguments. An alternative when handing over data would be to use environmental variables, but you can access these in the same way with ps aueww. Absurdly, many programs implement this insecure method of launching MySQL via the MYSQL_PWD environmental variable. The best way to transfer a cleartext password will thus be in the configuration files, which the software reads, or, alternatively, with the use of pipes, such as standard input. Purists would comment that unencrypted passwords should never be stored in the clear and point instead to hashes or tokens, in the style of Kerberos. However, you might find it difficult to implement this with Bash alone.