-
Notifications
You must be signed in to change notification settings - Fork 2
/
SshHelper
executable file
·457 lines (363 loc) · 15.5 KB
/
SshHelper
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
#!/usr/bin/env bash
. "${BASH_SOURCE[0]%/*}/function.sh" script || exit
usage() { ScriptUsage "$1" "\
Usage: $(ScriptName) [OPTION]... config|connect|IsAvailable|regenerate
Additional SSH commands."; }
init() { defaultCommand="connect"; knownHosts="$HOME/.ssh/known_hosts"; }
argStart() { unset -v file force; }
isAvailableUsage() { ScriptUsageEcho "Usage: $(ScriptName) IsAvailable HOST\nCheck if a host is available using SSH.\n\n -t, --timeout $(ScriptOptTimeoutUsage)"; }
isAvailableArgStart() { ScriptOptTimeoutArgStart; }
isAvailableArgs() { ScriptArgGet "host" -- "$@"; }
isAvailableOpt() { [[ "$1" == @(--timeout|--timeout=*|-t|-t=*) ]] && ScriptOptTimeout "$@"; }
isAvailableCommand() { otherArgs=(ls /); connectCommand > /dev/null; }
#
# Config Command
#
configUsage() { echot "Usage: $(ScriptName) config check|diff|edit|get|sync\nSSH configuration configuration."; }
configCommand() { usage; }
configEditCommand() { TextEdit ~/.ssh/config; }
#
# config check command
#
configCheckUsage() { echot "Usage: $(ScriptName) config check HOST\nReturn true if HOST matches an entry in ~/.ssh/config."; }
configCheckArgs() { ScriptArgGet "host" -- "$@"; }
configCheckCommand() { configCheck "$host"; }
configCheck()
{
local hostFull="$1" host="$(GetSshHost "$1")" defaultFull default="DEFAULT_CONFIG"
defaultFull="${hostFull/$host/$default}"
# if the SSH configuration changes a value other than the host, return 0 (host in SSH config)
[[ "$(ssh -G "$defaultFull" | grep -i -v "^hostname ${default}$")" != "$(ssh -G "$hostFull" | grep -i -v "^hostname ${host}$")" ]] && return 0
# if the host is unchanged return 1 (host not in SSH config)
ssh -G "$hostFull" | grep -i "^hostname ${host}$" >& /dev/null && return 1
# the host is changed, return 0 (host is in SSH config)
return 0
}
#
# config diff command
#
configDiffUsage() { echot "Usage: $(ScriptName) config diff HOST\n.Show the changes for the host in ~/.ssh/config."; }
configDiffArgs() { ScriptArgGet "host" -- "$@"; }
configDiffCommand() { configDiff "$host"; }
configDiff()
{
local hostFull="$1" host="$(GetSshHost "$1")" defaultFull default="DEFAULT_CONFIG"
defaultFull="${hostFull/$host/$default}"
ssh -G "$defaultFull" | grep -i -v "^hostname ${default}$" > "/tmp/default.txt"
ssh -G "$hostFull" | grep -i -v "^hostname ${host}$" > "/tmp/$host.txt"
merge "/tmp/default.txt" "/tmp/$host.txt"
}
#
# config get command
#
configGetUsage() { echot "Usage: $(ScriptName) config get HOST VALUE\nReturn the SSH configuration value for HOST from the SSH configuration."; }
configGetArgStart() { host="" value=""; }
configGetArgs() { ScriptArgGet "host" -- "$@"; shift; ScriptArgGet "value" -- "$@"; shift; }
configGetCommand() { configGet "$host" "$value"; }
configGet() { local host="$1" value="$2"; ssh -T -G "$host" | grep -i "^$value " | head -1 | cut -d" " -f2; }
#
# config sync command
#
configSyncUsage() { EchoWrap "Usage: $(ScriptName) config sync root|win|HOST...
Synronize SSH configuration with the specified host. This ensures that the current user can access other systems as the current user and as root.
-do, --dest-older assume destination configuration is older than the source configuration
-so, --src-older assume local configuration is older than the destination configuration"; }
configSyncArgStart() { unset -v hosts method; }
configSyncOpt()
{
case "$1" in
--destination-older|--dest-older|-do) method="--dest-older";;
--source-older|--src-older|-so) method="--src-older";;
*) return 1
esac
}
configSyncArgs() { hosts=( "$@" ); shift="$#"; }
configSyncCommand() { [[ ! $hosts ]] && MissingOperand "host"; local host; for host in "${hosts[@]}"; do configSyncHost "$host" || return; done; }
configSyncRootCommand() { configSync ~root/.ssh "sudoc"; }
configSyncWinCommand() { configSync "$WIN_HOME/.ssh"; }
configSync()
{
local dir="$1" sudo="$2" file files=(authorized_keys config environment id_ed25519 id_ed25519.pub id_rsa id_rsa.pub known_hosts) # id_rsa needed for UniFi devices
$sudo ${G}mkdir --parents "$dir" || return
for file in "${files[@]}"; do
[[ -f "$HOME/.ssh/$file" ]] && { sudoc cp "$HOME/.ssh/$file" "$dir" || return; }
done
return 0
}
configSyncHost()
{
local host="$1"
local args=(-confirmbigdel -ignore 'Name .*_sync.txt' -ignore 'Name environment'); [[ $noPrompt ]] && args+=(-batch)
# do not sync the source
IsLocalHost "$host" && return
# initialize
SshAgentConf "${globalArgs[@]}" || return
# sync
local src="$HOME/.ssh"
local dest=""ssh://$host/.ssh""
[[ "$method" == "--src-older" ]] && args+=(-force "$dest")
[[ "$method" == "--dest-older" ]] && args+=(-force "$src")
RunLog unison "${args[@]}" "$src" "$dest" || return
}
#
# Connect Command
#
connectUsage()
{
ScriptUsageEcho "Usage: $(ScriptName) connect HOST
Connect to a HOST using SSH.
-a, --all resolve host using all methods (DNS, MDNS)
--function define functions
$hostUsage
-i, --interactive interactive shell, implies --function, --pseudo-terminal, and --quote
-p, --password specify a password
--quote quote arguments
-t, --pseudo-terminal allocate a pseudo terminal
-x, --x-forwarding connect with X forwarding"
}
connectUsageVerbose()
{
ScriptUsageEcho "
Other options:
-c, --credential suport credential prompts on the remote host using the CLI, Vault, or an X Window
-m, --mosh connecting using mosh
-sa, --ssh-agent configure the SSH agent
-T, --trust trust host identification ignoring possible security issues
-to, --timeout $(ScriptOptTimeoutUsage)
-w, --wait wait for SSH to become available
Multiple hosts:
-b, --brief display a brief header by prefixing the command with the host name
-e, --errors if a command returns an error track it, return the total number of errors
Environment options:
--borg send BorgBackup environment variables
--credential-manager send credential manager environment variables
--hashi send HashiCorp environment variables"
}
connectArgStart()
{
unset -v all borg briefHeader credentialManager errors functions hashi interactive mosh password pseudoTerminal quote sshAgent trust wait x
unset hostArg hostOpt hosts
ScriptOptTimeoutArgStart
}
connectOpt()
{
ScriptOptHost "$@" && return
case "$1" in
--all|-a) all="--resolve-all";;
--borg) borg="--borg";;
--brief|-b) briefHeader="--brief";;
--credential|-c) hashi="--hashi" pseudoTerminal="-t" x="--x-forwarding";;
--credential-manager) credentialManager="--credential-manager";;
--errors|-e) errors="--errors";;
--function) functions="--functions";;
--hashi) hashi="--hashi";;
--interactive|-i) interactive="-i";;
--mosh|-m) mosh="--mosh";;
--password|--password=*|-p|-p*) ScriptOptGet "password" "$@";;
--pseudo-terminal|-t) pseudoTerminal="-t";;
--quote) quote="--quote";;
--ssh-agent|-sa) sshAgent="--ssh-agent";;
--trust|-T) trust="--trust";;
--timeout|--timeout=*|-to|-to=*) ScriptOptTimeout "$@";;
--wait|-w) wait="--wait";;
--x-forwarding|-x) x="--x-forwarding";;
*) return 1
esac
}
connectArgs()
{
[[ ! $hostOpt ]] && { ScriptArgGet --required "hostArg" "host" -- "$@"; shift; }
otherArgs=( "$@" "${otherArgs[@]}" ); (( shift+=$# )); return 0;
}
connectCommand()
{
local args=($errors); [[ $briefHeader ]] && args+=(--brief) || args+=(--header "running")
ForAllHosts connect "${args[@]}"
}
connect()
{
local host="$1"
# configure the SSH agent - do not do by default for perofrmance
[[ $sshAgent ]] && { RunLog SshAgentConf "${globalArgs[@]}" || return; }
# special handling
connectHostInit "$host" || return
# parse host, format USER@HOST:PORT
local hostOrig="$host" port user
GetSshHost "$hostOrig" host
GetSshPort "$hostOrig" port
GetSshUser "$hostOrig" user; user="${user:-$(SshUser "$host")}" || return
# get host alias
host="$(os name real "$host")" || return
# Get the IP unless host is specified in ~/.ssh/config.
# - this allows use of additional name resolution methods such as MDNS
# - the host must have a known IP address
local ip
host="$(SshConfigGet "$hostOrig" HostName)" || return
if [[ "$host" == "$hostOrig" ]] && ${G}grep "^$host" "$HOME/.ssh/known_hosts" | IpFilter --quiet; then
ip="$(GetIpAddress $all --vm "$hostOrig" $quiet)" || return
host="$ip"
fi
# get the port from configuration if none was specified so we can check if host is available
[[ ! $port ]] && ! port="$(configGet "$hostOrig" "port")" && { ScriptErr "could not find the SSH port for '$host'"; return 1; }
# logging
[[ $verbose ]] && log1 "host=${ip:-$(SshConfigGet "$host" "hostname")} hostOrig=$hostOrig port=$port user=${user:-$(configGet "$host" "user")}"
# wait for SSH to become available if needed
if [[ $wait ]]; then
[[ ! $port ]] && { port="$(portCommand)" || return; }
WaitForPort "$host" "$port" || return
# the host is not available on the specified port
elif [[ $port ]] && ! IsAvailablePort "$host" "$port" $timeout; then
local desc="$hostOrig"; [[ $ip ]] && desc+=" ($ip)"
ScriptErrQuiet "'$desc' is not responding on port $port"
return 1
fi
# arguments for all commands
local args=()
[[ $user ]] && args+=("$user@$host") || args+=("$host")
# mosh
if [[ $mosh ]]; then
IsPlatform --host "$host" mac && args+=( --server='/opt/homebrew/bin/mosh-server')
export LC_ALL=en_US.UTF-8; export LANG=en_US.UTF-8; export LANGUAGE=en_US.UTF-8 # fix UTF-8 errors connecting from WSL
RunLog mosh "${args[@]}"
return
fi
# hashi causes spawn error with password spawn
[[ $password ]] && unset hashi
# arguments
[[ $borg ]] && args+=("-o" SendEnv="BORG_*")
[[ $credentialManager ]] && ! IsPlatform mac && args+=("-o" SendEnv="CREDENTIAL_*") # don't send for mac as target likely uses a different credential manager. This setting may prevent recursion but is it necessary?
[[ $credentialManager && "$CREDENTIAL_MANAGER" == "vault" ]] && args+=("-o" SendEnv="VAULT_*")
[[ $hashi ]] && args+=("-o" SendEnv="CONSUL_* NOMAD_* VAULT_*")
[[ $port ]] && args+=(-p "$port")
# if otherArgs is set we are running an SSH command not a login shell
if [[ $otherArgs ]]; then
# allow the command to run interactively
if [[ $interactive ]]; then
args+=(-- BashRun) quote="--quote" pseudoTerminal="-t"
# allow command to use functions
elif [[ $functions ]]; then
args+=(-- ". function.sh \"\";")
fi
# use "$(ArrayShow otherArgs)" to preserve spaces
if [[ $quote ]]; then
args+=("$(ArrayShow otherArgs)")
else
args+=("${otherArgs[@]}")
fi
fi
set -- "${args[@]}"
# WSL 1 does not support X sockets over ssh and requires localhost
IsPlatform wsl1 && export DISPLAY="localhost:0"
# get the command
local command
if [[ ! $x ]]; then
command=(ssh)
elif IsPlatform mac,wsl2; then # WSL 2 and mac XQuartz requires trusted X11 forwarding (X programs are trusted to use all X features on the host)
command=(ssh -Y)
else # for everything else, use untrusted X Forwarding, where X programs are not trusted to use all X features on the host
command=(ssh -X)
fi
# supress warnings
[[ ! $verbose ]] && command+=(-q)
# verbosity - don't allow if sending credentials, otherwise reduce verbosity by 1 to reduce noise
[[ ! $borg && ! $credentialManager && ! $hashi ]] && (( verboseLevel > 1 )) && command+=(-$(StringRepeat v $((verboseLevel - 1))))
# pseudo termianl - allocate a pseudo terminal if possible (stdout is connected).
# If stdout is not connected, interactive shells have the following limitations which cause issues:
# - when the command writes to stderr, it is sent to the ssh stdout
# - writes to stdout cannout be read using the read command, i.e. CurrentColumn blocks
[[ $pseudoTerminal ]] && IsStdOut && command+=($pseudoTerminal)
# trust
[[ $trust ]] && command+=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
# logging
log1 "command: ${command[*]} $*"
[[ $test ]] && return
# connect
local result
if [[ "$password" ]]; then
command="spawn -noecho ${command[@]} -o PreferredAuthentications=keyboard-interactive,password -o PubkeyAuthentication=no $@; expect \"assword:\"; send \"$password\r\"; interact"
expect -c "$command"
else
"${command[@]}" "$@"
fi
result="$?"
# display SSH error (exit status 255) if supressed with -q
(( result == 255 )) && [[ $quietArg ]] && ScriptErr "SSH error occurred\nTry again with 'SshHelper --verbose' for more detail"
return "$result"
}
connectHostInit()
{
case "$(RemoveDnsSuffix "$1")" in
# nas2) password="$(credential get secure system)";;
# nas3) HashiConf --config-prefix=prod && ScriptEval qnap cli login vars $force;;
unifi) password="$(credential get unifi admin)";; # password not required if UniFi SSH keys are working
verabridge) password="$(credential get vera root)";; # password not required if UniFi SSH keys are working
oversoul|oversoulw1|oversoulw2|oversoulw3|oversoulw4|pm) host="oversoulw4" password="$(credential get wiggin pm)";;
esac
}
#
# Permission Command
#
permissionUsage() { echot "Usage: $(ScriptName) permission USER\nFix SSH permissions for user."; }
permissionArgStart() { user=""; }
permissionArgs() { ScriptArgGet "user" -- "$@"; shift; }
permissionCommand()
{
local skipGroup; IsPlatform mac && skipGroup="true"
sudo bash -l <<-EOF # does not work with sudoc
sudo find "$USERS/$user/.ssh" | ${G}xargs chown $user || exit
[[ "$skipGroup" != "true" ]] && { sudo find "$USERS/$user/.ssh" | sudo ${G}xargs chgrp "$user" || exit; }
sudo chmod 700 "$USERS/$user/.ssh" || exit
[[ -f "$USERS/$user/.ssh/config" ]] && { sudo chmod 700 "$USERS/$user/.ssh/config" || exit; }
[[ -f "$USERS/$user/.ssh/authorized_keys" ]] && { sudo chmod 700 "$USERS/$user/.ssh/authorized_keys" || exit; }
[[ -f "$USERS/$user/.ssh/id_rsa" ]] && { sudo chmod 700 "$USERS/$user/.ssh/id_rsa" || exit; }
[[ -f "$USERS/$user/.ssh/id_ed25519" ]] && { sudo chmod 700 "$USERS/$user/.ssh/id_ed25519" || exit; }
exit 0
EOF
}
#
# regenerate command
#cred
regenerateUsage() { echot "Usage: $(ScriptName) regenerate [check|duplicate]\nRegenerate SSH host keys."; }
regenerateArgStart() { hostKey="/etc/ssh/ssh_host_ecdsa_key.pub"; }
regenerateCommand()
{
# check if another hosts uses the SSH host key in ~/.ssh/known_hosts
local duplicate; duplicate="$(regenerateDuplicate)" || return
[[ -f "$hostKey" && "$duplicate" == "false" && ! $force ]] && return 0
# check if user really wants to regenerate the SSH host keys
if [[ ! $noPrompt ]]; then
! ask 'Regenerate SSH host keys' --default-response n && return
fi
# regenerate the SSH host keys
sudo rm -f /etc/ssh/ssh_host_* && sudo ssh-keygen -A
}
#
# regenerate check command
#
regenerateCheckUsage() { echot "Usage: $(ScriptName) regenerate \nCheck which hosts use SSH host keys."; }
regenerateCheckCommand() { regenerateCheck; }
regenerateCheck()
{
[[ ! -f "$hostKey" || ! -d "$knownHosts" ]] && return
grep "$(sudo cat "$hostKey" | cut -d" " -f2)" "$knownHosts"
}
#
# regenerate duplicate command
#
regenerateDuplicateUsage() { echot "Usage: $(ScriptName) regenerate \nReturn true if the SSH host key is used by another host and false if not."; }
regenerateDuplicateCommand() { regenerateDuplicate; }
regenerateDuplicate()
{
# get the number of hosts using this hosts SSH host key from the ~/.ssh/known_hosts file
local count; count="$(regenerateCheck | wc -l | RemoveSpace)"
! IsInteger "$count" && { ScriptErr "'$count' is not an integer" "regenerateDuplicate"; return 1; }
# more than one hosts are using this hosts SSH host key
(( count > 1 )) && { echo "true"; return; }
# no hosts are using the SSH host key
(( count == 0 )) && { echo "false"; return; }
# check if the one host using
local host; host="$(regenerateCheck | cut -d" " -f1 | cut -d"," -f1)" || return 2
[[ ! $host ]] && return 1
IsLocalHost "$host" && echo "false" || echo "true"
}
ScriptRun "$@"