1#!/bin/sh 2 3# Copyright (c) 1999-2024 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" >&2 60 exit 1 61 fi 62fi 63 64# shellcheck disable=SC2010 65DEFAULT_PUB_ID_FILE=$(ls -dt "${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]] [-t target_path] [-F ssh_config] [[-o ssh_option] ...] [-p port] [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" >&2 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/.*: *//')" >&2 107 exit 1 108 } 109 done 110 GET_ID="cat \"$PUB_ID_FILE\"" 111} 112 113if [ -n "$SSH_AUTH_SOCK" ] && ssh-add -L >/dev/null 2>&1 ; then 114 GET_ID="ssh-add -L" 115fi 116 117OPTS="io:p:F:t:fnsxh?" 118 119while getopts "$OPTS" OPT 120do 121 case "$OPT" in 122 i) 123 [ "${SEEN_OPT_I}" ] && { 124 printf '\n%s: ERROR: -i option must not be specified more than once\n\n' "$0" >&2 125 usage 126 } 127 SEEN_OPT_I="yes" 128 129 # Check for -i's optional parameter 130 eval "nextarg=\${$OPTIND}" 131 # shellcheck disable=SC2154 132 if [ $OPTIND = $# ]; then 133 if [ -r "$nextarg" ] && grep -iq ssh "$nextarg"; then 134 printf '\n%s: ERROR: Missing hostname. Use "-i -- %s" if you really mean to use this as the hostname\n\n' "$0" "$nextarg" >&2 135 usage 136 fi 137 elif ! expr -- "$nextarg" : "-[$(echo "$OPTS" | tr -d :)-]" >/dev/null ; then 138 # when not at the last arg, and not followed by an option, -i has an argument 139 OPTARG="$nextarg" 140 OPTIND=$((OPTIND + 1)) 141 fi 142 use_id_file "${OPTARG:-$DEFAULT_PUB_ID_FILE}" 143 ;; 144 o|F) 145 OPTS_oF="${OPTS_oF:+$OPTS_oF }-$OPT '$(quote "${OPTARG}")'" 146 ;; 147 f) 148 FORCED=1 149 ;; 150 n) 151 DRY_RUN=1 152 ;; 153 p) 154 SSH_PORT=${OPTARG} 155 ;; 156 s) 157 SFTP=sftp 158 ;; 159 t) 160 TARGET_PATH="${OPTARG}" 161 ;; 162 x) 163 SET_X="set -x;" 164 set -x 165 ;; 166 h|\?) 167 usage 168 ;; 169 esac 170done 171#shift all args to keep only USER_HOST 172shift $((OPTIND-1)) 173 174if [ $# = 0 ] ; then 175 usage 176fi 177if [ $# != 1 ] ; then 178 printf '%s: ERROR: Too many arguments. Expecting a target hostname, got: %s\n\n' "$0" "$SAVEARGS" >&2 179 usage 180fi 181 182USER_HOST="$*" 183# tack the hostname onto SSH_OPTS 184OPTS_USER_HOST="${OPTS_oF:+$OPTS_oF }'$(quote "$USER_HOST")'" 185SSH_OPTS="${SSH_PORT:+-p $SSH_PORT }$OPTS_USER_HOST" 186# and populate "$@" for later use (only way to get proper quoting of options) 187eval set -- "$SSH_OPTS" 188 189# shellcheck disable=SC2086 190if [ -z "$(eval $GET_ID)" ] && [ -r "${PUB_ID_FILE:=$DEFAULT_PUB_ID_FILE}" ] ; then 191 use_id_file "$PUB_ID_FILE" 192fi 193 194printf '%s: INFO: Source of key(s) to be installed: %s\n' "$0" "${GET_ID#cat }" >&2 195 196# shellcheck disable=SC2086 197if [ -z "$(eval $GET_ID)" ] ; then 198 printf '%s: ERROR: No identities found\n' "$0" >&2 199 exit 1 200fi 201 202# assert_scratch_ok() 203# ensures that $SCRATCH_DIR is setup. 204assert_scratch_ok() { 205 [ "$SCRATCH_DIR" ] && [ -d "$SCRATCH_DIR" ] && [ -w "$SCRATCH_DIR" ] && return 0 206 207 printf 'ERROR: Assertion failure: in %s(): scratch_dir was not correctly set up (SCRATCH_DIR = "%s")\n' "$1" "$SCRATCH_DIR" >&2 208 return 1 209} 210 211# filter_ids() 212# tries to log in using the keys piped to it, and filters out any that work 213filter_ids() { 214 L_SUCCESS="$1" 215 assert_scratch_ok filter_ids || return 216 L_TMP_ID_FILE="$SCRATCH_DIR"/popids_tmp_id 217 L_OUTPUT_FILE="$SCRATCH_DIR"/popids_output 218 219 # repopulate "$@" inside this function 220 eval set -- "$SSH_OPTS" 221 222 while read -r ID || [ "$ID" ] ; do 223 printf '%s\n' "$ID" > "$L_TMP_ID_FILE" 224 225 # the next line assumes $PRIV_ID_FILE only set if using a single id file - this 226 # assumption will break if we implement the possibility of multiple -i options. 227 # The point being that if file based, ssh needs the private key, which it cannot 228 # find if only given the contents of the .pub file in an unrelated tmpfile 229 $SSH -i "${PRIV_ID_FILE:-$L_TMP_ID_FILE}" \ 230 -o ControlPath=none \ 231 -o LogLevel=INFO \ 232 -o PreferredAuthentications=publickey \ 233 -o IdentitiesOnly=yes "$@" exit >"$L_OUTPUT_FILE" 2>&1 </dev/null 234 if [ "$?" = "$L_SUCCESS" ] || { 235 [ "$SFTP" ] && grep 'allows sftp connections only' "$L_OUTPUT_FILE" >/dev/null 236 # this error counts as a success if we're setting up an sftp connection 237 } 238 then 239 : > "$L_TMP_ID_FILE" 240 else 241 grep 'Permission denied' "$L_OUTPUT_FILE" >/dev/null || { 242 sed -e 's/^/ERROR: /' <"$L_OUTPUT_FILE" >"$L_TMP_ID_FILE" 243 cat >/dev/null #consume the other keys, causing loop to end 244 } 245 fi 246 247 cat "$L_TMP_ID_FILE" 248 done 249} 250 251# populate_new_ids() uses several global variables ($USER_HOST, $SSH_OPTS ...) 252# and has the side effect of setting $NEW_IDS 253populate_new_ids() { 254 if [ "$FORCED" ] ; then 255 # shellcheck disable=SC2086 256 NEW_IDS=$(eval $GET_ID) 257 return 258 fi 259 260 printf '%s: INFO: attempting to log in with the new key(s), to filter out any that are already installed\n' "$0" >&2 261 # shellcheck disable=SC2086 262 NEW_IDS=$(eval $GET_ID | filter_ids $1) 263 264 if expr "$NEW_IDS" : "^ERROR: " >/dev/null ; then 265 printf '\n%s: %s\n\n' "$0" "$NEW_IDS" >&2 266 exit 1 267 fi 268 if [ -z "$NEW_IDS" ] ; then 269 printf '\n%s: WARNING: All keys were skipped because they already exist on the remote system.\n' "$0" >&2 270 printf '\t\t(if you think this is a mistake, you may want to use -f option)\n\n' >&2 271 exit 0 272 fi 273 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 274} 275 276# installkey_sh [target_path] 277# produce a one-liner to add the keys to remote $TARGET_PATH 278installkeys_sh() { 279 # In setting INSTALLKEYS_SH: 280 # the tr puts it all on one line (to placate tcsh) 281 # (hence the excessive use of semi-colons (;) ) 282 # then in the command: 283 # cd to be at $HOME, just in case; 284 # the -z `tail ...` checks for a trailing newline. The echo adds one if was missing 285 # the cat adds the keys we're getting via STDIN 286 # and if available restorecon is used to restore the SELinux context 287 # OpenWrt has a special case for root only. 288 INSTALLKEYS_SH=$(tr '\t\n' ' ' <<-EOF 289 $SET_X 290 cd; 291 umask 077; 292 AUTH_KEY_FILE="${TARGET_PATH}"; 293 [ -f /etc/openwrt_release ] && [ "\$LOGNAME" = "root" ] && 294 AUTH_KEY_FILE=/etc/dropbear/authorized_keys; 295 AUTH_KEY_DIR=\`dirname "\${AUTH_KEY_FILE}"\`; 296 mkdir -p "\${AUTH_KEY_DIR}" && 297 { [ -z "\`tail -1c "\${AUTH_KEY_FILE}" 2>/dev/null\`" ] || 298 echo >> "\${AUTH_KEY_FILE}" || exit 1; } && 299 cat >> "\${AUTH_KEY_FILE}" || exit 1; 300 if type restorecon >/dev/null 2>&1; then 301 restorecon -F "\${AUTH_KEY_DIR}" "\${AUTH_KEY_FILE}"; 302 fi 303 EOF 304 ) 305 306 # to defend against quirky remote shells: use 'exec sh -c' to get POSIX; 307 printf "exec sh -c '%s'" "${INSTALLKEYS_SH}" 308} 309 310#shellcheck disable=SC2120 # the 'eval set' confuses this 311installkeys_via_sftp() { 312 AUTH_KEY_FILE=${TARGET_PATH} 313 AUTH_KEY_DIR=$(dirname "${AUTH_KEY_FILE}") 314 315 # repopulate "$@" inside this function 316 eval set -- "$SSH_OPTS" 317 318 assert_scratch_ok installkeys_via_sftp || return 1 319 L_KEYS="$SCRATCH_DIR"/authorized_keys 320 L_SHARED_CON="$SCRATCH_DIR"/master-conn 321 $SSH -f -N -M -S "$L_SHARED_CON" "$@" 322 L_CLEANUP="$SSH -S '$L_SHARED_CON' -O exit 'ignored' >/dev/null 2>&1 ; $SCRATCH_CLEANUP" 323 #shellcheck disable=SC2064 324 trap "$L_CLEANUP" EXIT TERM INT QUIT 325 sftp -b - -o "ControlPath='$L_SHARED_CON'" "ignored" <<-EOF || return 1 326 -get "$AUTH_KEY_FILE" "$L_KEYS" 327 EOF 328 # add a newline or create file if it's missing, same like above 329 [ -z "$(tail -1c "$L_KEYS" 2>/dev/null)" ] || echo >> "$L_KEYS" 330 # append the keys being piped in here 331 cat >> "$L_KEYS" 332 sftp -b - -o "ControlPath='$L_SHARED_CON'" "ignored" <<-EOF || return 1 333 -mkdir "$AUTH_KEY_DIR" 334 chmod 700 "$AUTH_KEY_DIR" 335 put "$L_KEYS" "$AUTH_KEY_FILE" 336 chmod 600 "$AUTH_KEY_FILE" 337 EOF 338 #shellcheck disable=SC2064 339 eval "$L_CLEANUP" && trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT 340} 341 342 343# create a scratch dir for any temporary files needed 344if SCRATCH_DIR=$(mktemp -d ~/.ssh/ssh-copy-id.XXXXXXXXXX) && 345 [ "$SCRATCH_DIR" ] && [ -d "$SCRATCH_DIR" ] 346then 347 chmod 0700 "$SCRATCH_DIR" 348 SCRATCH_CLEANUP="rm -rf \"$SCRATCH_DIR\"" 349 #shellcheck disable=SC2064 350 trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT 351else 352 printf '%s: ERROR: failed to create required temporary directory under ~/.ssh (HOME="%s")\n' "$0" "$HOME" >&2 353 exit 1 354fi 355 356REMOTE_VERSION=$($SSH -v -o PreferredAuthentications=',' -o ControlPath=none "$@" 2>&1 | 357 sed -ne 's/.*remote software version //p') 358 359# shellcheck disable=SC2029 360case "$REMOTE_VERSION" in 361 NetScreen*) 362 populate_new_ids 1 363 for KEY in $(printf "%s" "$NEW_IDS" | cut -d' ' -f2) ; do 364 KEY_NO=$((KEY_NO + 1)) 365 printf '%s\n' "$KEY" | grep ssh-dss >/dev/null || { 366 printf '%s: WARNING: Non-dsa key (#%d) skipped (NetScreen only supports DSA keys)\n' "$0" "$KEY_NO" >&2 367 continue 368 } 369 [ "$DRY_RUN" ] || printf 'set ssh pka-dsa key %s\nsave\nexit\n' "$KEY" | $SSH -T "$@" >/dev/null 2>&1 370 if [ $? = 255 ] ; then 371 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 372 else 373 ADDED=$((ADDED + 1)) 374 fi 375 done 376 if [ -z "$ADDED" ] ; then 377 exit 1 378 fi 379 ;; 380 *) 381 # Assuming that the remote host treats $TARGET_PATH as one might expect 382 populate_new_ids 0 383 if ! [ "$DRY_RUN" ] ; then 384 printf '%s\n' "$NEW_IDS" | \ 385 if [ "$SFTP" ] ; then 386 #shellcheck disable=SC2119 387 installkeys_via_sftp 388 else 389 $SSH "$@" "$(installkeys_sh)" 390 fi || exit 1 391 fi 392 ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l) 393 ;; 394esac 395 396if [ "$DRY_RUN" ] ; then 397 cat <<-EOF 398 =-=-=-=-=-=-=-= 399 Would have added the following key(s): 400 401 $NEW_IDS 402 =-=-=-=-=-=-=-= 403 EOF 404else 405 [ -z "$SFTP" ] || PORT_OPT=P 406 cat <<-EOF 407 408 Number of key(s) added: $ADDED 409 410 Now try logging into the machine, with: "${SFTP:-ssh} ${SEEN_OPT_I:+-i${PRIV_ID_FILE:+ $PRIV_ID_FILE} }${SSH_PORT:+-${PORT_OPT:-p} $SSH_PORT }${OPTS_USER_HOST}" 411 and check to make sure that only the key(s) you wanted were added. 412 413 EOF 414fi 415 416# =-=-=-= 417