Created
July 10, 2023 22:12
-
-
Save arrjay/2d3a6f9f761bd287e05a96e07603b63b to your computer and use it in GitHub Desktop.
password store extension for ssh wrangles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Created by argbash-init v2.10.0 | |
# Rearrange the order of options below according to what you would like to see in the help message. | |
# ARG_OPTIONAL_SINGLE([host],[h],[actual host to connect to],[]) | |
# ARG_OPTIONAL_SINGLE([port],[p],[port number for SSH daemon on actual host],[22]) | |
# ARG_OPTIONAL_SINGLE([labelname],[l],[value of SSH matching label for ProxyCommand use],[]) | |
# ARG_OPTIONAL_SINGLE([agentpath],[A],[path to SSH agent for ProxyCommand use (IdentityAgent)],[]) | |
# ARG_OPTIONAL_SINGLE([exec],[c],[for exec/run/shell, execute command string],[$SHELL -i]) | |
# ARG_OPTIONAL_SINGLE([item],[i],[password item to use],[]) | |
# ARG_OPTIONAL_BOOLEAN([passthrough],[],[if pulling an SSH key out of pass does not work, continue processing, but swap in $SSH_AGENT_SOCK instead of creating a transient agent],[off]) | |
# ARG_POSITIONAL_SINGLE([verb],[sub-command to perform],[]) | |
# ARG_DEFAULTS_POS() | |
# ARG_HELP([ssh key/agent integration for password-store],[],[?]) | |
# ARGBASH_GO() | |
# needed because of Argbash --> m4_ignore([ | |
### START OF CODE GENERATED BY Argbash v2.10.0 one line above ### | |
# Argbash is a bash code generator used to get arguments parsing right. | |
# Argbash is FREE SOFTWARE, see https://argbash.io for more info | |
die() | |
{ | |
local _ret="${2:-1}" | |
test "${_PRINT_HELP:-no}" = yes && print_help >&2 | |
echo "$1" >&2 | |
exit "${_ret}" | |
} | |
begins_with_short_option() | |
{ | |
local first_option all_short_options='hplAci?' | |
first_option="${1:0:1}" | |
test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 | |
} | |
# THE DEFAULTS INITIALIZATION - POSITIONALS | |
_positionals=() | |
_arg_verb= | |
# THE DEFAULTS INITIALIZATION - OPTIONALS | |
_arg_host= | |
_arg_port="22" | |
_arg_labelname= | |
_arg_agentpath= | |
_arg_exec="$SHELL -i" | |
_arg_item= | |
_arg_passthrough="off" | |
print_help() | |
{ | |
printf '%s\n' "ssh key/agent integration for password-store" | |
printf 'Usage: %s [-h|--host <arg>] [-p|--port <arg>] [-l|--labelname <arg>] [-A|--agentpath <arg>] [-c|--exec <arg>] [-i|--item <arg>] [--(no-)passthrough] [-?|--help] <verb>\n' "$0" | |
printf '\t%s\n' "<verb>: sub-command to perform" | |
printf '\t%s\n' "-h, --host: actual host to connect to (no default)" | |
printf '\t%s\n' "-p, --port: port number for SSH daemon on actual host (default: '22')" | |
printf '\t%s\n' "-l, --labelname: value of SSH matching label for ProxyCommand use (no default)" | |
printf '\t%s\n' "-A, --agentpath: path to SSH agent for ProxyCommand use (IdentityAgent) (no default)" | |
printf '\t%s\n' "-c, --exec: for exec/run/shell, execute command string (default: '$SHELL -i')" | |
printf '\t%s\n' "-i, --item: password item to use (no default)" | |
printf '\t%s\n' "--passthrough, --no-passthrough: if pulling an SSH key out of pass does not work, continue processing, but swap in $SSH_AGENT_SOCK instead of creating a transient agent (off by default)" | |
printf '\t%s\n' "-?, --help: Prints help" | |
} | |
parse_commandline() | |
{ | |
_positionals_count=0 | |
while test $# -gt 0 | |
do | |
_key="$1" | |
case "$_key" in | |
-h|--host) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_host="$2" | |
shift | |
;; | |
--host=*) | |
_arg_host="${_key##--host=}" | |
;; | |
-h*) | |
_arg_host="${_key##-h}" | |
;; | |
-p|--port) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_port="$2" | |
shift | |
;; | |
--port=*) | |
_arg_port="${_key##--port=}" | |
;; | |
-p*) | |
_arg_port="${_key##-p}" | |
;; | |
-l|--labelname) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_labelname="$2" | |
shift | |
;; | |
--labelname=*) | |
_arg_labelname="${_key##--labelname=}" | |
;; | |
-l*) | |
_arg_labelname="${_key##-l}" | |
;; | |
-A|--agentpath) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_agentpath="$2" | |
shift | |
;; | |
--agentpath=*) | |
_arg_agentpath="${_key##--agentpath=}" | |
;; | |
-A*) | |
_arg_agentpath="${_key##-A}" | |
;; | |
-c|--exec) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_exec="$2" | |
shift | |
;; | |
--exec=*) | |
_arg_exec="${_key##--exec=}" | |
;; | |
-c*) | |
_arg_exec="${_key##-c}" | |
;; | |
-i|--item) | |
test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1 | |
_arg_item="$2" | |
shift | |
;; | |
--item=*) | |
_arg_item="${_key##--item=}" | |
;; | |
-i*) | |
_arg_item="${_key##-i}" | |
;; | |
--no-passthrough|--passthrough) | |
_arg_passthrough="on" | |
test "${1:0:5}" = "--no-" && _arg_passthrough="off" | |
;; | |
-\?|--help) | |
print_help | |
exit 0 | |
;; | |
-\?*) | |
print_help | |
exit 0 | |
;; | |
*) | |
_last_positional="$1" | |
_positionals+=("$_last_positional") | |
_positionals_count=$((_positionals_count + 1)) | |
;; | |
esac | |
shift | |
done | |
} | |
handle_passed_args_count() | |
{ | |
local _required_args_string="'verb'" | |
test "${_positionals_count}" -ge 1 || _PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require exactly 1 (namely: $_required_args_string), but got only ${_positionals_count}." 1 | |
test "${_positionals_count}" -le 1 || _PRINT_HELP=yes die "FATAL ERROR: There were spurious positional arguments --- we expect exactly 1 (namely: $_required_args_string), but got ${_positionals_count} (the last one was: '${_last_positional}')." 1 | |
} | |
assign_positional_args() | |
{ | |
local _positional_name _shift_for=$1 | |
_positional_names="_arg_verb " | |
shift "$_shift_for" | |
for _positional_name in ${_positional_names} | |
do | |
test $# -gt 0 || break | |
eval "$_positional_name=\${1}" || die "Error during argument parsing, possibly an Argbash bug." 1 | |
shift | |
done | |
} | |
parse_commandline "$@" | |
handle_passed_args_count | |
assign_positional_args 1 "${_positionals[@]}" | |
# OTHER STUFF GENERATED BY Argbash | |
### END OF CODE GENERATED BY Argbash (sortof) ### ]) | |
# [ <-- needed because of Argbash | |
# begin non-argument script ;) | |
for cmd in pass mktemp awk grep socat ; do | |
type "${cmd}" 2>/dev/null 1>&2 || { printf '%s required, please add to PATH\n' "${cmd}" 1>&2 ; exit 1 ; } | |
done | |
cleanup () { | |
local function | |
[ "${exit_commands[0]}" ] || return 0 | |
for function in "${exit_commands[@]}" ; do | |
${function} | |
done | |
} | |
trap cleanup EXIT ERR | |
# save the SSH_AUTH_SOCK, then discard knowledge of any SSH agents. | |
# any SSH agents *we* start? KILL THEM WHEN DONE. | |
old_ssh_agent="${SSH_AUTH_SOCK}" | |
unset SSH_AGENT_PID SSH_AUTH_SOCK | |
cleanup_ssh_agent () { | |
[ "${SSH_AGENT_PID}" ] && ssh-agent -k > /dev/null | |
} | |
exit_commands=("${exit_commands[@]}" "cleanup_ssh_agent") | |
# we always have a file ready for holding pass item data. | |
cleanup_pass_data () { | |
[ "${pass_data}" ] && rm -f "${pass_data}" | |
} | |
exit_commands=("${exit_commands[@]}" "cleanup_pass_data") | |
pass_data="$(mktemp /dev/shm/pdata.XXXXXXXX)" | |
# similar, we also always have a key. | |
cleanup_ssh_key () { | |
[ "${ssh_key}" ] && rm -f "${ssh_key}" | |
} | |
exit_commands=("${exit_commands[@]}" "cleanup_ssh_key") | |
ssh_key="$(mktemp /dev/shm/tssh.XXXXXXXX)" | |
pull_pass_item () { | |
local item="${1}" | |
pass ls "${item}" > "${pass_data}" || return 1 | |
} | |
get_pass_sshkey () { | |
local item="${1}" | |
pull_pass_item "${item}" || return 1 | |
awk '/-----BEGIN OPENSSH PRIVATE KEY-----/{f=1}/-----END OPENSSH PRIVATE KEY-----/{print;f=0}f' "${pass_data}" > "${ssh_key}" | |
[ -s "${ssh_key}" ] || return 1 | |
} | |
start_transient_agent () { | |
local agentpath="${1}" | |
local ssh_args=() | |
[ "${agentpath}" ] && ssh_args=('-a' "${agentpath}") | |
# shellcheck disable=SC2046 | |
eval $(ssh-agent "${ssh_args[@]}") > /dev/null | |
} | |
cleanup_ssh_phrase () { | |
[ "${askpass_exec}" ] && rm -f "${askpass_exec}" | |
} | |
exit_commands=("${exit_commands[@]}" "cleanup_ssh_phrase") | |
get_pass_sshphrase () { | |
local item="${1}" | |
pull_pass_item "${item}" || return 1 | |
grep -qF "ssh_passphrase: " "${pass_data}" || return 1 | |
# NOTE: we leaked askpass_exec to globals on purpose | |
askpass_exec="$(mktemp /dev/shm/sshp.XXXXXXXX)" | |
{ | |
printf '%s\n' '#!/usr/bin/env -S awk -f' | |
printf 'BEGIN { ARGV[1] = "%s" ; ARGC = 2 }\n' "${pass_data}" | |
# shellcheck disable=SC2016 | |
printf '$1 == "ssh_passphrase:" { print $2 ; }\n' | |
} > "${askpass_exec}" | |
chmod +x "${askpass_exec}" | |
export SSH_ASKPASS="${askpass_exec}" | |
} | |
load_ssh_key () { | |
local item="${1}" | |
get_pass_sshkey "${item}" || return 1 | |
get_pass_sshphrase "${item}" | |
# handle forcing ssh-add to use our askpass, if we have it. | |
if [[ "${askpass_exec}" ]] ; then | |
ssh-add "${ssh_key}" < /dev/null | |
else | |
ssh-add "${ssh_key}" | |
fi | |
} | |
cleanup_socat () { | |
[ "${socat_pid}" ] && kill "${socat_pid}" | |
[ "${socat_wd}" ] && rmdir "${socat_wd}" | |
} | |
exit_commands=("${exit_commands[@]}" "cleanup_socat") | |
start_socat () { | |
local agentpath="${1}" | |
# we *need* to have the old agent, bail if we don't. | |
[ "${old_ssh_agent}" ] || return 1 | |
# if we don't have an agentpath, do something... | |
[ "${agentpath}" ] || { | |
socat_wd=$(mktemp -d) | |
agentpath="${socat_wd}/socat.agent" | |
} | |
socat "UNIX-LISTEN:${agentpath},fork" "UNIX:${old_ssh_agent}" & socat_pid=$! | |
} | |
setup_and_exec () { | |
if get_pass_sshkey "${_arg_item}" ; then | |
start_transient_agent "${_arg_agentpath}" | |
load_ssh_key "${_arg_item}" | |
else | |
# check if we have an ssh key to do things or if we are invoking passthrough | |
# hm. passthrough time? | |
case "${_arg_passthrough}" in | |
off) | |
# no key, stop here. | |
{ printf 'failed to load ssh keys for pass item, STOPPING.\n' 1>&2 ; exit 1 ; } | |
;; | |
on) | |
# this is...kinda silly for the exec command, but it ensures EVERYTHING WORKS. | |
start_socat "${_arg_agentpath}" | |
;; | |
esac | |
fi | |
# NOTE: we're unquoted here! | |
printf 'running %s\n' "${_arg_exec}" 1>&2 | |
${_arg_exec} | |
} | |
# fatally exit the script if we fail this | |
err_req_item () { | |
[[ "${_arg_item}" ]] || { printf 'password item (-i, --item) required\n' 1>&2 ; exit 1 ; } | |
} | |
err_req_privkey () { | |
get_pass_sshkey "${_arg_item}" || { printf 'did not find SSH key in item %s\n' "${_arg_item}" 1>&2 ; exit 1 ; } | |
} | |
# support function for my common case of pass encoding on top of a two-dot hostname. | |
munge_twodots () { | |
local label_in="${1}" | |
local qualifier="${2}" | |
local component="${2//.//}" # turn all dots into slashes for password store items | |
local res="${label_in%.*.*}" # remove the last two dots of a thing (hence munge_twodots) | |
res="${res//.//}" | |
# turn the last part _back into dots_ | |
local res_pre="${res%"${component}"*}" | |
local res_suf="${res#"${res_pre}${component}"}" | |
printf '%s%s%s' "${res_pre}" "${qualifier}" "${res_suf}" | |
} | |
# _this_ function is pass item specific, and probably the one you want to mess with. | |
munge_label () { | |
local label="${1}" | |
case "${label}" in | |
*.github.com) | |
munge_twodots "${label}" "github.com" | |
;; | |
*.gitlab.com) | |
munge_twodots "${label}" "gitlab.com" | |
;; | |
# default - get your label back | |
*) | |
printf '%s\n' "${label}" | |
;; | |
esac | |
} | |
case "${_arg_verb}" in | |
showpriv) | |
err_req_item | |
err_req_privkey | |
cat "${ssh_key}" | |
;; | |
showphrase) | |
err_req_item | |
err_req_privkey | |
get_pass_sshphrase "${_arg_item}" | |
if [[ "${askpass_exec}" ]] ; then | |
"${askpass_exec}" | |
else | |
{ printf 'no passphrase found in password item\n' 1>&2 ; exit 1 ; } | |
fi | |
;; | |
pubkey) | |
err_req_item | |
err_req_privkey | |
get_pass_sshphrase "${_arg_item}" | |
if [[ "${askpass_exec}" ]] ; then | |
ssh-keygen -y -f "${ssh_key}" < /dev/null | |
else | |
ssh-keygen -y -f "${ssh_key}" | |
fi | |
;; | |
exec|run|shell) | |
err_req_item | |
setup_and_exec | |
;; | |
proxy) | |
# oh lord this is a fun one. | |
[[ "${_arg_host}" ]] || { printf 'destination host (-h, --host) required' 1>&2 ; exit 1 ; } | |
[[ "${_arg_labelname}" ]] || { printf 'ssh-config label (-l, --labelname) required' 1>&2 ; exit 1 ; } | |
[[ "${_arg_agentpath}" ]] || { printf 'ssh IdentityAgent path (-A, --agentpath) required' 1>&2 ; exit 1 ; } | |
# now, we need to _derive_ arg_item...we will munge the labelname to find one. | |
_arg_item="$(munge_label "${_arg_labelname}")" | |
# replace the exec arg with socat... | |
_arg_exec="socat - tcp-connect:${_arg_host}:${_arg_port}" | |
# and away! | |
setup_and_exec | |
;; | |
esac | |
# ] <-- needed because of Argbash |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment