-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconnect
More file actions
executable file
·181 lines (164 loc) · 6.94 KB
/
Copy pathconnect
File metadata and controls
executable file
·181 lines (164 loc) · 6.94 KB
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
#!/usr/bin/env bash
# connect — SSH server launcher with fzf fuzzy search
# Config: ~/.ssh/connect.conf
# Format: name host [user] [port] [key]
CONFIG="${HOME}/.ssh/connect.conf"
EXAMPLE_CONFIG='# SSH server list for the `connect` command
# Format: name host [user] [port] [key]
# Fields are whitespace-separated. user, port and key are optional.
#
# The "key" field can be either:
# * a path (contains "/", e.g. ~/.ssh/id_ed25519) — an identity file on disk;
# * a bare name (e.g. homelab) — the name/comment of a key held by your SSH
# agent (as shown by `ssh-add -L`). connect pins that single agent key so
# only one identity is offered. Use this with the Bitwarden/Vaultwarden
# agent to avoid "Too many authentication failures" when the agent holds
# many keys — no private key is ever written to disk.
# If "key" is omitted, ssh uses normal auth: any SSH agent on $SSH_AUTH_SOCK
# (offering all its keys) and then a password prompt.
#
# A config-wide fallback key can be set with a "@key" directive (see below);
# it applies to any server that does not name its own key. To reach a server by
# password only — no key offered at all — set its key to "-" (or "none"). This
# skips the @key fallback and disables pubkey auth, avoiding "Too many
# authentication failures" when the agent holds more keys than the server allows.
#
# Examples:
# @key ~/.ssh/id_ed25519
# work-server 192.168.1.10
# dev-box 10.0.0.5 deploy 2222
# staging staging.example.com ubuntu
# backup 192.168.1.10 deploy 22 ~/.ssh/connect_backup
# homelab homelab.example.com deploy 22 homelab
# open-box 10.0.0.9 deploy 22 -
'
# Check dependencies
if ! command -v fzf &>/dev/null; then
echo "Error: fzf is required but not installed."
echo "Install it with: brew install fzf"
exit 1
fi
# Bootstrap config if missing
if [[ ! -f "$CONFIG" ]]; then
printf '%s' "$EXAMPLE_CONFIG" > "$CONFIG"
echo "Created example config at $CONFIG"
echo "Edit it to add your SSH servers, then run 'connect' again."
exit 0
fi
# Read config, skip comments and blank lines, build fzf display lines
# Each record is tab-separated: display, name, host, user, port, key
declare -a entries=()
default_key=""
while IFS= read -r line; do
# Strip leading whitespace, skip comments and blank lines
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "$line" || "$line" == \#* ]] && continue
# @key directive: config-wide fallback identity file for keyless entries
if [[ "$line" == @key[[:space:]]* ]]; then
read -r _ default_key _ <<< "$line"
default_key="${default_key/#\~/$HOME}"
continue
fi
read -r name host user port key extra <<< "$line"
[[ -z "$name" || -z "$host" ]] && continue
user="${user:-$USER}"
port="${port:-22}"
# A key of "-" or "none" means "no key at all": skip the @key fallback AND
# turn off pubkey auth entirely (go straight to password). This avoids
# "Too many authentication failures" on servers where the agent would
# otherwise offer more keys than the server's MaxAuthTries allows. The
# internal sentinel "@none" flows through to the ssh-building step below.
if [[ "$key" == "-" || "$key" == "none" ]]; then
key="@none"
else
key="${key:-$default_key}"
key="${key/#\~/$HOME}"
fi
# Mark key-auth entries in the picker (🔑 pinned key, 🔓 password-only)
marker=""
if [[ "$key" == "@none" ]]; then
marker="🔓"
elif [[ -n "$key" ]]; then
marker="🔑"
fi
# Tab-separated internal record; display columns with printf
display=$(printf "%-20s %-30s %-15s %-5s %s" "$name" "$host" "$user" "$port" "$marker")
entries+=("${display} ${name} ${host} ${user} ${port} ${key}")
done < "$CONFIG"
if [[ ${#entries[@]} -eq 0 ]]; then
echo "No servers found in $CONFIG"
echo "Add entries in the format: name host [user] [port]"
exit 1
fi
# Direct connect if a name argument is given
if [[ -n "$1" ]]; then
selected=$(printf '%s\n' "${entries[@]}" | awk -F'\t' -v name="$1" 'tolower($2) == tolower(name)' | head -1)
if [[ -z "$selected" ]]; then
selected=$(printf '%s\n' "${entries[@]}" | awk -F'\t' -v name="$1" 'index(tolower($2), tolower(name))' | head -1)
fi
if [[ -z "$selected" ]]; then
echo "Error: no server named '$1' found in $CONFIG"
exit 1
fi
else
# Present fzf — search matches on display columns (name + host + user + port)
selected=$(printf '%s\n' "${entries[@]}" | \
fzf \
--prompt="SSH> " \
--header="Select a server (type to filter by name or host)" \
--height=40% \
--layout=reverse \
--with-nth=1 \
--delimiter=$'\t' \
--ansi)
# Cancelled
[[ -z "$selected" ]] && exit 0
fi
# Parse fields from the selected tab-separated record
IFS=$'\t' read -r _display _name host user port key <<< "$selected"
# Build SSH command
ssh_args=()
[[ "$port" != "22" ]] && ssh_args+=(-p "$port")
# Clean up any ephemeral public key on exit (we never write private keys to disk)
tmp_pubkey=""
cleanup() { [[ -n "$tmp_pubkey" ]] && rm -f "$tmp_pubkey"; }
trap cleanup EXIT
if [[ "$key" == "@none" ]]; then
# Password-only: offer no keys at all (skip agent + on-disk identities) so
# a fat agent can't trip the server's MaxAuthTries.
ssh_args+=(-o PubkeyAuthentication=no -o IdentitiesOnly=yes)
elif [[ -n "$key" ]]; then
if [[ "$key" == */* ]]; then
# Identity file on disk: force this key only, no agent fallback
if [[ -f "$key" ]]; then
ssh_args+=(-i "$key" -o IdentitiesOnly=yes)
else
echo "Warning: key file '$key' for ${_name} not found; using default auth" >&2
fi
else
# Agent key name: pin the single matching key served by the SSH agent
# (e.g. the Bitwarden/Vaultwarden agent). The private key stays in the
# agent — only its public part is written to a temp file so ssh offers
# exactly one identity, avoiding "Too many authentication failures".
pub=$(ssh-add -L 2>/dev/null | awk -v n="$key" '
{ comment = $0; sub(/^[^ ]+[ ]+[^ ]+[ ]+/, "", comment) }
comment == n { print; exit }')
if [[ -n "$pub" ]]; then
tmp_pubkey=$(mktemp "${TMPDIR:-/tmp}/connect-XXXXXX") || tmp_pubkey=""
if [[ -n "$tmp_pubkey" ]]; then
printf '%s\n' "$pub" > "$tmp_pubkey"
ssh_args+=(-i "$tmp_pubkey" -o IdentitiesOnly=yes)
fi
else
echo "Warning: agent key '$key' for ${_name} not found in ssh-add -L; using default auth" >&2
fi
fi
fi
ssh_args+=("${user}@${host}")
if [[ "$key" == "@none" ]]; then
auth_note=" (password auth, no keys offered)"
else
auth_note="${key:+ using key ${key}}"
fi
echo "Connecting to ${_name} (${user}@${host}${port:+:${port}})${auth_note}..."
ssh "${ssh_args[@]}"