123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- #!/usr/bin/env sh
- # Copyright (c) 1999-2020 Philip Hands <phil@hands.com>
- # 2020 Matthias Blümel <blaimi@blaimi.de>
- # 2017 Sebastien Boyron <seb@boyron.eu>
- # 2013 Martin Kletzander <mkletzan@redhat.com>
- # 2010 Adeodato =?iso-8859-1?Q?Sim=F3?= <asp16@alu.ua.es>
- # 2010 Eric Moret <eric.moret@gmail.com>
- # 2009 Xr <xr@i-jeuxvideo.com>
- # 2007 Justin Pryzby <justinpryzby@users.sourceforge.net>
- # 2004 Reini Urban <rurban@x-ray.at>
- # 2003 Colin Watson <cjwatson@debian.org>
- # All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions
- # are met:
- # 1. Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- # 2. Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in the
- # documentation and/or other materials provided with the distribution.
- #
- # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- # Shell script to install your public key(s) on a remote machine
- # See the ssh-copy-id(1) man page for details
- # shellcheck shell=dash
- # check that we have something mildly sane as our shell, or try to find something better
- if false ^ printf "%s: WARNING: ancient shell, hunting for a more modern one... " "$0"; then
- SANE_SH=${SANE_SH:-/usr/bin/ksh}
- if printf 'true ^ false\n' | "$SANE_SH"; then
- printf "'%s' seems viable.\\n" "$SANE_SH"
- exec "$SANE_SH" "$0" "$@"
- else
- cat <<- EOF
- oh dear.
- If you have a more recent shell available, that supports \$(...) etc.
- please try setting the environment variable SANE_SH to the path of that
- shell, and then retry running this script. If that works, please report
- a bug describing your setup, and the shell you used to make it work.
- EOF
- printf '%s: ERROR: Less dimwitted shell required.\n' "$0"
- exit 1
- fi
- fi
- # shellcheck disable=SC2010
- DEFAULT_PUB_ID_FILE=$(ls -t "${HOME}"/.ssh/id*.pub 2> /dev/null | grep -v -- '-cert.pub$' | head -n 1)
- SSH="ssh -a -x"
- umask 0177
- usage()
- {
- 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
- printf '\t-f: force mode -- copy keys without trying to check if they are already installed\n' >&2
- printf '\t-n: dry run -- no keys are actually copied\n' >&2
- printf '\t-s: use sftp -- use sftp instead of executing remote-commands. Can be useful if the remote only allows sftp\n' >&2
- printf '\t-h|-?: print this help\n' >&2
- exit 1
- }
- # escape any single quotes in an argument
- quote()
- {
- printf '%s\n' "$1" | sed -e "s/'/'\\\\''/g"
- }
- use_id_file()
- {
- L_ID_FILE="$1"
- if [ -z "$L_ID_FILE" ]; then
- printf '%s: ERROR: no ID file found\n' "$0"
- exit 1
- fi
- if expr "$L_ID_FILE" : '.*\.pub$' > /dev/null; then
- PUB_ID_FILE="$L_ID_FILE"
- else
- PUB_ID_FILE="$L_ID_FILE.pub"
- fi
- [ "$FORCED" ] || PRIV_ID_FILE=$(dirname "$PUB_ID_FILE")/$(basename "$PUB_ID_FILE" .pub)
- # check that the files are readable
- for f in "$PUB_ID_FILE" ${PRIV_ID_FILE:+"$PRIV_ID_FILE"}; do
- ErrMSG=$( {
- : < "$f"
- } 2>&1) || {
- L_PRIVMSG=""
- [ "$f" = "$PRIV_ID_FILE" ] && L_PRIVMSG=" (to install the contents of '$PUB_ID_FILE' anyway, look at the -f option)"
- 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/.*: *//')"
- exit 1
- }
- done
- printf '%s: INFO: Source of key(s) to be installed: "%s"\n' "$0" "$PUB_ID_FILE" >&2
- GET_ID="cat \"$PUB_ID_FILE\""
- }
- if [ -n "$SSH_AUTH_SOCK" ] && ssh-add -L > /dev/null 2>&1; then
- GET_ID="ssh-add -L"
- fi
- while getopts "i:o:p:F:fnsh?" OPT; do
- case "$OPT" in
- i)
- [ "${SEEN_OPT_I}" ] && {
- printf '\n%s: ERROR: -i option must not be specified more than once\n\n' "$0"
- usage
- }
- SEEN_OPT_I="yes"
- use_id_file "${OPTARG:-$DEFAULT_PUB_ID_FILE}"
- ;;
- o | p | F)
- SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }-$OPT '$( quote "${OPTARG}")'"
- ;;
- f)
- FORCED=1
- ;;
- n)
- DRY_RUN=1
- ;;
- s)
- SFTP=sftp
- ;;
- h | \?)
- usage
- ;;
- esac
- done
- #shift all args to keep only USER_HOST
- shift $((OPTIND - 1))
- if [ $# = 0 ]; then
- usage
- fi
- if [ $# != 1 ]; then
- printf '%s: ERROR: Too many arguments. Expecting a target hostname, got: %s\n\n' "$0" "$SAVEARGS" >&2
- usage
- fi
- # drop trailing colon
- USER_HOST="$*"
- # tack the hostname onto SSH_OPTS
- SSH_OPTS="${SSH_OPTS:+$SSH_OPTS }'$(quote "$USER_HOST")'"
- # and populate "$@" for later use (only way to get proper quoting of options)
- eval set -- "$SSH_OPTS"
- # shellcheck disable=SC2086
- if [ -z "$(eval $GET_ID)" ] && [ -r "${PUB_ID_FILE:=$DEFAULT_PUB_ID_FILE}" ]; then
- use_id_file "$PUB_ID_FILE"
- fi
- # shellcheck disable=SC2086
- if [ -z "$(eval $GET_ID)" ]; then
- printf '%s: ERROR: No identities found\n' "$0" >&2
- exit 1
- fi
- # filter_ids()
- # tries to log in using the keys piped to it, and filters out any that work
- filter_ids()
- {
- L_SUCCESS="$1"
- L_TMP_ID_FILE="$SCRATCH_DIR"/popids_tmp_id
- L_OUTPUT_FILE="$SCRATCH_DIR"/popids_output
- # repopulate "$@" inside this function
- eval set -- "$SSH_OPTS"
- while read -r ID || [ "$ID" ]; do
- printf '%s\n' "$ID" > "$L_TMP_ID_FILE"
- # the next line assumes $PRIV_ID_FILE only set if using a single id file - this
- # assumption will break if we implement the possibility of multiple -i options.
- # The point being that if file based, ssh needs the private key, which it cannot
- # find if only given the contents of the .pub file in an unrelated tmpfile
- $SSH -i "${PRIV_ID_FILE:-$L_TMP_ID_FILE}" \
- -o ControlPath=none \
- -o LogLevel=INFO \
- -o PreferredAuthentications=publickey \
- -o IdentitiesOnly=yes "$@" exit > "$L_OUTPUT_FILE" 2>&1 < /dev/null
- if [ "$?" = "$L_SUCCESS" ] || {
- [ "$SFTP" ] && grep 'allows sftp connections only' "$L_OUTPUT_FILE" > /dev/null
- # this error counts as a success if we're setting up an sftp connection
- }; then
- : > "$L_TMP_ID_FILE"
- else
- grep 'Permission denied' "$L_OUTPUT_FILE" > /dev/null || {
- sed -e 's/^/ERROR: /' < "$L_OUTPUT_FILE" > "$L_TMP_ID_FILE"
- cat > /dev/null #consume the other keys, causing loop to end
- }
- fi
- cat "$L_TMP_ID_FILE"
- done
- }
- # populate_new_ids() uses several global variables ($USER_HOST, $SSH_OPTS ...)
- # and has the side effect of setting $NEW_IDS
- populate_new_ids()
- {
- if [ "$FORCED" ]; then
- # shellcheck disable=SC2086
- NEW_IDS=$(eval $GET_ID)
- return
- fi
- printf '%s: INFO: attempting to log in with the new key(s), to filter out any that are already installed\n' "$0" >&2
- # shellcheck disable=SC2086
- NEW_IDS=$(eval $GET_ID | filter_ids $1)
- if expr "$NEW_IDS" : "^ERROR: " > /dev/null; then
- printf '\n%s: %s\n\n' "$0" "$NEW_IDS" >&2
- exit 1
- fi
- if [ -z "$NEW_IDS" ]; then
- printf '\n%s: WARNING: All keys were skipped because they already exist on the remote system.\n' "$0" >&2
- printf '\t\t(if you think this is a mistake, you may want to use -f option)\n\n' >&2
- exit 0
- fi
- 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
- }
- # installkey_sh [target_path]
- # produce a one-liner to add the keys to remote authorized_keys file
- # optionally takes an alternative path for authorized_keys
- installkeys_sh()
- {
- AUTH_KEY_FILE=${1:-.ssh/authorized_keys}
- AUTH_KEY_DIR=$(dirname "${AUTH_KEY_FILE}")
- # In setting INSTALLKEYS_SH:
- # the tr puts it all on one line (to placate tcsh)
- # (hence the excessive use of semi-colons (;) )
- # then in the command:
- # cd to be at $HOME, just in case;
- # the -z `tail ...` checks for a trailing newline. The echo adds one if was missing
- # the cat adds the keys we're getting via STDIN
- # and if available restorecon is used to restore the SELinux context
- INSTALLKEYS_SH=$(
- tr '\t\n' ' ' <<- EOF
- cd;
- umask 077;
- mkdir -p "${AUTH_KEY_DIR}" &&
- { [ -z \`tail -1c ${AUTH_KEY_FILE} 2>/dev/null\` ] ||
- echo >> "${AUTH_KEY_FILE}" || exit 1; } &&
- cat >> "${AUTH_KEY_FILE}" || exit 1;
- if type restorecon >/dev/null 2>&1; then
- restorecon -F "${AUTH_KEY_DIR}" "${AUTH_KEY_FILE}";
- fi
- EOF
- )
- # to defend against quirky remote shells: use 'exec sh -c' to get POSIX;
- printf "exec sh -c '%s'" "${INSTALLKEYS_SH}"
- }
- #shellcheck disable=SC2120 # the 'eval set' confuses this
- installkeys_via_sftp()
- {
- # repopulate "$@" inside this function
- eval set -- "$SSH_OPTS"
- L_KEYS=$SCRATCH_DIR/authorized_keys
- L_SHARED_CON=$SCRATCH_DIR/master-conn
- $SSH -f -N -M -S "$L_SHARED_CON" "$@"
- L_CLEANUP="$SSH -S $L_SHARED_CON -O exit 'ignored' >/dev/null 2>&1 ; $SCRATCH_CLEANUP"
- #shellcheck disable=SC2064
- trap "$L_CLEANUP" EXIT TERM INT QUIT
- sftp -b - -o "ControlPath=$L_SHARED_CON" "ignored" <<- EOF || return 1
- -get .ssh/authorized_keys $L_KEYS
- EOF
- # add a newline or create file if it's missing, same like above
- [ -z "$(tail -1c "$L_KEYS" 2> /dev/null)" ] || echo >> "$L_KEYS"
- # append the keys being piped in here
- cat >> "$L_KEYS"
- sftp -b - -o "ControlPath=$L_SHARED_CON" "ignored" <<- EOF || return 1
- -mkdir .ssh
- chmod 700 .ssh
- put $L_KEYS .ssh/authorized_keys
- chmod 600 .ssh/authorized_keys
- EOF
- #shellcheck disable=SC2064
- eval "$L_CLEANUP" && trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT
- }
- # create a scratch dir for any temporary files needed
- if SCRATCH_DIR=$(mktemp -d ~/.ssh/ssh-copy-id.XXXXXXXXXX) &&
- [ "$SCRATCH_DIR" ] && [ -d "$SCRATCH_DIR" ]; then
- chmod 0700 "$SCRATCH_DIR"
- SCRATCH_CLEANUP="rm -rf \"$SCRATCH_DIR\""
- #shellcheck disable=SC2064
- trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT
- else
- printf '%s: ERROR: failed to create required temporary directory under ~/.ssh\n' "$0" >&2
- exit 1
- fi
- REMOTE_VERSION=$($SSH -v -o PreferredAuthentications=',' -o ControlPath=none "$@" 2>&1 |
- sed -ne 's/.*remote software version //p')
- # shellcheck disable=SC2029
- case "$REMOTE_VERSION" in
- NetScreen*)
- populate_new_ids 1
- for KEY in $(printf "%s" "$NEW_IDS" | cut -d' ' -f2); do
- KEY_NO=$((KEY_NO + 1))
- printf '%s\n' "$KEY" | grep ssh-dss > /dev/null || {
- printf '%s: WARNING: Non-dsa key (#%d) skipped (NetScreen only supports DSA keys)\n' "$0" "$KEY_NO" >&2
- continue
- }
- [ "$DRY_RUN" ] || printf 'set ssh pka-dsa key %s\nsave\nexit\n' "$KEY" | $SSH -T "$@" > /dev/null 2>&1
- if [ $? = 255 ]; then
- 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
- else
- ADDED=$((ADDED + 1))
- fi
- done
- if [ -z "$ADDED" ]; then
- exit 1
- fi
- ;;
- dropbear*)
- populate_new_ids 0
- [ "$DRY_RUN" ] || printf '%s\n' "$NEW_IDS" |
- $SSH "$@" "$(installkeys_sh /etc/dropbear/authorized_keys)" ||
- exit 1
- ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l)
- ;;
- *)
- # Assuming that the remote host treats ~/.ssh/authorized_keys as one might expect
- populate_new_ids 0
- if ! [ "$DRY_RUN" ]; then
- printf '%s\n' "$NEW_IDS" |
- if [ "$SFTP" ]; then
- #shellcheck disable=SC2119
- installkeys_via_sftp
- else
- $SSH "$@" "$(installkeys_sh)"
- fi || exit 1
- fi
- ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l)
- ;;
- esac
- if [ "$DRY_RUN" ]; then
- cat <<- EOF
- =-=-=-=-=-=-=-=
- Would have added the following key(s):
- $NEW_IDS
- =-=-=-=-=-=-=-=
- EOF
- else
- cat <<- EOF
- Number of key(s) added: $ADDED
- Now try logging into the machine, with: "${SFTP:-ssh} $SSH_OPTS"
- and check to make sure that only the key(s) you wanted were added.
- EOF
- fi
- # =-=-=-=
|