ssh-copy-id revision 250737
11841Swollman#!/bin/sh 21841Swollman 31841Swollman# Copyright (c) 1999-2013 Philip Hands <phil@hands.com> 41841Swollman# 2013 Martin Kletzander <mkletzan@redhat.com> 51841Swollman# 2010 Adeodato =?iso-8859-1?Q?Sim=F3?= <asp16@alu.ua.es> 61841Swollman# 2010 Eric Moret <eric.moret@gmail.com> 71841Swollman# 2009 Xr <xr@i-jeuxvideo.com> 81841Swollman# 2007 Justin Pryzby <justinpryzby@users.sourceforge.net> 91841Swollman# 2004 Reini Urban <rurban@x-ray.at> 101841Swollman# 2003 Colin Watson <cjwatson@debian.org> 111841Swollman# All rights reserved. 121841Swollman# 131841Swollman# Redistribution and use in source and binary forms, with or without 141841Swollman# modification, are permitted provided that the following conditions 151841Swollman# are met: 16148834Sstefanf# 1. Redistributions of source code must retain the above copyright 171841Swollman# notice, this list of conditions and the following disclaimer. 181841Swollman# 2. Redistributions in binary form must reproduce the above copyright 191841Swollman# notice, this list of conditions and the following disclaimer in the 201841Swollman# documentation and/or other materials provided with the distribution. 211841Swollman# 221841Swollman# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 231841Swollman# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 241841Swollman# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 251841Swollman# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 261841Swollman# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 271841Swollman# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 281841Swollman# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 291841Swollman# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 301841Swollman# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 311841Swollman# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 321841Swollman 33170547Sstefanf# Shell script to install your public key(s) on a remote machine 3484326Sobrien# See the ssh-copy-id(1) man page for details 351841Swollman 361841Swollman# check that we have something mildly sane as our shell, or try to find something better 371841Swollmanif false ^ printf "%s: WARNING: ancient shell, hunting for a more modern one... " "$0" 381841Swollmanthen 391841Swollman SANE_SH=${SANE_SH:-/usr/bin/ksh} 4084326Sobrien if printf 'true ^ false\n' | "$SANE_SH" 4184326Sobrien then 421841Swollman printf "'%s' seems viable.\n" "$SANE_SH" 431841Swollman exec "$SANE_SH" "$0" "$@" 441841Swollman else 451841Swollman cat <<-EOF 46117556Simp oh dear. 47117556Simp 481841Swollman If you have a more recent shell available, that supports \$(...) etc. 491841Swollman please try setting the environment variable SANE_SH to the path of that 501841Swollman shell, and then retry running this script. If that works, please report 51148834Sstefanf a bug describing your setup, and the shell you used to make it work. 521841Swollman 531841Swollman EOF 541841Swollman printf "%s: ERROR: Less dimwitted shell required.\n" "$0" 551841Swollman exit 1 561841Swollman fi 571841Swollmanfi 5884326Sobrien 5984326SobrienDEFAULT_PUB_ID_FILE=$(ls -t ${HOME}/.ssh/id*.pub 2>/dev/null | grep -v -- '-cert.pub$' | head -n 1) 6084326Sobrien 611841Swollmanusage () { 621841Swollman printf 'Usage: %s [-h|-?|-n] [-i [identity_file]] [-p port] [[-o <ssh -o options>] ...] [user@]hostname\n' "$0" >&2 631841Swollman exit 1 641841Swollman} 651841Swollman 661841Swollman# escape any single quotes in an argument 671841Swollmanquote() { 681841Swollman printf "%s\n" "$1" | sed -e "s/'/'\\\\''/g" 691841Swollman} 7084326Sobrien 7184326Sobrienuse_id_file() { 721841Swollman local L_ID_FILE="$1" 731841Swollman 7484326Sobrien if expr "$L_ID_FILE" : ".*\.pub$" >/dev/null ; then 7584326Sobrien PUB_ID_FILE="$L_ID_FILE" 7684325Sobrien else 771841Swollman PUB_ID_FILE="$L_ID_FILE.pub" 781841Swollman fi 791841Swollman 801841Swollman PRIV_ID_FILE=$(dirname "$PUB_ID_FILE")/$(basename "$PUB_ID_FILE" .pub) 8184326Sobrien 82148834Sstefanf # check that the files are readable 8384326Sobrien for f in $PUB_ID_FILE $PRIV_ID_FILE ; do 841841Swollman ErrMSG=$( { : < $f ; } 2>&1 ) || { 851841Swollman printf "\n%s: ERROR: failed to open ID file '%s': %s\n\n" "$0" "$f" "$(printf "%s\n" "$ErrMSG" | sed -e 's/.*: *//')" 861841Swollman exit 1 871841Swollman } 8884326Sobrien done 8984326Sobrien GET_ID="cat \"$PUB_ID_FILE\"" 90148834Sstefanf} 911841Swollman 921841Swollmanif [ -n "$SSH_AUTH_SOCK" ] && ssh-add -L >/dev/null 2>&1 ; then 9384325Sobrien GET_ID="ssh-add -L" 9484325Sobrienfi 9584325Sobrien 9684325Sobrienwhile test "$#" -gt 0 9784325Sobriendo 981841Swollman [ "${SEEN_OPT_I}" ] && expr "$1" : "[-]i" >/dev/null && { 991841Swollman printf "\n%s: ERROR: -i option must not be specified more than once\n\n" "$0" 1001841Swollman usage 101148834Sstefanf } 1021841Swollman 1031841Swollman OPT= OPTARG= 10484326Sobrien # implement something like getopt to avoid Solaris pain 1051841Swollman case "$1" in 10684326Sobrien -i?*|-o?*|-p?*) 107170511Sstefanf OPT="$(printf -- "$1"|cut -c1-2)" 108148834Sstefanf OPTARG="$(printf -- "$1"|cut -c3-)" 109148834Sstefanf shift 110148834Sstefanf ;; 1111841Swollman -o|-p) 1121841Swollman OPT="$1" 1131841Swollman OPTARG="$2" 1141841Swollman shift 2 11584326Sobrien ;; 11684326Sobrien -i) 11784326Sobrien OPT="$1" 11884326Sobrien test "$#" -le 2 || expr "$2" : "[-]" >/dev/null || { 1191841Swollman OPTARG="$2" 1201841Swollman shift 1211841Swollman } 1221841Swollman shift 1231841Swollman ;; 1241841Swollman -n|-h|-\?) 1251841Swollman OPT="$1" 12684326Sobrien OPTARG= 12784325Sobrien shift 12884325Sobrien ;; 129148834Sstefanf --) 130148834Sstefanf shift 131148834Sstefanf while test "$#" -gt 0 132148834Sstefanf do 133170511Sstefanf SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 134170547Sstefanf shift 135170547Sstefanf done 1361841Swollman break 137170547Sstefanf ;; 138148834Sstefanf -*) 1391841Swollman printf "\n%s: ERROR: invalid option (%s)\n\n" "$0" "$1" 1401841Swollman usage 1411841Swollman ;; 14284326Sobrien *) 1431841Swollman SAVEARGS="${SAVEARGS:+$SAVEARGS }'$(quote "$1")'" 1441841Swollman shift 1451841Swollman continue 1461841Swollman ;; 1471841Swollman esac 1481841Swollman 14984326Sobrien case "$OPT" in 1501841Swollman -i) 1511841Swollman SEEN_OPT_I="yes" 1521841Swollman use_id_file "${OPTARG:-$DEFAULT_PUB_ID_FILE}" 15398293Smdodd ;; 15498293Smdodd -o|-p) 15598293Smdodd SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }$OPT '$(quote "$OPTARG")'" 15698293Smdodd ;; 15798293Smdodd -n) 15898293Smdodd DRY_RUN=1 1591841Swollman ;; 1601841Swollman -h|-\?) 16184326Sobrien usage 16284326Sobrien ;; 16384326Sobrien esac 1641841Swollmandone 165148834Sstefanf 1661841Swollmaneval set -- "$SAVEARGS" 1671841Swollman 1681841Swollmanif [ $# == 0 ] ; then 1691841Swollman usage 1701841Swollmanfi 1711841Swollmanif [ $# != 1 ] ; then 1721841Swollman printf '%s: ERROR: Too many arguments. Expecting a target hostname, got: %s\n\n' "$0" "$SAVEARGS" >&2 17384326Sobrien usage 17484326Sobrienfi 1751841Swollman 1761841Swollman# drop trailing colon 1771841SwollmanUSER_HOST=$(printf "%s\n" "$1" | sed 's/:$//') 1781841Swollman# tack the hostname onto SSH_OPTS 1791841SwollmanSSH_OPTS="${SSH_OPTS:+$SSH_OPTS }'$(quote "$USER_HOST")'" 18084326Sobrien# and populate "$@" for later use (only way to get proper quoting of options) 18184326Sobrieneval set -- "$SSH_OPTS" 1821841Swollman 18384326Sobrienif [ -z "$(eval $GET_ID)" ] && [ -r "${PUB_ID_FILE:=$DEFAULT_PUB_ID_FILE}" ] ; then 1841841Swollman use_id_file "$PUB_ID_FILE" 18584326Sobrienfi 18684325Sobrien 187170547Sstefanfif [ -z "$(eval $GET_ID)" ] ; then 18884325Sobrien printf '%s: ERROR: No identities found\n' "$0" >&2 18984325Sobrien exit 1 19084325Sobrienfi 19184325Sobrien 19284325Sobrien# populate_new_ids() uses several global variables ($USER_HOST, $SSH_OPTS ...) 19384325Sobrien# and has the side effect of setting $NEW_IDS 194148834Sstefanfpopulate_new_ids() { 19584325Sobrien local L_SUCCESS="$1" 19684325Sobrien 19784325Sobrien # repopulate "$@" inside this function 19884325Sobrien eval set -- "$SSH_OPTS" 19984325Sobrien 20084325Sobrien umask 0177 20184325Sobrien local L_TMP_ID_FILE=$(mktemp ~/.ssh/ssh-copy-id_id.XXXXXXXXXX) 20284325Sobrien if test $? -ne 0 || test "x$L_TMP_ID_FILE" = "x" ; then 20384325Sobrien echo "mktemp failed" 1>&2 20484325Sobrien exit 1 20584325Sobrien fi 206148834Sstefanf trap "rm -f $L_TMP_ID_FILE ${L_TMP_ID_FILE}.pub" EXIT TERM INT QUIT 207148834Sstefanf printf '%s: INFO: attempting to log in with the new key(s), to filter out any that are already installed\n' "$0" >&2 208148834Sstefanf NEW_IDS=$( 2091841Swollman eval $GET_ID | { 210148834Sstefanf while read ID ; do 211148834Sstefanf printf '%s\n' "$ID" > $L_TMP_ID_FILE 212148834Sstefanf 213148834Sstefanf # the next line assumes $PRIV_ID_FILE only set if using a single id file - this 214148834Sstefanf # assumption will break if we implement the possibility of multiple -i options. 215148834Sstefanf # The point being that if file based, ssh needs the private key, which it cannot 216148834Sstefanf # find if only given the contents of the .pub file in an unrelated tmpfile 217148834Sstefanf ssh -i "${PRIV_ID_FILE:-$L_TMP_ID_FILE}" \ 218148834Sstefanf -o PreferredAuthentications=publickey \ 219148834Sstefanf -o IdentitiesOnly=yes "$@" exit 2>$L_TMP_ID_FILE.stderr </dev/null 220148834Sstefanf if [ "$?" = "$L_SUCCESS" ] ; then 221148834Sstefanf : > $L_TMP_ID_FILE 222148834Sstefanf else 223148834Sstefanf grep 'Permission denied' $L_TMP_ID_FILE.stderr >/dev/null || { 224148834Sstefanf sed -e 's/^/ERROR: /' <$L_TMP_ID_FILE.stderr >$L_TMP_ID_FILE 225148834Sstefanf cat >/dev/null #consume the other keys, causing loop to end 226148834Sstefanf } 227148834Sstefanf fi 228117556Simp 229117556Simp cat $L_TMP_ID_FILE 23084326Sobrien done 231 } 232 ) 233 rm -f $L_TMP_ID_FILE* && trap - EXIT TERM INT QUIT 234 235 if expr "$NEW_IDS" : "^ERROR: " >/dev/null ; then 236 printf '\n%s: %s\n\n' "$0" "$NEW_IDS" >&2 237 exit 1 238 fi 239 if [ -z "$NEW_IDS" ] ; then 240 printf '\n%s: WARNING: All keys were skipped because they already exist on the remote system.\n\n' "$0" >&2 241 exit 0 242 fi 243 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 244} 245 246REMOTE_VERSION=$(ssh -v -o PreferredAuthentications=',' "$@" 2>&1 | 247 sed -ne 's/.*remote software version //p') 248 249case "$REMOTE_VERSION" in 250 NetScreen*) 251 populate_new_ids 1 252 for KEY in $(printf "%s" "$NEW_IDS" | cut -d' ' -f2) ; do 253 KEY_NO=$(($KEY_NO + 1)) 254 printf "%s\n" "$KEY" | grep ssh-dss >/dev/null || { 255 printf '%s: WARNING: Non-dsa key (#%d) skipped (NetScreen only supports DSA keys)\n' "$0" "$KEY_NO" >&2 256 continue 257 } 258 [ "$DRY_RUN" ] || printf 'set ssh pka-dsa key %s\nsave\nexit\n' "$KEY" | ssh -T "$@" >/dev/null 2>&1 259 if [ $? = 255 ] ; then 260 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 261 else 262 ADDED=$(($ADDED + 1)) 263 fi 264 done 265 if [ -z "$ADDED" ] ; then 266 exit 1 267 fi 268 ;; 269 *) 270 # Assuming that the remote host treats ~/.ssh/authorized_keys as one might expect 271 populate_new_ids 0 272 [ "$DRY_RUN" ] || printf '%s\n' "$NEW_IDS" | ssh "$@" " 273 umask 077 ; 274 mkdir -p .ssh && cat >> .ssh/authorized_keys || exit 1 ; 275 if type restorecon >/dev/null 2>&1 ; then restorecon -F .ssh .ssh/authorized_keys ; fi" \ 276 || exit 1 277 ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l) 278 ;; 279esac 280 281if [ "$DRY_RUN" ] ; then 282 cat <<-EOF 283 =-=-=-=-=-=-=-= 284 Would have added the following key(s): 285 286 $NEW_IDS 287 =-=-=-=-=-=-=-= 288 EOF 289else 290 cat <<-EOF 291 292 Number of key(s) added: $ADDED 293 294 Now try logging into the machine, with: "ssh $SSH_OPTS" 295 and check to make sure that only the key(s) you wanted were added. 296 297 EOF 298fi 299 300# =-=-=-= 301