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