Skip to content

Instantly share code, notes, and snippets.

@arrjay
Created July 10, 2023 22:12
Show Gist options
  • Save arrjay/2d3a6f9f761bd287e05a96e07603b63b to your computer and use it in GitHub Desktop.
Save arrjay/2d3a6f9f761bd287e05a96e07603b63b to your computer and use it in GitHub Desktop.
password store extension for ssh wrangles
#!/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