ssh-copy-id revision 296781
1272343Sngie#!/bin/sh 2272343Sngie 3272343Sngie# Copyright (c) 1999-2013 Philip Hands <phil@hands.com> 4272343Sngie# 2013 Martin Kletzander <mkletzan@redhat.com> 5272343Sngie# 2010 Adeodato =?iso-8859-1?Q?Sim=F3?= <asp16@alu.ua.es> 6272343Sngie# 2010 Eric Moret <eric.moret@gmail.com> 7272343Sngie# 2009 Xr <xr@i-jeuxvideo.com> 8272343Sngie# 2007 Justin Pryzby <justinpryzby@users.sourceforge.net> 9272343Sngie# 2004 Reini Urban <rurban@x-ray.at> 10272343Sngie# 2003 Colin Watson <cjwatson@debian.org> 11272343Sngie# All rights reserved. 12272343Sngie# 13272343Sngie# Redistribution and use in source and binary forms, with or without 14272343Sngie# modification, are permitted provided that the following conditions 15272343Sngie# are met: 16272343Sngie# 1. Redistributions of source code must retain the above copyright 17272343Sngie# notice, this list of conditions and the following disclaimer. 18272343Sngie# 2. Redistributions in binary form must reproduce the above copyright 19272343Sngie# notice, this list of conditions and the following disclaimer in the 20272343Sngie# documentation and/or other materials provided with the distribution. 21272343Sngie# 22272343Sngie# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 23272343Sngie# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 24272343Sngie# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25272343Sngie# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 26272343Sngie# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 27272343Sngie# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28272343Sngie# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29272343Sngie# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30272343Sngie# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 31272343Sngie# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32272343Sngie 33272343Sngie# Shell script to install your public key(s) on a remote machine 34272343Sngie# See the ssh-copy-id(1) man page for details 35272343Sngie 36272343Sngie# check that we have something mildly sane as our shell, or try to find something better 37272343Sngieif false ^ printf "%s: WARNING: ancient shell, hunting for a more modern one... " "$0" 38272343Sngiethen 39272343Sngie SANE_SH=${SANE_SH:-/usr/bin/ksh} 40272343Sngie if printf 'true ^ false\n' | "$SANE_SH" 41272343Sngie then 42272343Sngie printf "'%s' seems viable.\n" "$SANE_SH" 43272343Sngie exec "$SANE_SH" "$0" "$@" 44272343Sngie else 45272343Sngie cat <<-EOF 46272343Sngie oh dear. 47272343Sngie 48272343Sngie If you have a more recent shell available, that supports \$(...) etc. 49272343Sngie please try setting the environment variable SANE_SH to the path of that 50272343Sngie shell, and then retry running this script. If that works, please report 51272343Sngie a bug describing your setup, and the shell you used to make it work. 52272343Sngie 53272343Sngie EOF 54272343Sngie printf "%s: ERROR: Less dimwitted shell required.\n" "$0" 55272343Sngie exit 1 56272343Sngie fi 57272343Sngiefi 58272343Sngie 59272343SngieDEFAULT_PUB_ID_FILE="$HOME/$(cd "$HOME" ; ls -t .ssh/id*.pub 2>/dev/null | grep -v -- '-cert.pub$' | head -n 1)" 60272343Sngie 61272343Sngieusage () { 62272343Sngie printf 'Usage: %s [-h|-?|-f|-n] [-i [identity_file]] [-p port] [[-o <ssh -o options>] ...] [user@]hostname\n' "$0" >&2 63272343Sngie printf '\t-f: force mode -- copy keys without trying to check if they are already installed\n' >&2 64272343Sngie printf '\t-n: dry run -- no keys are actually copied\n' >&2 65272343Sngie printf '\t-h|-?: print this help\n' >&2 66272343Sngie exit 1 67272343Sngie} 68272343Sngie 69272343Sngie# escape any single quotes in an argument 70272343Sngiequote() { 71272343Sngie printf "%s\n" "$1" | sed -e "s/'/'\\\\''/g" 72272343Sngie} 73272343Sngie 74272343Sngieuse_id_file() { 75272343Sngie local L_ID_FILE="$1" 76272343Sngie 77272343Sngie if expr "$L_ID_FILE" : ".*\.pub$" >/dev/null ; then 78272343Sngie PUB_ID_FILE="$L_ID_FILE" 79272343Sngie else 80272343Sngie PUB_ID_FILE="$L_ID_FILE.pub" 81272343Sngie fi 82272343Sngie 83272343Sngie [ "$FORCED" ] || PRIV_ID_FILE=$(dirname "$PUB_ID_FILE")/$(basename "$PUB_ID_FILE" .pub) 84272343Sngie 85272343Sngie # check that the files are readable 86272343Sngie for f in "$PUB_ID_FILE" ${PRIV_ID_FILE:+"$PRIV_ID_FILE"} ; do 87272343Sngie ErrMSG=$( { : < "$f" ; } 2>&1 ) || { 88272343Sngie local L_PRIVMSG="" 89272343Sngie [ "$f" = "$PRIV_ID_FILE" ] && L_PRIVMSG=" (to install the contents of '$PUB_ID_FILE' anyway, look at the -f option)" 90272343Sngie 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/.*: *//')" 91272343Sngie exit 1 92272343Sngie } 93272343Sngie done 94272343Sngie printf '%s: INFO: Source of key(s) to be installed: "%s"\n' "$0" "$PUB_ID_FILE" >&2 95272343Sngie GET_ID="cat \"$PUB_ID_FILE\"" 96272343Sngie} 97272343Sngie 98272343Sngieif [ -n "$SSH_AUTH_SOCK" ] && ssh-add -L >/dev/null 2>&1 ; then 99272343Sngie GET_ID="ssh-add -L" 100272343Sngiefi 101272343Sngie 102272343Sngiewhile test "$#" -gt 0 103272343Sngiedo 104272343Sngie [ "${SEEN_OPT_I}" ] && expr "$1" : "[-]i" >/dev/null && { 105272343Sngie printf "\n%s: ERROR: -i option must not be specified more than once\n\n" "$0" 106272343Sngie usage 107272343Sngie } 108272343Sngie 109272343Sngie OPT= OPTARG= 110272343Sngie # implement something like getopt to avoid Solaris pain 111272343Sngie case "$1" in 112272343Sngie -i?*|-o?*|-p?*) 113272343Sngie OPT="$(printf -- "$1"|cut -c1-2)" 114272343Sngie OPTARG="$(printf -- "$1"|cut -c3-)" 115272343Sngie shift 116272343Sngie ;; 117272343Sngie -o|-p) 118272343Sngie OPT="$1" 119272343Sngie OPTARG="$2" 120272343Sngie shift 2 121272343Sngie ;; 122272343Sngie -i) 123272343Sngie OPT="$1" 124272343Sngie test "$#" -le 2 || expr "$2" : "[-]" >/dev/null || { 125272343Sngie OPTARG="$2" 126272343Sngie shift 127272343Sngie } 128272343Sngie shift 129272343Sngie ;; 130272343Sngie -f|-n|-h|-\?) 131272343Sngie OPT="$1" 132272343Sngie OPTARG= 133272343Sngie shift 134272343Sngie ;; 135272343Sngie --) 136272343Sngie shift 137272343Sngie while test "$#" -gt 0 138272343Sngie do 139272343Sngie SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 140272343Sngie shift 141272343Sngie done 142272343Sngie break 143272343Sngie ;; 144272343Sngie -*) 145272343Sngie printf "\n%s: ERROR: invalid option (%s)\n\n" "$0" "$1" 146272343Sngie usage 147272343Sngie ;; 148272343Sngie *) 149272343Sngie SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 150272343Sngie shift 151272343Sngie continue 152272343Sngie ;; 153272343Sngie esac 154272343Sngie 155272343Sngie case "$OPT" in 156272343Sngie -i) 157272343Sngie SEEN_OPT_I="yes" 158272343Sngie use_id_file "${OPTARG:-$DEFAULT_PUB_ID_FILE}" 159272343Sngie ;; 160272343Sngie -o|-p) 161272343Sngie SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }$OPT '$(quote "$OPTARG")'" 162272343Sngie ;; 163272343Sngie -f) 164272343Sngie FORCED=1 165272343Sngie ;; 166272343Sngie -n) 167272343Sngie DRY_RUN=1 168272343Sngie ;; 169272343Sngie -h|-\?) 170272343Sngie usage 171272343Sngie ;; 172272343Sngie esac 173272343Sngiedone 174272343Sngie 175272343Sngieeval set -- "$SAVEARGS" 176272343Sngie 177272343Sngieif [ $# = 0 ] ; then 178272343Sngie usage 179272343Sngiefi 180272343Sngieif [ $# != 1 ] ; then 181272343Sngie printf '%s: ERROR: Too many arguments. Expecting a target hostname, got: %s\n\n' "$0" "$SAVEARGS" >&2 182272343Sngie usage 183272343Sngiefi 184272343Sngie 185272343Sngie# drop trailing colon 186272343SngieUSER_HOST=$(printf "%s\n" "$1" | sed 's/:$//') 187272343Sngie# tack the hostname onto SSH_OPTS 188272343SngieSSH_OPTS="${SSH_OPTS:+$SSH_OPTS }'$(quote "$USER_HOST")'" 189272343Sngie# and populate "$@" for later use (only way to get proper quoting of options) 190272343Sngieeval set -- "$SSH_OPTS" 191272343Sngie 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