Chapter 14. Writing Secure Shell Scripts

Writing secure shell scripts?! How can shell scripts be secure when you can read the source code?

Any system that depends on concealing implementation details is attempting to use security by obscurity, and that is no security at all. Just ask the major software manufacturers whose source code is a closely guarded trade secret, yet whose products are incessantly vulnerable to exploits written by people who have never seen that source code. Contrast that with the code from OpenSSH and OpenBSD, which is totally open, yet very secure.

Security by obscurity will never work for long, though some forms of it can be a useful additional layer of security. For example, having daemons assigned to listen on nonstandard port numbers will keep a lot of the so-called script-kiddies away. But security by obscurity must never be the only layer of security because sooner or later, someone is going to discover whatever you’ve hidden.

As Bruce Schneier says, security is a process. It’s not a product, object, or technique, and it is never finished. As technology, networks, attacks and defenses evolve, so must your security process. So what does it mean to write secure shell scripts?

Secure shell scripts will reliably do what they are supposed to do, and only what they are supposed to do. They won’t lend themselves to being exploited to gain root access, they won’t accidentally rm -rf /, and they won’t leak information, such as passwords. They will be robust, but will fail gracefully. They will tolerate inadvertent user mistakes and sanitize all user input. They will be as simple as possible, and contain only clear, readable code and documentation so that the intention of each line is unambiguous.

That sounds a lot like any well-designed, robust program, doesn’t it? Security should be part of any good design process from the start—it shouldn’t be tacked on at the end. In this chapter we highlighted the most common security weaknesses and questions, and show you how to tackle them.

A lot has been written about security over the years. If you’re interested, Practical UNIX & Internet Security, 3rd Edition, by Gene Spafford et al. (O’Reilly) is a good place to start. Chapter 15 of Classic Shell Scripting by Nelson H. F. Beebe and Arnold Robbins (O’Reilly) is another excellent resource. There are also many good online references, such as “A Lab engineer’s check list for writing secure Unix code”.

The listing in Example 14-1 collects the most universal of the secure shell programming techniques, so they are all in one place as a quick reference when you need them or to copy into a script template. Be sure to read the full recipe for each technique so you understand it.

Example 14-1. ch14/security_template
#!/usr/bin/env bash
# cookbook filename: security_template

# Set a sane/secure path
PATH='/usr/local/bin:/bin:/usr/bin'
# It's almost certainly already marked for export, but make sure
\export PATH

# Clear all aliases. Important: leading \ inhibits alias expansion.
\unalias -a

# Clear the command path hash
hash -r

# Set the hard limit to 0 to turn off core dumps
ulimit -H -c 0 --

# Set a sane/secure IFS (note this is bash & ksh93 syntax only--not portable!)
IFS=$' \t\n'

# Set a sane/secure umask variable and use it
# Note this does not affect files already redirected on the command line
# 022 results in 0755 perms, 077 results in 0700 perms, etc.
UMASK=022
umask $UMASK

until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; do
    temp_dir="/tmp/meaningful_prefix.${RANDOM}${RANDOM}${RANDOM}"
done
mkdir -p -m 0700 $temp_dir \
  || (echo "FATAL: Failed to create temp dir '$temp_dir': $?"; exit 100)

# Do our best to clean up temp files no matter what
# Note $temp_dir must be set before this, and must not change!
cleanup="rm -rf $temp_dir"
trap "$cleanup" ABRT EXIT HUP INT QUIT

14.1 Avoiding Common Security Problems

Problem

You want to avoid common security problems in your scripting.

Solution

Validate all external input, including interactive input and that from configuration files and interactive use. In particular, never eval input that you have not checked very thoroughly.

Use secure temporary files, ideally in secure temporary directories.

Make sure you are using trusted external executables.

Discussion

In a way, this recipe barely scratches the surface of scripting and system security. Yet it also covers the most common security problems you’ll find.

Data validation, or rather the lack of it, is a huge deal in computer security right now. This is the problem that leads to buffer overflow and data injection attacks, which are by far the most common classes of exploit going around. bash doesn’t suffer from this issue in the same way that C does, but the concepts are the same. In the bash world it’s more likely that unvalidated input will contain something like ;rm -rf / than a buffer overflow; however, neither is welcome. Validate your data!

Race conditions are another big issue, closely tied to the problem of an attacker gaining an ability to write over unexpected files. A race condition exists when two or more separate events must occur in the correct order at the correct time without external interference. They often result in providing an unprivileged user with read and/or write access to files that user shouldn’t be able to access, which in turn can result in so-called privilege escalation, where an ordinary user can gain root access. Insecure use of temporary files is a very common factor in this kind of attack. Using secure temporary files, especially inside secure temporary directories, will eliminate this attack vector.

Another common attack vector is Trojaned utilities. Like the Trojan horse, these appear to be one thing while they are in fact something else. The canonical example here is the Trojaned ls command that works just like the real ls command except when run by root. In that case it creates a new user called r00t, with a default password known to the attacker, and deletes itself. Using a secure $PATH is about the best you can do from the scripting side. From the systems side there are many tools, such as FCheck, Tripwire, and AIDE, to help you assure system integrity.

14.2 Avoiding Interpreter Spoofing

Problem

You want to avoid certain kinds of setuid root spoofing attacks.

Solution

Pass a single trailing dash to the shell, as in:

#!/bin/bash -

Discussion

The first line of a script is a magic line (often called the shebang line) that tells the kernel what interpreter to use to process the rest of the file. The kernel will also look for a single option to the specified interpreter. There are some attacks that take advantage of this fact, but if you pass an argument along, they are avoided. See section 4.7 of the Unix FAQs for details.

However, hardcoding the path to bash may present a portability issue. See Recipe 15.1 for details.

14.3 Setting a Secure $PATH

Problem

You want to make sure you are using a secure path.

Solution

Set $PATH to a known good state at the beginning of every script:

# Set a sane/secure path
PATH='/usr/local/bin:/bin:/usr/bin'
# It's almost certainly already marked for export, but make sure
export PATH

Or use the getconf utility to get a path guaranteed by POSIX to find all of the standard utilities:

export PATH=$(getconf PATH)

Discussion

There are two portability problems with the second example. First, $() is less portable (if more readable) than ``. Second, having the export command on the same line as the variable assignment won’t always work across every old or odd version of Unix and Linux. var='foo'; export var is more portable than export var='foo'. Also note that the export command need only be used once to flag a variable to be exported to child processes.

If you don’t use getconf, our example is a good default path for starters, though you may need to adjust it for your particular environment or needs. You might also use the less portable version:

export PATH='/usr/local/bin:/bin:/usr/bin'

Depending on your security risk and needs, you should also consider using absolute paths. This tends to be cumbersome and can be an issue where portability is concerned, as different operating systems put tools in different places. One way to mitigate these issues to some extent is to use variables. If you do this, sort them so you don’t end up with the same command three times because you missed it scanning the unsorted list. See Example 14-2 for an example.

Example 14-2. ch14/finding_tools
#!/usr/bin/env bash
# cookbook filename: finding_tools

# Export may or may not also be needed, depending on what you are doing

# These are fairly safe bets
_cp='/bin/cp'
_mv='/bin/mv'
_rm='/bin/rm'

# These are a little trickier
case $(/bin/uname) in
    'Linux')
        _cut='/bin/cut'
        _nice='/bin/nice'
        # [...]
    ;;
    'SunOS')
        _cut='/usr/bin/cut'
        _nice='/usr/bin/nice'
        # [...]
    ;;
    # [...]
esac

One other advantage of this method is that it makes it very easy to see exactly what tools your script depends on, and you can even add a simple function to make sure that each tool is available and executable before your script really gets going.

Warning

Be careful about the variable names you use. Some programs, like InfoZip, use environment variables such as $ZIP and $UNZIP to pass settings to the program itself, so if you do something like ZIP='/usr/bin/zip', you can spend days pulling your hair out wondering why it works fine from the command line, but not in your script. Trust us. We learned this one the hard way. Also RTFM.

14.4 Clearing All Aliases

Problem

You need to make sure that there are no malicious aliases in your environment for security reasons.

Solution

Use the \unalias -a command to unalias any existing aliases.

Discussion

If an attacker can trick root or even another user into running a command, they will be able to gain access to data or privileges they shouldn’t have. One way to trick another user into running a malicious program is to create an alias to some other common program (e.g., ls).

The leading \, which suppresses alias expansion, is very important because without it you can be tricked, like this:

$ alias unalias=echo

$ alias builtin=ls

$ builtin unalias vi
ls: unalias: No such file or directory
ls: vi: No such file or directory

$ unalias -a
-a
Warning

As Chet says, “This is a tricky problem”:

Since the shell finds shell functions before builtins in command search, and allows functions to be exported in the environment, it might be worth stressing to use builtin before every builtin, use command to skip function lookup, or just unset every function you might be interested in:

