-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathssh-oci-bastion.sh
executable file
·239 lines (198 loc) · 8.24 KB
/
ssh-oci-bastion.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env bash
set -e
set -o pipefail
readonly VER=2.1.7
# Remove the longest `*/` prefix
readonly SCRIPT_NAME_WITH_EXT="${0##*/}"
usage() {
cat <<HEREDOC
NAME
$SCRIPT_NAME_WITH_EXT -- configure and ssh or create a tunnel to an Oracle Cloud Infrastructure host via the bastion
SYNOPSIS
$SCRIPT_NAME_WITH_EXT [-o OCI_profile] [-n] host_user
$SCRIPT_NAME_WITH_EXT [-o OCI_profile] -p forwarded_host_port
$SCRIPT_NAME_WITH_EXT -h: display this help
DESCRIPTION
The following options are available:
host_user
* Create a session on the bastion for the OCI host with the maximum possible duration (3 h.)
* Configure (append or update) host-specific \`ProxyJump\` directive parameter in the SSH config, which enables
SSH/SFTP in all clients for the session duration.
* ssh as the specified user (unless -n is specified).
-n configure everything, but do not ssh
-p forwarded_host_port create a port-forwarding tunnel from the localhost port to the same port on an OCI bastion,
then the OCI host. The tunnel process would run until the session expires or the process is
terminated by the user.
-o OCI_profile use a specified profile section from the \`~/.oci/config\` file [default: \`DEFAULT\`]
ENVIRONMENT
* Required commands:
* OCI CLI
* \`jq\`
* \`pcregrep\`
* \`perl\`
* Required environment variables:
* \`OCI_INSTANCE_OCID\`, e.g., \`ocid1.instance.oc1.iad.xx\`
* \`OCI_BASTION_OCID\`, e.g., \`ocid1.bastion.oc1.iad.xx\`
* For \`host_user\` SSH sessions only:
* \`OCI_INSTANCE\`, Internal FQDN or Private IP e.g., \`host.example.com\`
* One of the following SSH public keys in \`~/.ssh/\`: \`id_rsa.pub\`, \`id_dsa.pub\`, \`id_ecdsa.pub\`,
\`id_ed25519.pub\`, or \`id_xmss.pub\`. If there are multiple keys the first one found in this order will be used.
* If the SSH config has global (\`Host *\`) \`ProxyJump\` parameter it would take precedence.
Since the first obtained value for each parameter is used, more host-specific declarations should be given near the
beginning of the file, and general defaults at the end. Prepend the following configuration manually in this case
before using this script:
\`\`\`
Host {target}
ProxyJump
\`\`\`
v$VER
HEREDOC
exit 1
}
#declare -a ports
# If a character is followed by a colon, the option is expected to have an argument
while getopts np:o:h OPT; do
case "$OPT" in
n)
readonly SKIP_SSH=true
;;
p)
port="$OPTARG"
;;
o)
readonly PROFILE_OPT=(--profile "$OPTARG")
;;
*) # -h or `?`: an unknown option
usage
;;
esac
done
echo -e "\n[$(date +'%T %Z') v$VER] ${USER:-${USERNAME:-${LOGNAME:-UID #$UID}}}@${HOSTNAME} ${PWD}> $0${*+ }$*\n"
shift $((OPTIND - 1))
# Process positional parameters
readonly HOST_USER=$1
if ! command -v oci >/dev/null; then
# shellcheck disable=SC2016
echo 'Please install OCI CLI' >&2
exit 1
fi
if ! command -v jq >/dev/null; then
# shellcheck disable=SC2016
echo 'Please install `jq`' >&2
exit 1
fi
if ! command -v pcregrep >/dev/null; then
# shellcheck disable=SC2016
echo 'Please install PCRE' >&2
exit 1
fi
if ! command -v perl >/dev/null; then
echo "Please install Perl" >&2
exit 1
fi
for required_env_var in 'OCI_INSTANCE' 'OCI_INSTANCE_OCID' 'OCI_BASTION_OCID'; do
if [[ ! ${!required_env_var} ]]; then
echo "Please define $required_env_var" >&2
exit 1
fi
done
readonly MAX_TTL=$((3 * 60 * 60))
readonly CHECK_INTERVAL_SEC=5
readonly AFTER_SESSION_CREATION_WAIT=7
# Determine which keypair ssh uses by default.
# The default key order as of OpenSSH 8.1p1m (see `ssh -v {destination}`)
for key_type in 'id_rsa' 'id_dsa' 'id_ecdsa' 'id_ed25519' 'id_xmss'; do
pub_key_file=~/.ssh/$key_type.pub
if [[ -f $pub_key_file ]]; then
readonly SSH_PUB_KEY=$pub_key_file
echo "Using $SSH_PUB_KEY as a public key"
break
fi
done
if [[ ! $SSH_PUB_KEY ]]; then
echo 'No SSH public key is found' >&2
exit 1
fi
if [[ $port ]]; then
echo -e "\nCreating a port forwarding tunnel for the port $port: this can take ≈ 10-21s ..."
# `--session-ttl`: session duration in seconds (defaults to 30 minutes, maximum is 3 hours).
# `--wait-interval-seconds`: state check interval (defaults to 30 seconds).
# `--ssh-public-key-file` is required
# `--target-private-ip` "${OCI_INSTANCE}"
time session_ocid=$(
oci bastion session create-port-forwarding "${PROFILE_OPT[@]}" --bastion-id "$OCI_BASTION_OCID" \
--target-resource-id "$OCI_INSTANCE_OCID" --target-port "$port" --session-ttl $MAX_TTL \
--ssh-public-key-file $SSH_PUB_KEY --wait-for-state SUCCEEDED --wait-for-state FAILED \
--wait-interval-seconds $CHECK_INTERVAL_SEC \
| jq --raw-output '.data.resources[0].identifier' &&
printf "It took:" >&2
)
echo "Created the bastion port forwarding session: $session_ocid"
ssh_command=$(oci bastion session get "${PROFILE_OPT[@]}" --session-id "$session_ocid" \
| jq --raw-output '.data["ssh-metadata"].command')
# Result: `ssh -i <privateKey> -N -L <localPort>:{HOST_IP}:5432 -p 22 ocid1.bastionsession.xx@yy.oraclecloud.com`
# Remove the placeholder
ssh_command="${ssh_command/-i <privateKey>/}"
# Replace the placeholder
ssh_command="${ssh_command/<localPort>/localhost:$port}"
echo "Waiting $AFTER_SESSION_CREATION_WAIT seconds..."
# Preventing intermittent `Permission denied (publickey)` errors when trying to ssh immediately after session creation
sleep $AFTER_SESSION_CREATION_WAIT
echo -e "\n[$(date +'%T %Z')] Opening an SSH tunnel. Interrupt (Ctrl+C) the process to close it."
set -x
# This only works assuming there are no internal quotes in the command
$ssh_command
#set +x
exit
fi
if [[ $HOST_USER ]]; then
echo -e "\nCreating a bastion session: this can take ≈ 1m:13s-1m:30s ..."
# `--session-ttl`: session duration in seconds (defaults to 30 minutes, maximum is 3 hours).
# `--wait-interval-seconds`: state check interval (defaults to 30 seconds).
# `--ssh-public-key-file` is required
time session_ocid=$(
oci bastion session create-managed-ssh "${PROFILE_OPT[@]}" --bastion-id "$OCI_BASTION_OCID" \
--target-resource-id "$OCI_INSTANCE_OCID" --target-os-username "$HOST_USER" --session-ttl $MAX_TTL \
--ssh-public-key-file $SSH_PUB_KEY --wait-for-state SUCCEEDED --wait-for-state FAILED \
--wait-interval-seconds $CHECK_INTERVAL_SEC \
| jq --raw-output '.data.resources[0].identifier' &&
printf "It took:" >&2
)
echo "Created the bastion session: $session_ocid"
ssh_command=$(oci bastion session get "${PROFILE_OPT[@]}" --session-id "$session_ocid" \
| jq --raw-output '.data["ssh-metadata"].command')
# Result: `ssh -i <privateKey> -o ProxyCommand=\"ssh -i <privateKey> -W %h:%p -p 22
# ocid1.bastionsession.xx@yy.oraclecloud.com\" -p 22 {HOST_USER}@{HOST_IP}`
# Extract the bastion session SSH destination: the `ocid1.bastionsession.xx@yy.oraclecloud.com` part
# Remove the string head
bastion_session_dest=${ssh_command#*ocid1.bastionsession.}
# Remove the string tail and reconstruct `ocid1.bastionsession.xx@yy.oraclecloud.com`
bastion_session_dest="ocid1.bastionsession.${bastion_session_dest%%oraclecloud.com*}oraclecloud.com"
# Multi-line upsert
if pcregrep -M -q "(?s)Host ${OCI_INSTANCE}.*?ProxyJump" ~/.ssh/config; then
# Update
# -i input edited in-place
# -p iterate over filename arguments
# -0 use null as record separator
# Don't combine these options: the combination might not work
# `@host` in the bastion session has to be escaped
perl -i -p -0 -e "s/(Host ${OCI_INSTANCE}.*?)ProxyJump.*?\n/\1ProxyJump ${bastion_session_dest//@/\\@}\n/s" \
~/.ssh/config
else
# Append
cat >>~/.ssh/config <<HEREDOC
Host ${OCI_INSTANCE}
ProxyJump ${bastion_session_dest}
HEREDOC
fi
if [[ $SKIP_SSH ]]; then
exit 0
fi
echo "Waiting $AFTER_SESSION_CREATION_WAIT seconds..."
# Preventing intermittent `Permission denied (publickey)` errors when trying to ssh immediately after session creation
sleep $AFTER_SESSION_CREATION_WAIT
echo -e "\n[$(date +'%T %Z')] SSH to the target instance via a jump host"
set -x
ssh "${HOST_USER}@${OCI_INSTANCE}"
#set +x
fi