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