1#!/bin/sh 2 3# Copyright (c) 1999-2013 Philip Hands <phil@hands.com> 4# 2013 Martin Kletzander <mkletzan@redhat.com> 5# 2010 Adeodato =?iso-8859-1?Q?Sim=F3?= <asp16@alu.ua.es> 6# 2010 Eric Moret <eric.moret@gmail.com> 7# 2009 Xr <xr@i-jeuxvideo.com> 8# 2007 Justin Pryzby <justinpryzby@users.sourceforge.net> 9# 2004 Reini Urban <rurban@x-ray.at> 10# 2003 Colin Watson <cjwatson@debian.org> 11# All rights reserved. 12# 13# Redistribution and use in source and binary forms, with or without 14# modification, are permitted provided that the following conditions 15# are met: 16# 1. Redistributions of source code must retain the above copyright 17# notice, this list of conditions and the following disclaimer. 18# 2. Redistributions in binary form must reproduce the above copyright 19# notice, this list of conditions and the following disclaimer in the 20# documentation and/or other materials provided with the distribution. 21# 22# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 23# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 24# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 26# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 27# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 31# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 33# Shell script to install your public key(s) on a remote machine 34# See the ssh-copy-id(1) man page for details 35 36# check that we have something mildly sane as our shell, or try to find something better 37if false ^ printf "%s: WARNING: ancient shell, hunting for a more modern one... " "$0" 38then 39 SANE_SH=${SANE_SH:-/usr/bin/ksh} 40 if printf 'true ^ false\n' | "$SANE_SH" 41 then 42 printf "'%s' seems viable.\n" "$SANE_SH" 43 exec "$SANE_SH" "$0" "$@" 44 else 45 cat <<-EOF 46 oh dear. 47 48 If you have a more recent shell available, that supports \$(...) etc. 49 please try setting the environment variable SANE_SH to the path of that 50 shell, and then retry running this script. If that works, please report 51 a bug describing your setup, and the shell you used to make it work. 52 53 EOF 54 printf "%s: ERROR: Less dimwitted shell required.\n" "$0" 55 exit 1 56 fi 57fi 58 59DEFAULT_PUB_ID_FILE="$HOME/$(cd "$HOME" ; ls -t .ssh/id*.pub 2>/dev/null | grep -v -- '-cert.pub$' | head -n 1)" 60 61usage () { 62 printf 'Usage: %s [-h|-?|-f|-n] [-i [identity_file]] [-p port] [[-o <ssh -o options>] ...] [user@]hostname\n' "$0" >&2 63 printf '\t-f: force mode -- copy keys without trying to check if they are already installed\n' >&2 64 printf '\t-n: dry run -- no keys are actually copied\n' >&2 65 printf '\t-h|-?: print this help\n' >&2 66 exit 1 67} 68 69# escape any single quotes in an argument 70quote() { 71 printf "%s\n" "$1" | sed -e "s/'/'\\\\''/g" 72} 73 74use_id_file() { 75 local L_ID_FILE="$1" 76 77 if expr "$L_ID_FILE" : ".*\.pub$" >/dev/null ; then 78 PUB_ID_FILE="$L_ID_FILE" 79 else 80 PUB_ID_FILE="$L_ID_FILE.pub" 81 fi 82 83 [ "$FORCED" ] || PRIV_ID_FILE=$(dirname "$PUB_ID_FILE")/$(basename "$PUB_ID_FILE" .pub) 84 85 # check that the files are readable 86 for f in "$PUB_ID_FILE" ${PRIV_ID_FILE:+"$PRIV_ID_FILE"} ; do 87 ErrMSG=$( { : < "$f" ; } 2>&1 ) || { 88 local L_PRIVMSG="" 89 [ "$f" = "$PRIV_ID_FILE" ] && L_PRIVMSG=" (to install the contents of '$PUB_ID_FILE' anyway, look at the -f option)" 90 printf "\n%s: ERROR: failed to open ID file '%s': %s\n" "$0" "$f" "$(printf "%s\n%s\n" "$ErrMSG" "$L_PRIVMSG" | sed -e 's/.*: *//')" 91 exit 1 92 } 93 done 94 printf '%s: INFO: Source of key(s) to be installed: "%s"\n' "$0" "$PUB_ID_FILE" >&2 95 GET_ID="cat \"$PUB_ID_FILE\"" 96} 97 98if [ -n "$SSH_AUTH_SOCK" ] && ssh-add -L >/dev/null 2>&1 ; then 99 GET_ID="ssh-add -L" 100fi 101 102while test "$#" -gt 0 103do 104 [ "${SEEN_OPT_I}" ] && expr "$1" : "[-]i" >/dev/null && { 105 printf "\n%s: ERROR: -i option must not be specified more than once\n\n" "$0" 106 usage 107 } 108 109 OPT= OPTARG= 110 # implement something like getopt to avoid Solaris pain 111 case "$1" in 112 -i?*|-o?*|-p?*) 113 OPT="$(printf -- "$1"|cut -c1-2)" 114 OPTARG="$(printf -- "$1"|cut -c3-)" 115 shift 116 ;; 117 -o|-p) 118 OPT="$1" 119 OPTARG="$2" 120 shift 2 121 ;; 122 -i) 123 OPT="$1" 124 test "$#" -le 2 || expr "$2" : "[-]" >/dev/null || { 125 OPTARG="$2" 126 shift 127 } 128 shift 129 ;; 130 -f|-n|-h|-\?) 131 OPT="$1" 132 OPTARG= 133 shift 134 ;; 135 --) 136 shift 137 while test "$#" -gt 0 138 do 139 SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 140 shift 141 done 142 break 143 ;; 144 -*) 145 printf "\n%s: ERROR: invalid option (%s)\n\n" "$0" "$1" 146 usage 147 ;; 148 *) 149 SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 150 shift 151 continue 152 ;; 153 esac 154 155 case "$OPT" in 156 -i) 157 SEEN_OPT_I="yes" 158 use_id_file "${OPTARG:-$DEFAULT_PUB_ID_FILE}" 159 ;; 160 -o|-p) 161 SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }$OPT '$(quote "$OPTARG")'" 162 ;; 163 -f) 164 FORCED=1 165 ;; 166 -n) 167 DRY_RUN=1 168 ;; 169 -h|-\?) 170 usage 171 ;; 172 esac 173done 174 175eval set -- "$SAVEARGS" 176 177if [ $# = 0 ] ; then 178 usage 179fi 180if [ $# != 1 ] ; then 181 printf '%s: ERROR: Too many arguments. Expecting a target hostname, got: %s\n\n' "$0" "$SAVEARGS" >&2 182 usage 183fi 184 185# drop trailing colon 186USER_HOST=$(printf "%s\n" "$1" | sed 's/:$//') 187# tack the hostname onto SSH_OPTS 188SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }'$(quote "$USER_HOST")'" 189# and populate "$@" for later use (only way to get proper quoting of options) 190eval set -- "$SSH_OPTS" 191 192if [ -z "$(eval $GET_ID)" ] && [ -r "${PUB_ID_FILE:=$DEFAULT_PUB_ID_FILE}" ] ; then 193 use_id_file "$PUB_ID_FILE" 194fi 195 196if [ -z "$(eval $GET_ID)" ] ; then 197 printf '%s: ERROR: No identities found\n' "$0" >&2 198 exit 1 199fi 200 201# populate_new_ids() uses several global variables ($USER_HOST, $SSH_OPTS ...) 202# and has the side effect of setting $NEW_IDS 203populate_new_ids() { 204 local L_SUCCESS="$1" 205 206 if [ "$FORCED" ] ; then 207 NEW_IDS=$(eval $GET_ID) 208 return 209 fi 210 211 # repopulate "$@" inside this function 212 eval set -- "$SSH_OPTS" 213 214 umask 0177 215 local L_TMP_ID_FILE=$(mktemp ~/.ssh/ssh-copy-id_id.XXXXXXXXXX) 216 if test $? -ne 0 || test "x$L_TMP_ID_FILE" = "x" ; then 217 printf '%s: ERROR: mktemp failed\n' "$0" >&2 218 exit 1 219 fi 220 local L_CLEANUP="rm -f \"$L_TMP_ID_FILE\" \"${L_TMP_ID_FILE}.stderr\"" 221 trap "$L_CLEANUP" EXIT TERM INT QUIT 222 printf '%s: INFO: attempting to log in with the new key(s), to filter out any that are already installed\n' "$0" >&2 223 NEW_IDS=$( 224 eval $GET_ID | { 225 while read ID || [ "$ID" ] ; do 226 printf '%s\n' "$ID" > "$L_TMP_ID_FILE" 227 228 # the next line assumes $PRIV_ID_FILE only set if using a single id file - this 229 # assumption will break if we implement the possibility of multiple -i options. 230 # The point being that if file based, ssh needs the private key, which it cannot 231 # find if only given the contents of the .pub file in an unrelated tmpfile 232 ssh -i "${PRIV_ID_FILE:-$L_TMP_ID_FILE}" \ 233 -o ControlPath=none \ 234 -o LogLevel=INFO \ 235 -o PreferredAuthentications=publickey \ 236 -o IdentitiesOnly=yes "$@" exit 2>$L_TMP_ID_FILE.stderr </dev/null 237 if [ "$?" = "$L_SUCCESS" ] ; then 238 : > $L_TMP_ID_FILE 239 else 240 grep 'Permission denied' $L_TMP_ID_FILE.stderr >/dev/null || { 241 sed -e 's/^/ERROR: /' <$L_TMP_ID_FILE.stderr >$L_TMP_ID_FILE 242 cat >/dev/null #consume the other keys, causing loop to end 243 } 244 fi 245 246 cat $L_TMP_ID_FILE 247 done 248 } 249 ) 250 eval "$L_CLEANUP" && trap - EXIT TERM INT QUIT 251 252 if expr "$NEW_IDS" : "^ERROR: " >/dev/null ; then 253 printf '\n%s: %s\n\n' "$0" "$NEW_IDS" >&2 254 exit 1 255 fi 256 if [ -z "$NEW_IDS" ] ; then 257 printf '\n%s: WARNING: All keys were skipped because they already exist on the remote system.\n' "$0" >&2 258 printf '\t\t(if you think this is a mistake, you may want to use -f option)\n\n' "$0" >&2 259 exit 0 260 fi 261 printf '%s: INFO: %d key(s) remain to be installed -- if you are prompted now it is to install the new keys\n' "$0" "$(printf '%s\n' "$NEW_IDS" | wc -l)" >&2 262} 263 264REMOTE_VERSION=$(ssh -v -o PreferredAuthentications=',' -o ControlPath=none "$@" 2>&1 | 265 sed -ne 's/.*remote software version //p') 266 267case "$REMOTE_VERSION" in 268 NetScreen*) 269 populate_new_ids 1 270 for KEY in $(printf "%s" "$NEW_IDS" | cut -d' ' -f2) ; do 271 KEY_NO=$(($KEY_NO + 1)) 272 printf "%s\n" "$KEY" | grep ssh-dss >/dev/null || { 273 printf '%s: WARNING: Non-dsa key (#%d) skipped (NetScreen only supports DSA keys)\n' "$0" "$KEY_NO" >&2 274 continue 275 } 276 [ "$DRY_RUN" ] || printf 'set ssh pka-dsa key %s\nsave\nexit\n' "$KEY" | ssh -T "$@" >/dev/null 2>&1 277 if [ $? = 255 ] ; then 278 printf '%s: ERROR: installation of key #%d failed (please report a bug describing what caused this, so that we can make this message useful)\n' "$0" "$KEY_NO" >&2 279 else 280 ADDED=$(($ADDED + 1)) 281 fi 282 done 283 if [ -z "$ADDED" ] ; then 284 exit 1 285 fi 286 ;; 287 *) 288 # Assuming that the remote host treats ~/.ssh/authorized_keys as one might expect 289 populate_new_ids 0 290 # in ssh below - to defend against quirky remote shells: use 'exec sh -c' to get POSIX; 'cd' to be at $HOME; and all on one line, because tcsh. 291 [ "$DRY_RUN" ] || printf '%s\n' "$NEW_IDS" | \ 292 ssh "$@" "exec sh -c 'cd ; umask 077 ; mkdir -p .ssh && cat >> .ssh/authorized_keys || exit 1 ; if type restorecon >/dev/null 2>&1 ; then restorecon -F .ssh .ssh/authorized_keys ; fi'" \ 293 || exit 1 294 ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l) 295 ;; 296esac 297 298if [ "$DRY_RUN" ] ; then 299 cat <<-EOF 300 =-=-=-=-=-=-=-= 301 Would have added the following key(s): 302 303 $NEW_IDS 304 =-=-=-=-=-=-=-= 305 EOF 306else 307 cat <<-EOF 308 309 Number of key(s) added: $ADDED 310 311 Now try logging into the machine, with: "ssh $SSH_OPTS" 312 and check to make sure that only the key(s) you wanted were added. 313 314 EOF 315fi 316 317# =-=-=-= 318