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