_OLD=$POSIXLY_CORRECT; POSIXLY_CORRECT=1
\unset -f builtin command unset
POSIXLY_CORRECT=$_OLD ; \unset _OLD
builtin unalias command builtin unset
unset -f $(command declare -F \
 | command sed 's/^declare -f //')

Or consider unsetting the expand_aliases option, in which case you have to do the unset/unalias dance for shopt as well.

Chet Ramey

14.5 Clearing the Command Hash

Problem

You need to make sure that your command hash has not been subverted.

Solution

Use the hash -r command to clear entries from the command hash.

Discussion

On execution, bash “remembers” the location of most commands found in the $PATH to speed up subsequent invocations.

If an attacker can trick root or even another user into running a command, they will be able to gain access to data or privileges they shouldn’t have. One way to trick another user into running a malicious program is to poison the hash so that the wrong program may be run.

14.6 Preventing Core Dumps

Problem

You want to prevent your script from dumping core in the case of an unrecoverable error, since core dumps may contain sensitive data from memory such as passwords.

Solution

Use the bash builtin ulimit to set the core file size limit to 0, typically in your .bashrc file:

ulimit -H -c 0 --

Discussion

Core dumps are intended for debugging and contain an image of the memory used by the process at the time it failed. As such, the file will contain anything the process had stored in memory (e.g., user-entered passwords).

Set this in a system-level file such as /etc/profile or /etc/bashrc to which users have no write access if you don’t want them to be able to change it.

See Also

  • help ulimit

14.7 Setting a Secure $IFS

Problem

You want to make sure your internal field separator environment variable is clean.

Solution

Set it to a known good state at the beginning of every script using this clear (but not POSIX-compliant) syntax:

# Set a sane/secure IFS (note this is bash & ksh93 syntax only--not portable!)
IFS=$' \t\n'

Discussion

As noted, this syntax is not portable. However, the canonical portable syntax is unreliable because it may easily be inadvertently stripped by editors that trim whitespace. The values are traditionally space, tab, newline—and the order is important. $*, which returns all positional parameters, the special ${!prefix@} and ${!prefix*} parameter expansions, and programmable completion all use the first value of $IFS as their separator.

The typical method for writing leaves a trailing space and tab (indicated here by the dot and arrow) on the first line:

1 IFS=' • → ¶
2 '

Newline, space, tab is less likely to be trimmed, but that changes the default order, which may result in unexpected results from some commands:

1 IFS=' ¶
2 • → '

14.8 Setting a Secure umask

Problem

You want to make sure you are using a secure umask.

Solution

Use the bash builtin umask to set a known good state at the beginning of every script:

# Set a sane/secure umask variable and use it
# Note this does not affect files already redirected on the command line
# 002 results in 0775 perms, 077 results in 0700 perms, etc.
UMASK=002
umask $UMASK

Discussion

We set the $UMASK variable in case we need to use different masks elsewhere in the program. You could just as easily skip it and do the following—it’s not a big deal:

umask 002
Tip

Remember that umask is a mask that specifies the bits to be taken away from the default permissions of 777 for directories and 666 for files. When in doubt, test it out:

# Run a new shell so you don't affect your current
# environment
/tmp$ bash

# Check the current settings
/tmp$ touch um_current

# Check some other settings
/tmp$ umask 000 ; touch um_000
/tmp$ umask 022 ; touch um_022
/tmp$ umask 077 ; touch um_077

/tmp$ ls -l um_*
-rw-rw-rw-   1 jp   jp  0 Jul 22 06:05 um000
-rw-r--r--   1 jp   jp  0 Jul 22 06:05 um022
-rw-------   1 jp   jp  0 Jul 22 06:05 um077
-rw-rw-r--   1 jp   jp  0 Jul 22 06:05 umcurrent

# Clean up and exit the subshell
/tmp$ rm um_*
/tmp$ exit

14.9 Finding World-Writable Directories in Your $PATH

Problem

You want to make sure that there are no world-writable directories in root’s $PATH. (To see why, read Recipe 14.10.)

Solution

Use the simple script in Example 14-3 to check your $PATH. Use it in conjunction with su or sudo to check paths for other users.

Example 14-3. ch14/chkpath.1
#!/usr/bin/env bash
# cookbook filename: chkpath.1
# Check your $PATH for world-writable or missing directories

exit_code=0

