add -s flag: to install keys via SFTP

This is prompted by:

 https://bugzilla.mindrot.org/show_bug.cgi?id=3201

Thanks go to Matthias Blümel for the idea, and the helpful patch, from
which this patch grew.

SSH-Copy-ID-Upstream: f7c76dc64427cd20287a6868f672423b62057614
This commit is contained in:
Philip Hands 2020-09-16 16:13:30 +02:00 committed by Darren Tucker
parent f92424970c
commit a9c9e91a82
2 changed files with 80 additions and 28 deletions

View File

@ -1,6 +1,7 @@
#!/bin/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>
@ -61,11 +62,14 @@ 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] [-i [identity_file]] [-p port] [-F alternative ssh_config file] [[-o <ssh -o options>] ...] [user@]hostname\n' "$0" >&2
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
}
@ -108,9 +112,8 @@ 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:fnh?" OPT
while getopts "i:o:p:F:fnsh?" OPT
do
case "$OPT" in
i)
[ "${SEEN_OPT_I}" ] && {
@ -129,6 +132,9 @@ do
n)
DRY_RUN=1
;;
s)
SFTP=sftp
;;
h|\?)
usage
;;
@ -137,9 +143,6 @@ done
#shift all args to keep only USER_HOST
shift $((OPTIND-1))
if [ $# = 0 ] ; then
usage
fi
@ -170,6 +173,8 @@ fi
# and has the side effect of setting $NEW_IDS
populate_new_ids() {
L_SUCCESS="$1"
L_TMP_ID_FILE="$SCRATCH_DIR"/popids_tmp_id
L_OUTPUT_FILE="$SCRATCH_DIR"/popids_output
# shellcheck disable=SC2086
if [ "$FORCED" ] ; then
@ -180,15 +185,6 @@ populate_new_ids() {
# repopulate "$@" inside this function
eval set -- "$SSH_OPTS"
umask 0177
L_TMP_ID_FILE=$(mktemp ~/.ssh/ssh-copy-id_id.XXXXXXXXXX)
if test $? -ne 0 || test "x$L_TMP_ID_FILE" = "x" ; then
printf '%s: ERROR: mktemp failed\n' "$0" >&2
exit 1
fi
L_CLEANUP="rm -f \"$L_TMP_ID_FILE\" \"${L_TMP_ID_FILE}.stderr\""
# shellcheck disable=SC2064
trap "$L_CLEANUP" EXIT TERM INT QUIT
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=$(
@ -200,16 +196,19 @@ populate_new_ids() {
# 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}" \
$SSH -i "${PRIV_ID_FILE:-$L_TMP_ID_FILE}" \
-o ControlPath=none \
-o LogLevel=INFO \
-o PreferredAuthentications=publickey \
-o IdentitiesOnly=yes "$@" exit 2>"$L_TMP_ID_FILE.stderr" </dev/null
-o IdentitiesOnly=yes "$@" exit >"$L_OUTPUT_FILE" 2>&1 </dev/null
if [ "$?" = "$L_SUCCESS" ] ; then
: > "$L_TMP_ID_FILE"
elif [ "$SFTP" ] && grep 'allows sftp connections only' "$L_OUTPUT_FILE" >/dev/null ; then
# this error counts as a success when we're setting up an sftp connection
: > "$L_TMP_ID_FILE"
else
grep 'Permission denied' "$L_TMP_ID_FILE.stderr" >/dev/null || {
sed -e 's/^/ERROR: /' <"$L_TMP_ID_FILE.stderr" >"$L_TMP_ID_FILE"
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
@ -218,7 +217,6 @@ populate_new_ids() {
done
}
)
eval "$L_CLEANUP" && trap - EXIT TERM INT QUIT
if expr "$NEW_IDS" : "^ERROR: " >/dev/null ; then
printf '\n%s: %s\n\n' "$0" "$NEW_IDS" >&2
@ -264,7 +262,49 @@ EOF
printf "exec sh -c '%s'" "${INSTALLKEYS_SH}"
}
REMOTE_VERSION=$(ssh -v -o PreferredAuthentications=',' -o ControlPath=none "$@" 2>&1 |
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"
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
eval "$L_CLEANUP" && trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT
}
# create a scratch dir for any temporary files needed
SCRATCH_DIR=$(mktemp -d ~/.ssh/ssh-copy-id.XXXXXXXXXX)
if test $? -ne 0 || test "x$SCRATCH_DIR" = "x" ; then
printf '%s: ERROR: mktemp failed\n' "$0" >&2
exit 1
fi
chmod 0700 $SCRATCH_DIR
if [ -d "$SCRATCH_DIR" ] ; then
SCRATCH_CLEANUP="rm -rf \"$SCRATCH_DIR\""
trap "$SCRATCH_CLEANUP" EXIT TERM INT QUIT
else
printf '%s: ERROR: Required scratch directory (%s) was not created\n' "$0" "$SCRATCH_DIR" >&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
@ -277,7 +317,7 @@ case "$REMOTE_VERSION" in
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
[ "$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
@ -291,16 +331,21 @@ case "$REMOTE_VERSION" in
dropbear*)
populate_new_ids 0
[ "$DRY_RUN" ] || printf '%s\n' "$NEW_IDS" | \
ssh "$@" "$(installkeys_sh /etc/dropbear/authorized_keys)" \
$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
[ "$DRY_RUN" ] || printf '%s\n' "$NEW_IDS" | \
ssh "$@" "$(installkeys_sh)" \
|| exit 1
if ! [ "$DRY_RUN" ] ; then
printf '%s\n' "$NEW_IDS" | \
if [ "$SFTP" ] ; then
installkeys_via_sftp
else
$SSH "$@" "$(installkeys_sh)"
fi || exit 1
fi
ADDED=$(printf '%s\n' "$NEW_IDS" | wc -l)
;;
esac
@ -318,7 +363,7 @@ else
Number of key(s) added: $ADDED
Now try logging into the machine, with: "ssh $SSH_OPTS"
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

View File

@ -1,5 +1,5 @@
.ig \" -*- nroff -*-
Copyright (c) 1999-2016 hands.com Ltd. <http://hands.com/>
Copyright (c) 1999-2020 hands.com Ltd. <http://hands.com/>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
@ -31,6 +31,7 @@ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.Nm
.Op Fl f
.Op Fl n
.Op Fl s
.Op Fl i Op Ar identity_file
.Op Fl p Ar port
.Op Fl o Ar ssh_option
@ -84,6 +85,12 @@ in more than one copy of the key being installed on the remote system.
.It Fl n
do a dry-run. Instead of installing keys on the remote system simply
prints the key(s) that would have been installed.
.It Fl s
SFTP mode: usually the public keys are installed by executing commands on the remote side.
With this option the user's
.Pa ~/.ssh/authorized_keys
file will be downloaded, modified locally and uploaded with sftp.
This option is useful if the server has restrictions on commands which can be used on the remote side.
.It Fl h , Fl ?
Print Usage summary
.It Fl p Ar port , Fl o Ar ssh_option