ssh-copy-id 12 KB

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