for dir in ${PATH//:/ }; do
    [ -L "$dir" ] && printf "%b" "symlink, "
    if [ ! -d "$dir" ]; then
        printf "%b" "missing\t\t"
          (( exit_code++ ))
    elif [ -n "$(ls -lLd $dir | grep '^d.......w. ')" ]; then
          printf "%b" "world writable\t"
          (( exit_code++ ))
    else
          printf "%b" "ok\t\t"
    fi
    printf "%b" "$dir\n"
done
exit $exit_code

For example:

# ./chkpath
ok              /usr/local/sbin
ok              /usr/local/bin
ok              /sbin
ok              /bin
ok              /usr/sbin
ok              /usr/bin
ok              /usr/X11R6/bin
ok              /root/bin
missing         /does_not_exist
world writable  /tmp
symlink, world writable /tmp/bin
symlink, ok /root/sbin
#

Discussion

We convert the $PATH to a space-delimited list using the technique from Recipe 9.11, test for symbolic links (-L), and make sure the directory actually exists (-d). Then we get a long directory listing (-l), dereferencing symbolic links (-L) and listing the directory name only (-d), not the directory’s contents. Then we finally get to grep for world-writable directories.

As you can see, we spaced out the ok directories, while directories with a problem may get a little cluttered. We also broke the usual rule of Unix tools being quiet unless there’s a problem, because we felt it was a useful opportunity to see exactly what is in your path and give it a once-over in addition to the automated check.

We also provide an exit code of zero on success with no problems detected in the $PATH, or the count of errors found. With a little more tweaking, as in Example 14-4, we can add the file’s mode, owner, and group into the output, which might be even more valuable to check.

Example 14-4. ch14/chkpath.2
#!/usr/bin/env bash
# cookbook filename: chkpath.2
# Check your $PATH for world-writable or missing directories, with 'stat'

exit_code=0

for dir in ${PATH//:/ }; do
    [ -L "$dir" ] && printf "%b" "symlink, "
    if [ ! -d "$dir" ]; then
        printf "%b" "missing\t\t\t\t"
        (( exit_code++ ))
    else
        stat=$(ls -lHd $dir | awk '{print $1, $3, $4}')
        if [ -n "$(echo $stat | grep '^d.......w. ')" ]; then
            printf "%b" "world writable\t$stat "
            (( exit_code++ ))
        else
            printf "%b" "ok\t\t$stat "
        fi
    fi
    printf "%b" "$dir\n"

done
exit $exit_code

For example:

# ./chkpath ; echo $?
ok              drwxr-xr-x root root /usr/local/sbin
ok              drwxr-xr-x root root /usr/local/bin
ok              drwxr-xr-x root root /sbin
ok              drwxr-xr-x root root /bin
ok              drwxr-xr-x root root /usr/sbin
ok              drwxr-xr-x root root /usr/bin
ok              drwxr-xr-x root root /usr/X11R6/bin
ok              drwx------ root root /root/bin
missing                         /does_not_exist
world writable drwxrwxrwt root root /tmp
symlink, ok            drwxr-xr-x root root /root/sbin
2
#

14.10 Adding the Current Directory to the $PATH

Problem

Having to type ./script (the leading dot-slash all the time) is tedious, and you’d rather just add . (or an empty directory, meaning a leading or trailing : or a :: in the middle) to your $PATH.

Solution

We advise against doing this for any user, but we strongly advise against doing it for root. If you absolutely must do this, make sure the . comes last. Never do it as root.

Discussion

As you know, the shell searches the directories listed in $PATH when you enter a command name without a path. The reason not to add . is the same reason not to allow world-writable directories in your $PATH (see Recipe 14.9 for how to find these).

Say you are in /tmp and have . as the first thing in your $PATH. If you type ls and there happens to be a file called /tmp/ls, you will run that file instead of the /bin/ls you meant to run. Now what? Well, it depends. It’s possible (even likely, given the name) that /tmp/ls is a malicious script, and if you have just run it as root there is no telling what it could do, up to and including deleting itself when it’s finished to remove the evidence.

So what if you put it last? Well, have you ever typed mc instead of mv? We have. So unless Midnight Commander is installed on your system, you could accidentally run ./mc when you meant /bin/mv, with the same consequences as just described.

Just say no to dot!

14.11 Using Secure Temporary Files

Problem

You need to create a temporary file or directory, but are aware of the security implications of using a predictable name.

Solution

Try using echo "~$TMPDIR~" to see if your system provides a secure temporary directory. We’re using the ~s as brackets so you see something if the variable is not set.

The easy, portable, and “usually good enough” solution is to just use $RANDOM inline in your script. For example:

# Make sure $TMP is set to something
[ -n "$TMP" ] || TMP='/tmp'

# Make a "good enough" random temp directory
until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; do
    temp_dir="/$TMP/meaningful_prefix.${RANDOM}${RANDOM}${RANDOM}"
done
mkdir -p -m 0700 $temp_dir
  || { echo "FATAL: Failed to create temp dir '$temp_dir': $?"; exit 100 }
  # Make a "good enough" random temp file
  until [ -n "$temp_file" -a ! -e "$temp_file" ]; do
      temp_file="/$TMP/meaningful_prefix.${RANDOM}${RANDOM}${RANDOM}"
done
touch $temp_file && chmod 0600 $temp_file
  || { echo "FATAL: Failed to create temp file '$temp_file': $?"; exit 101 }

Even better, use both a random temporary directory and a random filename, as in Example 14-5!

Example 14-5. ch14/make_temp
# cookbook filename: make_temp

# Make sure $TMP is set to something
[ -n "$TMP" ] || TMP='/tmp'

# Make a "good enough" random temp directory
until [ -n "$temp_dir" -a ! -d "$temp_dir" ]; do
    temp_dir="/$TMP/meaningful_prefix.${RANDOM}${RANDOM}${RANDOM}"
done
mkdir -p -m 0700 $temp_dir \
  || { echo "FATAL: Failed to create temp dir '$temp_dir': $?"; exit 100; }

# Make a "good enough" random temp file in the temp dir
temp_file="$temp_dir/meaningful_prefix.${RANDOM}${RANDOM}${RANDOM}"
touch $temp_file && chmod 0600 $temp_file \
  || { echo "FATAL: Failed to create temp file '$temp_file': $?"; exit 101; }

No matter how you do it, don’t forget to set a trap to clean up (Example 14-6). As noted, $temp_dir must be set before this trap is declared, and its value must not change. If those things aren’t true, rewrite the logic to account for your needs.

Example 14-6. ch14/clean_temp
# cookbook filename: clean_temp

# Do our best to clean up temp files no matter what
# Note $temp_dir must be set before this, and must not change!
cleanup="rm -rf $temp_dir"
trap "$cleanup" ABRT EXIT HUP INT QUIT
Warning

$RANDOM is not available in dash, which is /bin/sh in some Linux distributions. Notably, current versions of Debian and Ubuntu use dash because it is smaller and faster than bash and thus helps to boot faster. But that means that /bin/sh, which used to be a symlink to bash, is now a symlink to dash instead, and various bash-specific features will not work.

Discussion

$RANDOM has been available since at least bash 2.0, and using it is probably good enough. Simple code is better and easier to secure than complicated code, so using $RANDOM may make your code more secure than having to deal with the validation and error-checking complexities of mktemp or /dev/urandom. You may also tend to use it more because it is so simple. However, $RANDOM provides only numbers, while mktemp provides numbers and upper- and lowercase letters, and urandom provides numbers and lowercase letters, thus vastly increasing the key space.

However you create it, using a temporary directory in which to work has the following advantages:

  • mkdir -p -m 0700 $temp_dir avoids the race condition inherent in touch $temp_ file && chmod 0600 $temp_file.

  • Files created inside the directory are not even visible to a non-root attacker outside the directory when 0700 permissions are set.

  • A temporary directory makes it easy to ensure all of your temporary files are removed at exit. If you have temp files scattered about, there’s always a chance of forgetting one when cleaning up.

  • You can choose to use meaningful names for temp files inside such a directory, which may make development and debugging easier, and thus improve script security and robustness.

  • Use of a meaningful prefix in the path makes it clear what scripts are running (this may be good or bad, but consider that ps or /proc do the same thing). More importantly, it might highlight a script that has failed to clean up after itself, which could possibly lead to an information leak.

Example 14-5 advises using a meaningful_prefix in the pathname you are creating. Some people will undoubtedly argue that since that is predictable, it reduces the security. It’s true that part of the path is predictable, but we still feel the advantages we’ve outlined outweigh this objection. If you still disagree, simply omit the meaningful prefix.

Depending on your risk and security needs, you may want to use random temporary files inside the random temporary directory, as we did in our example. That will probably not do anything to materially increase security, but if it makes you feel better, go for it.

We talked about a race condition in touch $temp_file&&chmod 0600$temp_file. One way to avoid that is to do this:

saved_umask=$(umask)
umask 077
touch $temp_file
umask $saved_umask
unset saved_umask

We recommended using both a random temporary directory and a random (or semi-random) filename since it provides more overall benefits.

If the numeric-only nature of $RANDOM really bothers you, consider combining some other sources of pseudounpredictable and pseudorandom data and a hash function:

nice_long_random_string=$( (last ; who ; netstat -a ; free ; date \
 ; echo $RANDOM) | md5sum | cut -d' ' -f1 )
Warning

We do not recommend using the fallback method shown here because the additional complexity is probably a cure that is worse than the disease. But it’s an interesting look at a way to make things a lot harder than they need to be.

A theoretically more secure approach is to use the mktemp utility present on many modern systems, with a fallback to /dev/urandom, also present on many modern systems, or even $RANDOM. The problem is that mktemp and /dev/urandom are not always available, and dealing with that in practice in a portable way is much more complicated than our solution. Example 14-7 is one way it could look, but try to use something simpler if possible.

Example 14-7. ch14/MakeTemp
# cookbook filename: MakeTemp
# Function to incorporate or source into another script
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Try to create a secure temp file name or directory
# Called like: $temp_file=$(MakeTemp <file|dir> [path/to/name-prefix])
# Returns the name of secure temp file name or directory in $TEMP_NAME
# For example:

#        $temp_dir=$(MakeTemp dir /tmp/$PROGRAM.foo)
#        $temp_file=$(MakeTemp file /tmp/$PROGRAM.foo)
#
function MakeTemp {

    # Make sure $TMP is set to something
    [ -n "$TMP" ] || TMP='/tmp'

    local type_name=$1
    local prefix=${2:-$TMP/temp} # Unless prefix is defined, use $TMP + temp
    local temp_type=''
    local sanity_check=''

    case $type_name in
        file )
            temp_type=''
            ur_cmd='touch'
            #                  -f Regular file  -r Readable
            #                  -w Writable      -O Owned by me
            sanity_check='test -f $TEMP_NAME -a -r $TEMP_NAME \
                            -a -w $TEMP_NAME -a -O $TEMP_NAME'
            ;;
        dir|directory )
            temp_type='-d'
            ur_cmd='mkdir -p -m0700'
            #                  -d Directory     -r Readable
            #                  -w Writable       -x Searchable   -O Owned by me
            sanity_check='test -d $TEMP_NAME -a -r $TEMP_NAME \
                            -a -w $TEMP_NAME -a -x $TEMP_NAME -a -O $TEMP_NAME'
            ;;
        * ) Error "\nBad type in $PROGRAM:MakeTemp! Needs file|dir." 1 ;;
    esac

    # First try mktemp
    TEMP_NAME=$(mktemp $temp_type ${prefix}.XXXXXXXXX)

    # If that fails try urandom, if that fails give up
    if [ -z "$TEMP_NAME" ]; then
        TEMP_NAME="${prefix}.$(cat /dev/urandom | od -x | tr -d ' ' | head -1)"
        $ur_cmd $TEMP_NAME
    fi

    # Make sure the file or directory was actually created, or DIE
    if ! eval $sanity_check; then
        Error \
        "\aFATAL ERROR: can't make temp $type_name with '$0:MakeTemp$*'!\n" 2
    else
        echo "$TEMP_NAME"
    fi

} # end of function MakeTemp

14.12 Validating Input

Problem

You’ve asked for input (e.g., from a user or a program), and to ensure security or data integrity you need to make sure you got what you asked for.

Solution

There are various ways to validate your input, depending on what the input is and how strict you need to be.

Use pattern matching for simple “it matches or it doesn’t” situations (see Recipes 6.6, 6.7, and 6.8):

[[ "$raw_input" == *.jpg ]] && echo "Got a JPEG file."

Use a case statement, as in Example 14-8, when there are various things that might be valid (see Recipes 6.14 and 6.15).

Example 14-8. ch14/validate_using_case
# cookbook filename: validate_using_case

case $raw_input in
    *.company.com        ) # Probably a local hostname
        ;;
    *.jpg                ) # Probably a JPEG file
        ;;
    *.[jJ][pP][gG]       ) # Probably a JPEG file, case-insensitive
        ;;
    foo | bar            ) # Entered 'foo' or 'bar
        ;;
    [0-9][0-9][0-9]      ) # A 3-digit number
        ;;
    [a-z][a-z][a-z][a-z] ) # A 4-lowercase-char word
        ;;
    *                    ) # None of the above
        ;;
esac

Use a regular expression when pattern matching isn’t specific enough and you have bash version 3.0+ (see Recipe 6.8). This example is looking for a three- to six-alphanumeric-character filename with a .jpg extension (case-sensitive):

[[ "$raw_input" =~ [[:alpha:]]{3,6}\.jpg ]] && echo "Got a JPEG file."

Discussion

For a larger and more detailed example, see the examples/scripts/shprompt in a recent bash tarball. Note this was written by Chet Ramey, who maintains bash:

# shprompt -- give a prompt and get an answer satisfying certain criteria
#
# shprompt [-dDfFsy] prompt
#    s = prompt for string
#    f = prompt for filename
#    F = prompt for full pathname to a file or directory
#    d = prompt for a directory name
#    D = prompt for a full pathname to a directory
#    y = prompt for y or n answer
#
# Chet Ramey
# chet@ins.CWRU.Edu

For a similar example, see examples/scripts.noah/y_or_n_p.bash, written circa 1993 by Noah Friedman and later converted to bash version 2 syntax by Chet Ramey. Also in the examples, see ./functions/isnum2 and ./functions/isvalidip.

14.13 Setting Permissions

Problem

You want to set permissions in a secure manner.

Solution

If you need to set exact permissions for security reasons (or you are sure that you don’t care what is already there, and you just need to change it), use chmod with four-digit octal modes:

chmod 0755 some_script

If you only want to add or remove permissions, but need to leave other existing permissions unchanged, use the + and - operations in symbolic mode:

chmod +x some_script

If you try to recursively set permissions on all the files in a directory structure using something like chmod -R 0644 some_directory then you’ll regret it because you’ve now rendered any subdirectories nonexecutable, which means you won’t be able to access their content, cd into them, or traverse below them. Use find and xargs with chmod to set the files and directories individually. For file permissions:

find some_directory -type f -print0 | xargs -0 chmod 0644

For directory permissions:

find some_directory -type d -print0 | xargs -0 chmod 0755

Of course, if you only want to set permissions on the files in a single directory (non-recursive), just cd in there and set them.

When creating a directory, use mkdir -m mode new_directory since you not only accomplish two tasks with one command, but you avoid any possible race condition between creating the directory and setting the permissions.

Discussion

Many people are in the habit of using three-digit octal modes, but we like to use all four possible digits to be explicit about what we mean to do with all attributes. We also prefer using octal mode when possible because it’s very clear what permissions you are going to end up with. You may also use the absolute operation (=) in symbolic mode if you like, but we’re traditionalists who like the old octal method best.

Ensuring the final permissions when using the symbolic mode and the + or - operations is trickier since they are relative, not absolute. Unfortunately, there are many cases where you can’t simply arbitrarily replace the existing permissions using octal mode. In such cases you have no choice but to use symbolic mode, often using + to add a permission while not disturbing other existing permissions. Consult your specific system’s chmod for details, and verify that your results are as you expect. Here are a few examples:

$ ls -l
-rw-r--r--1 jp users 0 Dec 1 02:09 script.sh
$

# Make file readable, writable, and executable for the owner using octal notation
$ chmod 0700 script.sh

$ ls -l
-rwx------1 jp users 0 Dec 1 02:09 script.sh

# Make file readable and executable for everyone using symbolic notation
$ chmod ugo+rx *.sh

$ ls -l
-rwxr-xr-x 1 jp users 0 Dec 1 02:09 script.sh

Note in the last example that although we added (+) rx to everyone (ugo), the owner still has write (w) permission. That’s what we wanted to do here, and that is often the case. But do you see how, in a security setting, it might be easy to make a mistake and allow an undesirable permission to slip through the cracks? That’s why we like to use the absolute octal mode if possible, and of course we always check the results of our command.

In any case, before you adjust the permissions on a large group of files, thoroughly test your command. You may also want to back up the permissions and owners of the files. See Recipe 17.8 for details.

14.14 Leaking Passwords into the Process List

Problem

ps may show passwords entered on the command line in the clear. For example:

$ ./cheesy_app -u user -p password &
[1] 13301

$ ps
  PID TT STAT     TIME COMMAND
 5280 p0 S    0:00.08 -bash
 9784 p0 R+   0:00.00 ps
13301 p0 S   0:00.01 /bin/sh ./cheesy_app -u user -p password

Solution

Try really hard not to use passwords on the command line.

Discussion

Really. Don’t do that.

Many applications that provide a -p or similar switch will also prompt you if a password is required and you do not provide it on the command line. That’s great for interactive use, but not so great in scripts. You may be tempted to write a trivial “wrapper” script or an alias to try and encapsulate the password on the command line. Unfortunately, that won’t work since the command is eventually run and so ends up in the process list anyway. If the command can accept the password on STDIN, you may be able to pass it in that way:

./bad_app < ~/.hidden/bad_apps_password

That creates other problems, but at least avoids displaying the password in the process list.

If that won’t work, you’ll need to either find a new app, patch the one you are using, or just live with it.

14.15 Writing setuid or setgid Scripts

Problem

You have a problem you think you can solve by using the setuid or setgid bit on a shell script.

Solution

Use Unix groups and file permissions and/or sudo to grant the appropriate users the least privileges they need to accomplish their tasks.

Using the setuid or setgid bit on a shell script will create more problems—especially security problems—than it solves. Some systems (such as Linux) don’t even honor the setuid bit on shell scripts, so creating setuid shell scripts creates an unnecessary portability problem in addition to the security risks.

Discussion

setuid root scripts are especially dangerous, so don’t even think about it. Use sudo.

setuid and setgid have a different meaning when applied to directories than they do when applied to executable files. When one of these is set on a directory it causes any newly created files or subdirectories to be owned by the directory’s owner or group, respectively.

Note you can check a file to see if it is setuid by using test -u and check to see if it is setgid by using test -g:

$ mkdir suid_dir sgid_dir

$ touch suid_file sgid_file

$ ls -l
total 4
drwxr-xr-x 2 jp users 512 Dec 9 03:45 sgid_dir
-rw-r--r-- 1 jp users   0 Dec 9 03:45 sgid_file
drwxr-xr-x 2 jp users 512 Dec 9 03:45 suid_dir
-rw-r--r-- 1 jp users   0 Dec 9 03:45 suid_file

$ chmod 4755 suid_dir suid_file

$ chmod 2755 sgid_dir sgid_file

$ ls -l
total 4
drwxr-sr-x 2 jp users 512 Dec 9 03:45 sgid_dir
-rwxr-sr-x 1 jp users   0 Dec 9 03:45 sgid_file
drwsr-xr-x 2 jp users 512 Dec 9 03:45 suid_dir
-rwsr-xr-x 1 jp users   0 Dec 9 03:45 suid_file

$ [ -u suid_dir ] && echo 'Yup, suid' || echo 'Nope, not suid'
Yup, suid

$ [ -u sgid_dir ] && echo 'Yup, suid' || echo 'Nope, not suid'
Nope, not suid

$ [ -g sgid_file ] && echo 'Yup, sgid' || echo 'Nope, not sgid'
Yup, sgid

$ [ -g suid_file ] && echo 'Yup, sgid' || echo 'Nope, not sgid'
Nope, not sgid

14.16 Restricting Guest Users

The material concerning the restricted shell in this recipe also appears in Learning the bash Shell, 3rd Edition, by Cameron Newham (O’Reilly).

Problem

You need to allow some guest users on your system and need to restrict what they can do.

Solution

Avoid using shared accounts if possible, since you lose accountability and create logistical headaches when users leave and you need to change the password and inform the other users. Create separate accounts with the least possible permissions necessary to do whatever is needed. Consider using:

  • A chroot jail, as discussed in Recipe 14.17

  • SSH to allow noninteractive access to commands or resources, as discussed in Recipe 14.21

  • bash’s restricted shell

Discussion

The restricted shell is designed to put the user into an environment where their ability to move around and write files is severely limited. It’s usually used for guest accounts. You can make a user’s login shell restricted by putting rbash in the user’s /etc/passwd entry if this option was included when bash was compiled.

The specific constraints imposed by the restricted shell disallow the user from doing the following:

  • Changing working directories. cd is inoperative. If you try to use it, you will get the error message cd:restricted from bash.

  • Redirecting output to a file. The redirectors >, >|, <>, and >> are not allowed.

  • Assigning a new value to the environment variables $ENV, $BASH_ENV, $SHELL, or $PATH.

  • Specifying any commands with slashes (/) in them. The shell will treat files outside of the current directory as “not found.”

  • Using the exec builtin.

  • Specifying a filename containing a / as an argument to the . (source) builtin command.

  • Importing function definitions from the shell environment at startup.

  • Adding or deleting builtin commands with the -f and -d options to the enable builtin command.

  • Specifying the -p option to the command builtin command.

  • Turning off restricted mode with set +r.

These restrictions go into effect after the user’s .bash_profile and environment files are run. In addition, it is wise to change the owner of the user’s .bash_profile and .bashrc files to root, and make these files read-only. The user’s home directory should also be made read-only.

This means that the restricted shell user’s entire environment is set up in /etc/profile and .bash_profile. Since the user can’t access /etc/profile and can’t overwrite .bash_ profile, this lets the system administrator configure the environment as they see fit. It’s also a good idea that the last command in the startup file be a cd to some other directory, usually a subdirectory of the user’s $HOME for an extra layer of protection.

Two common ways of setting up such environments are to set up a directory of safe commands and have that directory be the only one in $PATH, and to set up a command menu from which the user can’t escape without exiting the shell.

Warning

The restricted shell is not proof against a determined attacker. It can also be difficult to lock down as well as you think you have, since many common applications, such as vi and Emacs, allow shell escapes that might bypass the restricted shell entirely.

Used wisely it can be a valuable additional layer of security, but it should not be the only layer.

Note that the original Bourne shell has a restricted version called rsh, which may be confused with the so-called r-tools (rsh, rcp, rlogin, etc.) remote shell program, which is also rsh. The very insecure rsh has been mostly replaced (we most sincerely hope) by ssh (the Secure Shell).

14.17 Using chroot Jails

Problem

You have to use a script or application that you don’t trust.

Solution

Consider placing it in a so-called chroot jail. The chroot command changes the root directory of the current process to the directory you specify, then returns a shell or execs a given command. That has the effect of placing the process, and thus the program, into a jail from which it theoretically can’t escape to the parent directory. So if that application is compromised or otherwise does something malicious, it can only affect the small portion of the filesystem you restricted it to. In conjunction with running as a user with very limited rights, this is a very useful layer of security to add.

Unfortunately, covering all the details of chroot is beyond the scope of this recipe, since it would probably require a whole separate book. We present it here to promote awareness of the functionality.

Discussion

So why doesn’t everything run in chroot jails? Because many applications need to interact with other applications, files, directories, or sockets all over the filesystem. That’s the tricky part about using chroot jails; the application can’t see outside of its walls, so everything it needs must be inside those walls. The more complicated the application, the more difficult it is to run in a jail.

Some applications that must inherently be exposed to the internet, such as DNS (e.g., BIND), web, and mail (e.g., Postfix) servers, may be configured to run in chroot jails with varying degrees of difficulty. See the documentation for the distribution and specific applications you are running for details.

Another interesting use of chroot is during system recovery. Once you have booted from a LiveCD and mounted the root filesystem on your hard drive, you may need to run a tool such as LILO or GRUB which, depending on your configuration, might need to believe it’s really running on the damaged system. If the LiveCD and the installed system are not too different, you can usually chroot into the mount point of the damaged system and fix it. That works because all the tools, libraries, configuration files, and devices already exist in the jail, since they really are a complete (if not quite working) system. You might have to experiment with your $PATH in order to find things you need once you’ve chrooted though (that’s an aspect of the “if the LiveCD and the installed system are not too different” caveat).

On a related note, the NSA’s Security Enhanced Linux (SELinux) implementation of Mandatory Access Control (MAC) may be of interest. MAC provides a very granular way to specify at a system level what is and is not allowed, and how various components of the system may interact. The granular definition is called a security policy and it has a similar effect to a jail, in that a given application or process can do only what the policy allows it to do.

Red Hat Linux has incorporated SELinux into its enterprise product. Novell’s SUSE product has a similar MAC implementation called AppArmor, and there are similar implementations for Solaris, BSD, and macOS.

14.18 Running as a Non-root User

Problem

You’d like to run your scripts as a non-root user, but are afraid you won’t be able to do the things you need to do.

Solution

Run your scripts under non-root user IDs, either as you or as dedicated users, and run interactively as non-root, but configure sudo to handle any tasks that require elevated privileges.

Discussion

sudo may be used in a script as easily as it may be used interactively. See the sudoers NOPASSWD option especially (see Recipe 14.19 and the sudoers manpage).

14.19 Using sudo More Securely

Problem

You want to use sudo but are worried about granting too many people too many privileges.

Solution

Good! You should be worrying about security. While using sudo is much more secure than not using it, the default settings may be greatly improved.

Take the time to learn a bit about sudo itself and the /etc/sudoers file. In particular, learn why in most cases you should not be using the ALL=(ALL) ALL specification! Yes, that will work, but it’s not even remotely secure. The only difference between that and just giving everyone the root password is that they don’t know the root password; they can still do everything root can do. sudo logs the commands it runs, but that’s trivial to avoid by using sudo bash.

Second, give your needs some serious thought. Just as you shouldn’t be using the ALL=(ALL) ALL specification, you probably shouldn’t be managing users one by one either. The sudoers utility allows for very granular management, and we strongly recommend using it. man sudoers provides a wealth of material and examples, especially the section on preventing shell escapes.

Third, the sudoers file has a NOPASSWD tag that, as you might expect, allows user accounts to perform privileged operations without first having to enter their user passwords. This is one way to allow automation requiring root access without leaving plain-text passwords all over the place, but it’s also obviously a double-edged sword.

sudoers allows for four kinds of aliases: user, runas, host, and command. Judicious use of them as roles or groups will significantly reduce the maintenance burden. For instance, you can set up a User_Alias for BUILD_USERS, then define the machines those users need to run on with Host_Alias and the commands they need to run with Cmnd_Alias. If you set a policy to only edit /etc/sudoers on one machine and copy it around to all relevant machines periodically using scp with public-key authentication, you can set up a very secure yet usable system of least privilege.

Tip

When sudo asks for your password, it’s really asking for your password. As in, your user account. Not root. For some reason people often get confused by this at first.

Discussion

Unfortunately, sudo is not installed by default on every system. It is usually installed on Linux, macOS, and OpenBSD; other systems will vary. You should consult your system’s documentation and install it if it’s not already there.

Warning

You should always use visudo to edit your /etc/sudoers file. Like vipw, visudo locks the file so that only one person can edit it at a time, and it performs some syntax sanity checks before replacing the production file so that you don’t accidentally lock yourself out of your system.

14.20 Using Passwords in Scripts

Problem

You need to hardcode a password in a script.

Solution

This is obviously a bad idea and should be avoided whenever possible. Unfortunately, sometimes it isn’t possible to avoid it.

The first way to try to avoid doing this is to see if you can use sudo with the NOPASSWD option to avoid having to hardcode a password anywhere. This obviously has its own risks, but is worth checking out. See Recipe 14.19 for more details.

Another alternative may be to use SSH with public keys and ideally restricted commands (see Recipe 14.21).

If there is no other way around it, about the best you can do is put the user ID and password in a separate file that is readable only by the user who needs it, then source that file when necessary (Recipe 10.3). Leave that file out of revision control, of course.

Discussion

Accessing data on remote machines in a secure manner is relatively easy using SSH (see Recipe 14.21 and Recipe 15.11). It may even be possible to use that SSH method to access other data on the same host, but it’s probably much more efficient to use sudo for that. But what about accessing data in a remote database, perhaps using some SQL command? There is not much you can do in that case.

Yes, you say, but what about crypt or the other password hashes? The problem is that the secure methods for storing passwords all involve using what’s known as a one-way hash. The password checks in, but it can’t check out. In other words, given the hash, there is theoretically no way to get the plain-text password back out. And that plain-text password is the point—we need it to access our database or whatever. So secure storage is out.

That leaves insecure storage, but the problem here is that it may actually be worse than plain text because it might give you a false sense of security. If it really makes you feel better, and you promise not to get a false sense of security, go ahead and use ROT13 or something to obfuscate the password:

ROT13=$(echo password | tr 'A-Za-z' 'N-ZA-Mn-za-m')

ROT13 only handles ASCII letters, so you could also use ROT47 to handle some punctuation as well:

ROT47=$(echo password | tr '!-~' 'P-~!-O')
Warning

We really can’t stress enough that ROT13 and ROT47 are nothing more than “security by obscurity” and thus are not security at all. They are better than nothing, if and only if you (or your management) do not get a false sense that you are “secure” when you are not. Just be aware of your risks. Having said that, the reality is sometimes the benefit outweighs the risk.

14.21 Using SSH Without a Password

Problem

You need to use SSH or scp in a script and would like to do so without using a password. Or you’re using them in a cron job and can’t have a password.1

Warning

SSH1 (the protocol) and ssh1 (the executable) are deprecated and considered less secure than the newer SSH2 protocol as implemented by OpenSSH and SSH Communications Security. We strongly recommend using SSH2 with OpenSSH and will not cover SSH1 here.

Solution

There are two ways to use SSH without a password: the wrong way and the right way. The wrong way is to use a public key that is not encrypted by a passphrase. The right way is to use a passphrase-protected public key with ssh-agent or keychain.

We assume you are using OpenSSH; if not, consult your documentation (the commands and files will be similar).

First, you need to create a key pair if you don’t already have one. Only one key pair is necessary to authenticate you to as many machines as you configure, but you may decide to use more than one key pair, perhaps for personal and work reasons. The pair consists of a private key that you should protect at all costs, and a public key (*.pub) that you can post on a billboard if you like. The two are related in a complex mathematical way such that they can identify each other, but you can’t derive one from the other.

Use ssh-keygen (this might be ssh-keygen2 if you’re not using OpenSSH) to create a key pair. -t is used to specify the type; consult your system’s man page for possible values. -b is optional and specifies the number of bits in the new key (2,048 is the default for RSA keys at the time of this writing). -C allows you to specify a comment, but it defaults to user@hostname if you omit it. We recommend using -t rsa -b 4096 -C meaningful comment and we recommend strongly against using no passphrase. ssh-keygen also allows you to change your key file’s passphrase or comment:

$ ssh-keygen --help
unknown option -- -
usage: ssh-keygen [options]
Options:
  -A          Generate non-existent host keys for all key types.
  -a number   Number of KDF rounds for new key format or moduli primality tests.
  -B          Show bubblebabble digest of key file.
  -b bits     Number of bits in the key to create.
  -C comment  Provide new comment.
  -c          Change comment in private and public key files.
  -D pkcs11   Download public key from pkcs11 token.
  -e          Export OpenSSH to foreign format key file.
  -F hostname Find hostname in known hosts file.
  -f filename Filename of the key file.
  -G file     Generate candidates for DH-GEX moduli.
  -g          Use generic DNS resource record format.
  -H          Hash names in known_hosts file.
  -h          Generate host certificate instead of a user certificate.
  -I key_id   Key identifier to include in certificate.
  -i          Import foreign format to OpenSSH key file.
  -J number   Screen this number of moduli lines.
  -j number   Start screening moduli at specified line.
  -K checkpt  Write checkpoints to this file.
  -k          Generate a KRL file.
  -L          Print the contents of a certificate.
  -l          Show fingerprint of key file.
  -M memory   Amount of memory (MB) to use for generating DH-GEX moduli.
  -m key_fmt  Conversion format for -e/-i (PEM|PKCS8|RFC4716).
  -N phrase   Provide new passphrase.
  -n name,... User/host principal names to include in certificate
  -O option   Specify a certificate option.
  -o          Enforce new private key format.
  -P phrase   Provide old passphrase.
  -p          Change passphrase of private key file.
  -Q          Test whether key(s) are revoked in KRL.
  -q          Quiet.
  -R hostname Remove host from known_hosts file.
  -r hostname Print DNS resource record.
  -S start    Start point (hex) for generating DH-GEX moduli.
  -s ca_key   Certify keys with CA key.
  -T file     Screen candidates for DH-GEX moduli.
  -t type     Specify type of key to create.
  -u          Update KRL rather than creating a new one.
  -V from:to  Specify certificate validity interval.
  -v          Verbose.
  -W gen      Generator to use for generating DH-GEX moduli.
  -y          Read private key file and print public key.
  -Z cipher   Specify a cipher for new private key format.
  -z serial   Specify a serial number.

$ ssh-keygen -v -t rsa -b 4096 -C 'This is my new key'
Generating public/private rsa key pair.
Enter file in which to save the key (/home/jp/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/jp/.ssh/id_rsa.
Your public key has been saved in /home/jp/.ssh/id_rsa.pub.
The key fingerprint is:
eb:b3:0b:3a:d8:9f:d0:02:5d:99:ce:69:98:ef:f0:0c This is my new key
The key's randomart image is:
+--[ RSA 4096]----+
|                 |
|       o         |
|      +          |
|   . * .         |
|  . + = S        |
|   . +   .       |
|   oE + .        |
|  . oX +.        |
|    .o* ++       |
+-----------------+


$ $ ls -l ~/.ssh/id_rsa*
-rw------- 1 jp jp 3.3K Aug 27 15:10 /home/jp/.ssh/id_rsa
-rw-r--r-- 1 jp jp  744 Aug 27 15:10 /home/jp/.ssh/id_rsa.pub

$ fold -w75 ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCrxvIjPrLxx9VgkE0uBfdiGGZ5KC38OyTB477
MFyw4W7JMDnN5p7Yx8dvl91Fuc13U+RsuBBqWjNvB6hHesdWr/6D2EgoTGJDbegNNla+qb8jJtX
ZK1s+B9sk9SoIlT4AF5wEAMag0K4Jmv0v/xFHwVRm1BfuEQQIVP7Z8v56e7HWz/pZMb0tM89WMg
ITyJh6cuTG1XHRmYxpOoaPBEKeDXTM0mfyAQwO2yQt6fl29RW1DH5J+jVYarsWScGe6SKSYGQPZ
L7a3KRkbpGPRdVK2CY2P1tXQlnh9hPYqvHtAzXUMYJpSwBkNzRN3A571FBtNUxLGtP+xHNEN7Kz
WpUsT1wv6DQw//UDSHJZShVUHMKp414y6dwmKgXTtqVWXYbB/t2EU+CuWk8OkLA2Tv7dKUnn8tA
87D1LU3hAhr58jDEzXbIfl9yYhV2xHBxVUDf80Lv9p9ZKngRx8hkj8MoDr0J6Eql3JhWKRqRdJy
GwKAyjCk5UQ9EH/sQ3NjhJE1Qb31o0dgE3ZKXfm8VXBZS0XTH4OHjd9RA4VCQWjEpdR2QUgeSXW
aM94v3p6O6njKT6fFXV36S33/F/ROc1vZlcJDTpRCbpCXRNkgPtDAImBNmmweaYB0Ym3wqHRB2I
bnw5vftDpptndB774sV2FcRxptkM8Pd/vRS35q56FSgcT6Q== This is my new key

Once you have a key pair, add your public key to the ~/.ssh/authorized_keys file in your home directory on any other machines to which you wish to connect using this key pair. You can use scp, cp with a floppy or USB key, or simple cut-and-paste from terminal sessions to do that. The important part is that it all ends up on a single line. While you can do it all in one command (e.g., scp id_dsa.pub remote_host:.ssh/ authorized_keys), we don’t recommend that even when you’re “absolutely sure” that authorized_keys doesn’t exist. Instead, you can use a slightly more complicated but much safer command:

$ ssh remote_host "echo $(cat ~/.ssh/id_rsa.pub) >> ~/.ssh/authorized_keys"
jp@remote_host's password:

$ ssh remote_host
Last login: Thu Dec 14 00:02:52 2006 from openbsd.jpsdomain.org
NetBSD 2.0.2 (GENERIC) #0: Wed Mar 23 08:53:42 UTC 2005

Welcome to NetBSD!

$ exit
logout
Connection to remote_host closed.

As you can see, we were prompted for a password for the initial scp, but after that ssh just worked. What isn’t shown here is the use of ssh-agent, which cached the passphrase to the key so that we didn’t have to type it.

The command shown here also assumes that ~/.ssh exists on both machines. If not, create it using mkdir -m 0700 -p ~/.ssh. Your ~/.ssh directory must be mode 0700 or OpenSSH will complain. It’s not a bad idea to use chmod 0600 ~/.ssh/authorized_keys as well.

It’s also worth noting that we’ve just set up a one-way relationship. We can SSH from our local host to our remote host with no password, but the same is not true in reverse, due to both lack of the private key and lack of the agent on the remote host. You can simply copy your private key all over the place to enable a “web of passwordless SSH,” but that complicates matters when you want to change your passphrase and it makes it harder to secure your private key. If possible, you are better off having one well-protected and trusted machine from which you ssh out to remote hosts as needed.

The SSH agent is clever and subtle in its use. We might argue it’s too clever. The way it is intended to be used in practice is via an eval and command substitution: eval 'ssh-agent'. That creates two environment variables so that ssh or scp can find the agent and ask it about your identities. That’s very slick, and it’s well documented in many places. The only problem is that this is unlike any other program in common use (except some of the features of less; see Recipe 8.15) and is totally obscure to a new or uninformed user.

If you just run the agent, it prints out some details and looks like it worked. And it did, in that it’s now running. But it won’t actually do anything, because the necessary environment variables were never actually set. We should also mention in passing that the handy -k switch tells the agent to exit.

Here are some examples of incorrect and correct ways to use the SSH agent:

# The Wrong Way to Use the Agent
# Nothing in the environment
$ set | grep SSH
$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-bACKp27592/agent.27592; export SSH_AUTH_SOCK;
SSH_AGENT_PID=24809; export SSH_AGENT_PID;
echo Agent pid 24809;

# Still nothing
$ set | grep SSH
# Can't even kill it, because -k needs $SSH_AGENT_PID
$ ssh-agent -k
SSH_AGENT_PID not set, cannot kill agent

# Is it even running? Yes
$ ps x
  PID TT  STAT    TIME COMMAND
24809 ??  Is      0:00.01 ssh-agent
22903 p0  I       0:03.05 -bash (bash)
11303 p0  R+      0:00.00 ps -x

$ kill 24809

$ ps x
  PID TT    STAT    TIME COMMAND
22903 p0    I       0:03.06 -bash (bash)
30542 p0    R+      0:00.00 ps -x

# This is correct
$ eval `ssh-agent`
Agent pid 21642

# Hey, it worked!
$ set | grep SSH
SSH_AGENT_PID=21642
SSH_AUTH_SOCK=/tmp/ssh-ZfEsa28724/agent.28724

# Kill it - the wrong way
$ ssh-agent -k
unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 21642 killed;

# Oops, the process is dead but it didn't clean up after itself
$ set | grep SSH
SSH_AGENT_PID=21642
SSH_AUTH_SOCK=/tmp/ssh-ZfEsa28724/agent.28724

# The Right Way to Use the Agent
$ eval `ssh-agent`
Agent pid 19330

$ set | grep SSH
SSH_AGENT_PID=19330
SSH_AUTH_SOCK=/tmp/ssh-fwxMfj4987/agent.4987

$ eval `ssh-agent -k`
Agent pid 19330 killed

$ set | grep SSH
$

Intuitive, isn’t it? Not. Very slick, very efficient, very subtle, yes. User-friendly, not so much.

Once we have the agent running as expected we have to load our identities using the ssh-add command. That’s very easy: we just run it, optionally with a list of key files to load. It will prompt for all the passphrases needed. In this example we don’t list any keys, so it just uses the default as set in the main SSH configuration file:

$ ssh-add
Enter passphrase for /home/jp/.ssh/id_rsa:
Identity added: /home/jp/.ssh/id_rsa (/home/jp/.ssh/id_rsa)
$

So now we can use SSH interactively, in this particular shell session, to log in to any machine we’ve previously configured, without a password or passphrase. So what about other sessions, scripts, or cron?

Use Daniel Robbins’s keychain script, which:

acts as a frontend to ssh-agent and ssh-add, but allows you to easily have one long-running ssh-agent process per system, rather than the norm of one ssh-agent per login session.

This dramatically reduces the number of times you need to enter your passphrase. With keychain, you only need to enter a passphrase once every time your local machine is rebooted.

Keychain also makes it easy for remote cron jobs to securely “hook in” to a long-running ssh-agent process, allowing your scripts to take advantage of key-based logins.

keychain is a clever, well-written, and well-commented shell script that automates and manages the otherwise tedious process of exporting those environment variables we discussed earlier into other sessions. It also makes them available to scripts and cron. But you’re probably saying to yourself, wait a second here, you want me to leave all my keys in this thing forever, until the machine reboots? Well, yes, but it’s not as bad as it sounds.

First of all, you can always kill it, though that will also prevent scripts or cron from using it. Second, there is a --clear option that flushes cached keys when you log in. Sound backward? It actually makes sense. Here are the details, from keychain’s author (first published by IBM developerWorks; see http://www.ibm.com/developerworks/linux/library/l-keyc2/):

I explained that using unencrypted private keys is a dangerous practice, because it allows someone to steal your private key and use it to log in to your remote accounts from any other system without supplying a password. Well, while keychain isn’t vulnerable to this kind of abuse (as long as you use encrypted private keys, that is), there is a potentially exploitable weakness directly related to the fact that keychain makes it so easy to “hook in” to a long-running ssh-agent process. What would happen, I thought, if some intruder were somehow able to figure out my password or pass-phrase and log into my local system? If they were somehow able to log in under my username, keychain would grant them instant access to my decrypted private keys, making it a no-brainer for them to access my other accounts.

Now, before I continue, let’s put this security threat in perspective. If some malicious user were somehow able to log in as me, keychain would indeed allow them to access my remote accounts. Yet, even so, it would be very difficult for the intruder to steal my decrypted private keys since they are still encrypted on disk. Also, gaining access to my private keys would require a user to actually log in as me, not just read files in my directory. So, abusing ssh-agent would be a much more difficult task than simply stealing an unencrypted private key, which only requires that an intruder somehow gain access to my files in ~/.ssh, whether logged in as me or not. Nevertheless, if an intruder were successfully able to log in as me, they could do quite a bit of additional damage by using my decrypted private keys. So, if you happen to be using keychain on a server that you don’t log into very often or don’t actively monitor for security breaches, then consider using the --clear option to provide an additional layer of security.

The --clear option allows you to tell keychain to assume that every new login to your account should be considered a potential security breach until proven otherwise. When you start keychain with the --clear option, keychain immediately flushes all your private keys from ssh-agent’s cache when you log in, before performing its normal duties. Thus, if you’re an intruder, keychain will prompt you for passphrases rather than giving you access to your existing set of cached keys. However, even though this enhances security, it does make things a bit more inconvenient and very similar to running ssh-agent all by itself, without keychain. Here, as is often the case, one can opt for greater security or greater convenience, but not both.

Despite this, using keychain with --clear still has advantages over using ssh-agent all by itself; remember, when you use keychain --clear, your cron jobs and scripts will still be able to establish passwordless connections; this is because your private keys are flushed at login, not logout. Since a logout from the system does not constitute a potential security breach, there’s no reason for keychain to respond by flushing ssh-agent’s keys. Thus, the --clear option [is] an ideal choice for infrequently accessed servers that need to perform occasional secure copying tasks, such as backup servers, firewalls, and routers.

To actually use the keychain-wrapped ssh-agent from a script or cron, simply source the file keychain creates from your script. keychain can also handle GPG keys:

[ -r ~/.ssh-agent ] && source ~/.ssh-agent \
|| { echo "keychain not runnin" >&2 ; exit 1; }

Discussion

When using SSH in a script, you don’t want to be prompted to authenticate or have extraneous warnings displayed. The -q option will turn on quiet mode and suppress warnings, while -o 'BatchMode yes' will prevent user prompts. Obviously if there is no way for SSH to authenticate itself, it will fail, since it can’t even fall back to prompting for a password. But that shouldn’t be a problem since you’ve made it this far in this recipe.

SSH is an amazing, wonderful tool and there is a lot to it—enough to fill another book about this size. We highly recommend SSH, The Secure Shell: The Definitive Guide, 2nd Edition, by Daniel J. Barrett, Richard Silverman, and Robert G. Byrnes (O’Reilly) for everything you ever wanted to know (and more) about SSH.

Using public keys between OpenSSH and SSH2 Server from SSH Communications Security can be tricky; see Chapter 6 in Linux Security Cookbook by the same authors (O’Reilly) for tips.

The IBM developerWorks articles on SSH by keychain author (and Gentoo Chief Architect) Daniel Robbins are also a great reference (http://www.ibm.com/developerworks/linux/library/l-keyc.html, http://www.ibm.com/developerworks/linux/library/l-keyc2/, http://www.ibm.com/developerworks/linux/library/l-keyc3/).

If keychain doesn’t seem to be working, or if it works for a while then seems to stop, you may have another script somewhere else rerunning ssh-agent and getting things out of sync. Check the following and make sure the PIDs and socket all agree:

$ ps -ef | grep [s]sh-agent
jp17364  0.0  0.0  3312 1132?       S   Dec16   0:00 ssh-agent

$ cat ~/.keychain/$HOSTNAME-sh
SSH_AUTH_SOCK=/tmp/ssh-UJc17363/agent.17363; export SSH_AUTH_SOCK;
SSH_AGENT_PID=17364; export SSH_AGENT_PID;

$ set | grep SSH_A
SSH_AGENT_PID=17364
SSH_AUTH_SOCK=/tmp/ssh-UJc17363/agent.17363
Note

Depending on your operating system, you may have to adjust your ps command; if -ef doesn’t work, try -eu.

See Also

14.22 Restricting SSH Commands

Problem

You’d like to restrict what an incoming SSH user or script can do.2

Solution

Edit the ~/.ssh/authorized_keys file, use SSH forced commands, and optionally disable unnecessary SSH features. For example, suppose you want to allow an rsync process without also allowing interactive use.

First, you need to figure out exactly what command is being run on the remote side. Create a key (Recipe 14.21) and add a forced command to tell you. Edit the ~/.ssh/authorized_keys file and add:

command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND"

before the key. It will look something like this, all on one line:

command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND" ssh-dss
AAAAB3NzaC1kc3MAAAEBANpgvvTslst2m0ZJA0ayhh1Mqa3aWwU3kfv0m9+myFZ9veFsxM7
IVxIjWfAlQh3jplY+Q78fMzCTiG+ZrGZYn8adZ9yg5wAC03KXm2vKt8LfTx6I+qkMR7v15N
I7tZyhxGah5qHNehReFWLuk7JXCtRrzRvWMdsHcL2SA1Y4fJ9Y9FfVlBdE1Er+ZIuc5xIlO
6D1HFjKjt3wjbAal+oJxwZJaupZ0Q7N47uwMslmc5ELQBRNDsaoqFRKlerZASPQ5P+AH/+C
xa/fCGYwsogXSJJ0H5S7+QJJHFze35YZI+A1D3BIa4JBf1KvtoaFr5bMdhVAkChdAdMjo96
xhbdEAAAAVAJSKzCEsrUo3KAvyUO8KVD6e0B/NAAAA/3uAx2TIB/M9MmPqjeH67Mh5Y5NaV
WuMqwebDIXuvKQQDMUU4EPjRGmS89Hl8UKAN0Cq/C1T+OGzn4zrbE06COSm3SRMP24HyIbE
lhlWV49sfLR05Qmh9fRl1s7ZdcUrxkDkr2J6on5cMVB9M2nIl90IhRVLd5RxP01u81yqvhv
E61ORdA6IMjzXcQ8ebuD2R733O37oGFD7e2O7DaabKKkHZIduL/zFbQkzMDK6uAMP8ylRJN
0fUsqIhHhtc/16OT2H6nMU09MccxZTFUfqF8xIOndElP6um4jXYk5Q30i/CtU3TZyvNeWVw
yGwDi4wg2jeVe0YHU2RhZcZpwAAAQEAv2O86701U9sIuRijp8sO4h13eZrsE5rdn6aul/mk
m+xAlO+WQeDXRONm9BwVSrNEmIJB74tEJL3qQTMEFoCoN9Kp00Ya7Qt8n4gZ0vcZlI5u+cg
yd1mKaggS2SnoorsRlb2LhHpe6mXus8pUTf5QT8apgXM3TgFsLDT+3rCt40IdGCZLaP+UDB
uNUSKfFwCru6uGoXEwxaL08Nv1wZOc19qrc0Yzp7i33m6i3a0Z9Pu+TPHqYC74QmBbWq8U9
DAo+7yhRIhqfdJzk3vIKSLbCxg4PbMwx2Qfh4dLk+L7wOasKnl5//W+RWBUrOlaZ1ZP1/az
sK0Ncygno/0F1ew== This is my new key

Now execute your command and see what the result is:

$ ssh remote_host 'ls -l /etc'
Command was: ls -l /etc
$

Now, the problem with this approach is that it will break a program like rsync that depends on having the STDOUT/STDIN channel all to itself:

$ rsync -avzL -e ssh remote_host:/etc .
protocol version mismatch -- is your shell clean?
(see the rsync manpage for an explanation)
rsync error: protocol incompatibility (code 2) at compat.c(64)
$

But we can work around that by modifying our forced command as follows:

command="/bin/echo Command was: $SSH_ORIGINAL_COMMAND >> ~/ssh_command"

On the client side we try again:

$ rsync -avzL -e ssh 192.168.99.56:/etc .
rsync: connection unexpectedly closed (0 bytes received so far) [receiver]
rsync error: error in rsync protocol data stream (code 12) at io.c(420)
$

And on the remote host side we now have:

$ cat ../ssh_command
Command was: rsync --server --sender -vlLogDtprz . /etc
$

So we can update our forced command as necessary.

Two other things we can do are set a from host restriction and disable SSH commands. The host restriction specifies the hostname or IP address of the source host. Disabling commands is also pretty intuitive:

no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty

When we put it all together, it looks like this (still all on one giant line):

no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,from="local_
client",command="rsync --server --sender -vlLogDtprz . /etc" ssh-dss
AAAAB3NzaC1kc3MAAAEBANpgvvTslst2m0ZJA0ayhh1Mqa3aWwU3kfv0m9+myFZ9veFsxM7
IVxIjWfAlQh3jplY+Q78fMzCTiG+ZrGZYn8adZ9yg5wAC03KXm2vKt8LfTx6I+qkMR7v15N
I7tZyhxGah5qHNehReFWLuk7JXCtRrzRvWMdsHcL2SA1Y4fJ9Y9FfVlBdE1Er+ZIuc5xIlO
6D1HFjKjt3wjbAal+oJxwZJaupZ0Q7N47uwMslmc5ELQBRNDsaoqFRKlerZASPQ5P+AH/+C
xa/fCGYwsogXSJJ0H5S7+QJJHFze35YZI+A1D3BIa4JBf1KvtoaFr5bMdhVAkChdAdMjo96
xhbdEAAAAVAJSKzCEsrUo3KAvyUO8KVD6e0B/NAAAA/3uAx2TIB/M9MmPqjeH67Mh5Y5NaV
WuMqwebDIXuvKQQDMUU4EPjRGmS89Hl8UKAN0Cq/C1T+OGzn4zrbE06COSm3SRMP24HyIbE
lhlWV49sfLR05Qmh9fRl1s7ZdcUrxkDkr2J6on5cMVB9M2nIl90IhRVLd5RxP01u81yqvhv
E61ORdA6IMjzXcQ8ebuD2R733O37oGFD7e2O7DaabKKkHZIduL/zFbQkzMDK6uAMP8ylRJN
0fUsqIhHhtc/16OT2H6nMU09MccxZTFUfqF8xIOndElP6um4jXYk5Q30i/CtU3TZyvNeWVw
yGwDi4wg2jeVe0YHU2RhZcZpwAAAQEAv2O86701U9sIuRijp8sO4h13eZrsE5rdn6aul/mk
m+xAlO+WQeDXRONm9BwVSrNEmIJB74tEJL3qQTMEFoCoN9Kp00Ya7Qt8n4gZ0vcZlI5u+cg
yd1mKaggS2SnoorsRlb2LhHpe6mXus8pUTf5QT8apgXM3TgFsLDT+3rCt40IdGCZLaP+UDB
uNUSKfFwCru6uGoXEwxaL08Nv1wZOc19qrc0Yzp7i33m6i3a0Z9Pu+TPHqYC74QmBbWq8U9
DAo+7yhRIhqfdJzk3vIKSLbCxg4PbMwx2Qfh4dLk+L7wOasKnl5//W+RWBUrOlaZ1ZP1/az
sK0Ncygno/0F1ew== This is my new key

Discussion

If you have any problems with ssh, the -v option is very helpful. ssh -v or ssh -v -v will almost always give you at least a clue about what’s going wrong. Give them a try when things are working to get an idea of what their output looks like.

If you’d like to be a little more open about what the key can and can’t do, look into the OpenSSH restricted shell, rssh, which supports scp, sftp, rdist, rsync, and cvs.

You’d think restrictions like these would be easy, but it turns out they are not. The problem has to do with the way SSH (and the r-commands before it) actually works. It’s a brilliant idea and it works very well, except that it’s hard to limit. To vastly oversimplify it, you can think of SSH as connecting your local STDOUT to STDIN on the remote side and the remote STDOUT to your local STDIN, so all things like scp or rsync do is stream bytes from the local machine to the remote machine as if over a pipe. But that very flexibility precludes SSH from being able to restrict interactive access while allowing scp. There’s no difference. And that’s why you can’t put lots of echo and debugging statements in your bash configuration files (see Recipe 16.21); that output will intermingle with the byte stream and cause havoc.

So how does rssh work? It provides a wrapper that you use instead of a default login shell (like bash) in /etc/passwd. That wrapper determines what it will and will not allow, but with much more flexibility than a plain old SSH-restricted command.

See Also

14.23 Disconnecting Inactive Sessions

Problem

You’d like to be able to automatically log out inactive users, especially root.

Solution

Set the $TMOUT environment variable in /etc/bashrc or ~/.bashrc to the number of seconds of inactivity before ending the session. In interactive mode, once a prompt is issued, if the user does not enter a command in $TMOUT seconds, bash will exit.

Discussion

$TMOUT is also used in the read builtin and the select command in scripts.

Don’t forget to set this as a read-only variable in a system-level file such as /etc/profile or /etc/bashrc to which users have no write access if you don’t want them to be able to change it:

declare -r TMOUT=3600

# Or:
readonly TMOUT=3600
Warning

Since users have control over their own environments, you cannot totally rely on $TMOUT, even if you set it as read-only: the user could just run a different shell, or even a difference instance of bash itself! Think of it as a helpful reminder to cooperative users, especially knowledgeable and interrupt-driven system administrators who may get distracted (constantly).

1 We thank Daniel Barrett, Richard Silverman, and Robert Byrnes for their inspiration and excellent work in SSH, The Secure Shell: The Definitive Guide—especially Chapters 2, 6, and 11—and Linux Security Cookbook, without which this recipe would be a mere shadow of itself.

2 We thank Daniel Barrett, Richard Silverman, and Robert G. Byrnes for their inspiration and excellent work in SSH, The Secure Shell: The Definitive Guide (especially Chapters 2, 6, and 11) and Linux Security Cookbook, without which this recipe would be a mere shadow of itself.