Skip to content

Instantly share code, notes, and snippets.

@TurtleEngr
Last active June 11, 2024 02:47
Show Gist options
  • Save TurtleEngr/d4725cd6989e0ebbb32e84d8d1a1abe0 to your computer and use it in GitHub Desktop.
Save TurtleEngr/d4725cd6989e0ebbb32e84d8d1a1abe0 to your computer and use it in GitHub Desktop.
ssh-agent helper script
#!/bin/bash
# $Id: sshagent,v 1.81 2024/06/01 16:13:53 bruce Exp $
#set -u
# Var prefix key
# cgVar - global constant
# gVar - global var
# gpVar - global parameter. Usually a CLI option, or predefined
# pVar - a function parameter (local)
# tVar - a local variable
# Globals that will be unset at end
export cgBin=""
export cgEnvDir=""
export cgHomeDir=""
export cgProgList=""
export cgScriptName=""
export gpAction=""
export gErr=0
export gSARunning=""
export gpParm=""
# Globals that will not be unset at end
export cgAgentOwner
export gpDebug=${gpDebug:-0}
export cgEnvFile
# --------------------
fUsage() {
local pStyle=$1
gErr=1
if [ "$pStyle" = "short" ]; then
pod2usage $cgBin/$cgScriptName
return 1
fi
pod2text $cgBin/$cgScriptName | less
return 1
cat <<'EOF' &>/dev/null
=pod
=head1 NAME
sshagent - setup the ssh agent process
=head1 SYNOPSIS
. sshagent [-h] [-s] [-k] [-x] [pKey ...]
cgAgentOwner - user name. Default: $USER
For online help page, go to:
https://github.com/TurtleEngr/my-utility-scripts/blob/develop/doc/sshagent.md
=head1 DESCRIPTION
Using ssh-agent is a lot more secure than using a passwordless ssh
key. If you use passwordless keys, you are following a very bad
pattern, which could lead to large security issues.
This sshagent script is a wrapper for ssh-agent and ssh-add to make it
easier to setup and use a ssh-agent. It only starts an agent process
if one isn't already running, and it saves the PID env. var. values
for use by scripts.
See the EXAMPLES section for how to use this script.
If an agent is found, then the env. var. are set, the pKeys are added,
then listed. If an agent is not found, all other agents are killed,
and a new agent is initialized, the pKeys are added, and the
env. var. are set.
If -s is used (no pKeys) and an agent is running, then env. vars. are
set to use the agent. If an agent is not running, then the agent
related env. vars. are unset. -s is commonly used in cron job scripts.
If -k is used (no pKeys), then all ssh-agent will be killed.
If cgAgentOwner env. is set, then that will be used instead of $USER,
to find the sshagent.env file. This is only useful for scripts that
are run with the "root" user. This is only useful with the -s option,
after agent is started as a regular user.
If root user and cgAgentOwner is not set, cgAgentOwner will be set
to SUDO_USER if SUDO_USER is defined.
If any Errors messages are output, that means the script has done
nothing. Correct the error and try again.
=head1 OPTIONS
=over 4
=item B<-h>
Output full help.
=item B<-s>
Set env. var. to use an already running agent, and list the keys.
If an agent is not running an Error message will be output.
=item B<-k>
Kill all agents owned by the current cgAgentOwner, i.e. $USER.
=item B<-x>
Increment gpDebug level. Default: 0
=item B<pKey ...>
Start a new ssh-agent if one is not running. Add one or more keys to
the agent.
If a pKey is not found, the script will try prepending the key with
"$HOME/.ssh/". If that is not found, the script will stop with an
error.
=back
=for comment =head1 RETURN VALUE
=head1 ERRORS
=head2 Env and option Errors
Error: sshagent is not 'sourced' [LINENO]
Error: No options were found [LINENO]"
Error: Unknown option: OPTARG [LINENO]
Error: Missing USER env. var. [LINENO]
Error: Missing HOME env. var. [LINENO]
Error: Missing program: PROG [LINENO]
=head2 Directory Errors
Error: HOME dir is not writable [LINENO]
This could be caused by cgAgentOwner being set to an unknown user.
Error: $cgEnvDir is not writable or is missing [LINENO]
This could be caused by cgAgentOwner being set to a user that has no
~/.ssh/ dir.
Error: with chown [LINENO]"
Error: with chmod [LINENO]"
The attempt to change the owner and permissions on the cgEnvDir
failed. The dir and files should ONLY have "user" read/write
permissions.
Error: Not found: KEY [LINENO]"
A KEY was not found, even after prepending with "$HOME/.ssh/"
=head2 -s Errors
Error: agent is not running [LINENO]"
Error: not found: $cgEnvFile [LINENO]"
=head2 Key add errors or warnings
Error: cgAgentOwner override cannot be used to add keys [LINENO]
Warning: KEY has no password!!! [LINENO]"
=head1 EXAMPLES
For these examples the private keys are located in ~/.ssh/. For
better security put your private keys on a USB drive which would only
be mounted when you add a key to your ssh-agent.
=over 4
=item *
The first time you login to your computer, run sshagent to authenticate
your ssh keys for the agent. Or put this in your profile. That way
You will only need to do this when the computer started or rebooted.
if ! pgrep ssh-agent &>/dev/null; then
. sshagent ~/.ssh/id.home ~/.ssh/id.work
fi
=item *
You ran sshagent to create an agent, but you forgot to "source" the
script so that the SSH_* env. are not set. Just repeat the command,
with a ". " at the front.
=item *
In a profile script add this line. That way when you start a new
terminal it will use any keys from the agent. This can also be
put in a script if it needs the keys saved on the agent.
. sshagent -s
=item *
In a script run by the 'root' user:
export $cgAgentOwner=george
. sshagent -s
If you used sudo to change to root user, cgAgentOwner will be set to
$SUDO_USER
=item *
Add another key to a running agent:
. sshagent ~/.ssh/id_foo_rsa
=item *
Kill all your agents. This would be a good practice if you don't want
your keys "active" on the computer.
. sshagent -k
=back
=head1 ENVIRONMENT
cgAgentOwner - user name. Default: $USER
cgEnvFile - sshagent env. file. Default: /home/$USER/.ssh/.sshagent.env
gpDebug - set to debug level (use before getops -x option)
SSH_AGENT_PID - set by ssh-agent
SSH_AUTH_SOCK - set by ssh-agent
HOME - set by OS. The usual default: /home/$USER
USER - set by OS
=head1 FILES
/home/$cgAgentOwner/.ssh/.sshagent.env
=head1 SEE ALSO
ssh-agent, ssh-add, sshagent-test, ssh-askpass, shunit2
=head1 NOTES
=head2 Security
=over 4
=item *
B<DO NOT USE PASSWORDLESS KEYS for ssh or gpg!> The ONLY exception
might be for a production server that might be rebooted when no one
would be around to authenticate the keys. If you do use passwordless
keys, then make sure the keys are ONLY used on production, the
permissions prevent copying the keys, and the keys are
"managed". I.e. the keys are not in any non-production user's account,
AND they are regularly rotated. Of course, with passwordless keys
there is no need for ssh-agent.
=item *
If the account of the root user or the owner of the ssh-agent is
"cracked", then all of the keys on the agent will be compromised.
=item *
Tip: if ssh keys are "shared", each user should change the password,
on their copy of the key, to one that only they know.
=back
=head2 Help Text Format
You can output this help text in different formats, if you have these
other pod programs. For example:
pod2html --title="sshagent" sshagent >sshagent.html
pod2markdown sshagent >sshagent.md
pod2man sshagent >sshagent.man
pod2pdf --margins=36 --outlines sshagent >sshagent.pdf
=head2 Test Driven Development
For TDD you can find the latest versions of sshagent and sshagent-test
at:
L<github|https://github.com/TurtleEngr/my-utility-scripts/tree/develop/bin>
=head1 CAVEATS
=over 4
=item *
There could be conflicts with an ssh-agent that started by an X11
session manager. This script is designed to work on headless services
or workstations, I<across sessions.> So if an ssh-agent process
already exists, before using this script, you'll need to track down
where it is being started, and prevent it from starting. For example,
on my Linux laptop I removed "use-ssh-agent" from file
/etc/X11/Xsession.options
=item *
The $cgAgentOwner option for using another user's agent will only work
for the root user, because ssh will only work if the ~/.ssh/ directory
is only readable by its owner. If non-root users need to share the
ssh-agent, then put put the .sshagent.env in a location that only
those users can read, using "group" permissions. See the cgEnvFile
variable.
=item *
The weird coding style of using functions, gErr, and returns, is done
to avoid using "exit," which would exit the active process (i.e. the
terminal or a calling script).
=back
=for comment =head1 DIAGNOSTICS
=head1 BUGS
When ssh-agent is active it will send each of the keys to a ssh
command, until one works. This could cause problems. For example what
if you have a rate limit of only 3 login attempts over a one minute
period. If the "correct" key is not one of the first 3 on the agent,
then ssh will always fail.
=head1 RESTRICTIONS
sshagent only works well with bash.
=head1 AUTHOR
TurtleEngr
=head1 HISTORY
$Revision: 1.81 $
=cut
EOF
} # fUsage
# --------------------
fSetBin()
{
local tBinLoc=home
local tBin=$PWD
case $tBinLoc in
current)
tBin=$PWD
;;
home) tBin=~/bin ;;
local) tBin=/usr/local/bin ;;
system) tBin=/usr/bin ;;
this)
tBin=${0%/*}
if [ "$tBin" = "." ]; then
tBin=$PWD
fi
cd $tBin &>/dev/null
tBin=$PWD
cd - &>/dev/null
;;
esac
echo "$tBin"
} # fSetBin
# --------------------
fConfig() {
if [ "$USER" = "root" ]; then
if [ -z "$cgAgentOwner" ] && [ -n "$SUDO_USER" ]; then
cgAgentOwner=$SUDO_USER
echo "Notice: cgAgentOwner=$SUDO_USER"
fi
# Hard coded. So this could fail if this is not the home dir
# for users. The could be fixed by looking up the user's home
# dir in /etc/passwd
cgHomeDir='/home'
else
# This fails if home is /root
cgHomeDir=${HOME%/*}
fi
cgAgentOwner=${cgAgentOwner:-$USER}
cgScriptName=sshagent
cgBin=$(fSetBin)
cgEnvDir=$cgHomeDir/$cgAgentOwner/.ssh
cgEnvFile=${cgEnvFile:-$cgEnvDir/.sshagent.env}
cgProgList='ssh-agent ssh-add pod2text pod2usage'
gErr=0
gpAction=""
gpParm=""
gpDebug=${gpDebug:-0}
gDebug=$gpDebug
} # fConfig
# --------------------
fGetOpts() {
if [ $gErr -ne 0 ]; then
return 1
fi
local tArg
OPTIND=1
if [ $gDebug -ne 0 ]; then echo 1NumArgs: "$#" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 1ArgsAs: "$*" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 1ArgsAt: "$@" OPTIND: $OPTIND; fi
while getopts :hksx tArg; do
case $tArg in
h)
fUsage long
return 1
;;
k) gpAction='kill' ;;
s) gpAction='script' ;;
x) ((++gDebug)) ;;
\?)
echo "Error: Unknown option: -$OPTARG [$LINENO]"
fUsage short
return 1
;;
:)
echo "Error: Value required for option: $tArg [$LINENO]"
fUsage short
return 1
;;
esac
done
if [ $gDebug -ne 0 ]; then echo 2NumArgs: "$#" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 2ArgsAs: "$*" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 2ArgsAt: "$@" OPTIND: $OPTIND; fi
((--OPTIND))
shift $OPTIND
if [ $gDebug -ne 0 ]; then echo 3NumArgs: "$#" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 3ArgsAs: "$*" OPTIND: $OPTIND; fi
if [ $gDebug -ne 0 ]; then echo 3ArgsAt: "$@" OPTIND: $OPTIND; fi
if [ $# -ne 0 ]; then
gpParm="$*"
gpAction='add'
fi
if [ $gDebug -ne 0 ]; then echo "gpParam=$gpParm"; fi
return 0
} # fGetOpts
# --------------------
fValidate() {
if [ $gErr -ne 0 ]; then
return 1
fi
local tParm=""
local tParmList=""
local tProg
if [ -z "$USER" ]; then
gErr=$LINENO
echo "Error: Missing USER env. var. [$gErr]"
fUsage short
return 1
fi
if [ -z "$HOME" ]; then
gErr=$LINENO
echo "Error: Missing HOME env. var. [$gErr]"
fUsage short
return 1
fi
if [ ! -w $HOME ]; then
gErr=$LINENO
echo "Error: $HOME dir is not writable [$gErr]"
fUsage short
return 1
fi
if [ ! -w $cgEnvDir ]; then
gErr=$LINENO
echo "Error: $cgEnvDir is not writable or is missing [$gErr]"
fUsage short
return 1
fi
if [ -z "$gpAction" ]; then
gErr=$LINENO
echo "Error: No options were found [$gErr]"
fUsage short
return 1
fi
for tProg in $cgProgList; do
if ! which $tProg &>/dev/null; then
gErr=$LINENO
echo "Error: Missing program: $tProg [$gErr]"
fUsage short
return 1
fi
done
if [[ "${BASH_SOURCE[0]}" = "$0" ]]; then
gErr=$LINENO
echo "Error: $cgScriptName is not 'sourced' [$gErr]"
fUsage short
return 1
fi
# Keep the permissions low
if ! chown -R $cgAgentOwner $cgEnvDir; then
gErr=$LINENO
echo "Error: with chown [$gErr]"
fUsage short
return 1
fi
find $cgEnvDir -type d -exec chmod u=rwx,go= {} \;
if [ $? -ne 0 ]; then
gErr=$LINENO
echo "Error: with chmod [$gErr]"
fUsage short
return 1
fi
find $cgEnvDir -type f -exec chmod u+rw,go= {} \;
if [ $? -ne 0 ]; then
gErr=$LINENO
echo "Error: with chmod [$gErr]"
fUsage short
return 1
fi
for tParm in $gpParm; do
if [ ! -r $tParm ]; then
if [ -r ~/.ssh/$tParm ]; then
tParm=~/.ssh/$tParm
else
gErr=$LINENO
echo "Error: Not found: $tParm [$gErr]"
return 1
fi
fi
ssh-keygen -y -f $tParm -P 'xxx' &>/dev/null 2>&1
if [ $? -eq 0 ]; then
echo "Warning: $tParm has no password!!! [$LINENO]"
fi
tParmList="$tParmList $tParm"
done
gpParm=$tParmList
return 0
} # fValidate
# --------------------
fSARunning() {
if [ $gErr -ne 0 ]; then
return 1
fi
# Check to see if an agent is running
gSARunning=0
if [ -x $cgEnvFile ]; then
. $cgEnvFile &>/dev/null
pgrep -u $cgAgentOwner ssh-agent | grep -q $SSH_AGENT_PID &>/dev/null
if [ $? -eq 0 ]; then
gSARunning=1
fi
fi
if [ $gSARunning -eq 0 ]; then
unset SSH_AUTH_SOCK SSH_AGENT_PID
killall -u $cgAgentOwner ssh-agent &>/dev/null
rm -f $cgEnvFile &>/dev/null
fi
return 0
} # fSARunning
# --------------------
fKill() {
if [ $gErr -ne 0 ]; then
return 1
fi
# Kill all agents owned by $cgAgentOwner
echo "Notice: Killing all of your ssh-agents"
killall -u $cgAgentOwner ssh-agent &>/dev/null
rm -f $cgEnvFile &>/dev/null
return 0
} # fKill
# --------------------
fScript() {
if [ $gErr -ne 0 ]; then
return 1
fi
if [ $gSARunning -eq 0 ]; then
gErr=$LINENO
echo "Error: agent is not running [$gErr]"
fUsage short
return 1
fi
if [ ! -x $cgEnvFile ]; then
gErr=$LINENO
echo "Error: not found: $cgEnvFile [$gErr]"
fUsage short
return 1
fi
. $cgEnvFile &>/dev/null
ssh-add -l
return 0
} # fScript
fAdd() {
if [ $gErr -ne 0 ]; then
return 1
fi
if [ "$cgAgentOwner" != "$USER" ]; then
gErr=$LINENO
echo "Error: cgAgentOwner override cannot be used to add keys [$gErr]"
fUsage short
return 1
fi
if [ $gSARunning -eq 0 ]; then
echo "Notice: Starting a new ssh-agent"
ssh-agent >$cgEnvFile
chmod u+rwx,o= $cgEnvFile
. $cgEnvFile &>/dev/null
gSARunning=1
fi
ssh-add $gpParm
ssh-add -l
return 0
} # fAdd
# --------------------
fAction() {
if [ $gErr -ne 0 ]; then
return 1
fi
case $gpAction in
kill)
fKill
;;
script)
fScript
;;
add)
fAdd
;;
esac
return 0
} # fAction
# ========================================
# Main
fConfig
if [ $gDebug -ne 0 ]; then echo NumArgs: "$#"; fi
# shellcheck disable=SC2048
fGetOpts $*
fValidate
fSARunning
fAction
set +u
unset cgBin cgEnvDir cgHomeDir cgProgList cgScriptName gDebug gErr
unset gSARunning gpAction gpParm
unset -f fAction fAdd fConfig fGetOpts fKill fSARunning fScript
unset -f fSetBin fUsage fValidate

NAME

sshagent - setup the ssh agent process

SYNOPSIS

. sshagent [-h] [-s] [-k] [-x] [pKey ...]

cgAgentOwner - user name. Default: $USER

For online help page, go to:
https://github.com/TurtleEngr/my-utility-scripts/blob/develop/doc/sshagent.md

DESCRIPTION

Using ssh-agent is a lot more secure than using a passwordless ssh key. If you use passwordless keys, you are following a very bad pattern, which could lead to large security issues.

This sshagent script is a wrapper for ssh-agent and ssh-add to make it easier to setup and use a ssh-agent. It only starts an agent process if one isn't already running, and it saves the PID env. var. values for use by scripts.

See the EXAMPLES section for how to use this script.

If an agent is found, then the env. var. are set, the pKeys are added, then listed. If an agent is not found, all other agents are killed, and a new agent is initialized, the pKeys are added, and the env. var. are set.

If -s is used (no pKeys) and an agent is running, then env. vars. are set to use the agent. If an agent is not running, then the agent related env. vars. are unset. -s is commonly used in cron job scripts.

If -k is used (no pKeys), then all ssh-agent will be killed.

If cgAgentOwner env. is set, then that will be used instead of $USER, to find the sshagent.env file. This is only useful for scripts that are run with the "root" user. This is only useful with the -s option, after agent is started as a regular user.

If root user and cgAgentOwner is not set, cgAgentOwner will be set to SUDO_USER if SUDO_USER is defined.

If any Errors messages are output, that means the script has done nothing. Correct the error and try again.

OPTIONS

  • -h

    Output full help.

  • -s

    Set env. var. to use an already running agent, and list the keys. If an agent is not running an Error message will be output.

  • -k

    Kill all agents owned by the current cgAgentOwner, i.e. $USER.

  • -x

    Increment gpDebug level. Default: 0

  • pKey ...

    Start a new ssh-agent if one is not running. Add one or more keys to the agent.

    If a pKey is not found, the script will try prepending the key with "$HOME/.ssh/". If that is not found, the script will stop with an error.

ERRORS

Env and option Errors

Error: sshagent is not 'sourced' [LINENO]
Error: No options were found [LINENO]"
Error: Unknown option: OPTARG [LINENO]
Error: Missing USER env. var. [LINENO]
Error: Missing HOME env. var. [LINENO]
Error: Missing program: PROG [LINENO]

Directory Errors

Error: HOME dir is not writable [LINENO]

This could be caused by cgAgentOwner being set to an unknown user.

Error: $cgEnvDir is not writable or is missing [LINENO]

This could be caused by cgAgentOwner being set to a user that has no ~/.ssh/ dir.

Error: with chown [LINENO]"
Error: with chmod [LINENO]"

The attempt to change the owner and permissions on the cgEnvDir failed. The dir and files should ONLY have "user" read/write permissions.

Error: Not found: KEY [LINENO]"

A KEY was not found, even after prepending with "$HOME/.ssh/"

-s Errors

Error: agent is not running [LINENO]"
Error: not found: $cgEnvFile [LINENO]"

Key add errors or warnings

Error: cgAgentOwner override cannot be used to add keys [LINENO]
Warning: KEY has no password!!! [LINENO]"

EXAMPLES

For these examples the private keys are located in ~/.ssh/. For better security put your private keys on a USB drive which would only be mounted when you add a key to your ssh-agent.

  • The first time you login to your computer, run sshagent to authenticate your ssh keys for the agent. Or put this in your profile. That way You will only need to do this when the computer started or rebooted.

      if ! pgrep ssh-agent &>/dev/null; then
          . sshagent ~/.ssh/id.home ~/.ssh/id.work
      fi
    
  • You ran sshagent to create an agent, but you forgot to "source" the script so that the SSH_* env. are not set. Just repeat the command, with a ". " at the front.

  • In a profile script add this line. That way when you start a new terminal it will use any keys from the agent. This can also be put in a script if it needs the keys saved on the agent.

      . sshagent -s
    
  • In a script run by the 'root' user:

      export $cgAgentOwner=george
      . sshagent -s
    

    If you used sudo to change to root user, cgAgentOwner will be set to $SUDO_USER

  • Add another key to a running agent:

      . sshagent ~/.ssh/id_foo_rsa
    
  • Kill all your agents. This would be a good practice if you don't want your keys "active" on the computer.

      . sshagent -k
    

ENVIRONMENT

cgAgentOwner - user name. Default: $USER
cgEnvFile - sshagent env. file. Default: /home/$USER/.ssh/.sshagent.env
gpDebug - set to debug level (use before getops -x option)
SSH_AGENT_PID - set by ssh-agent
SSH_AUTH_SOCK - set by ssh-agent
HOME - set by OS. The usual default: /home/$USER
USER - set by OS

FILES

/home/$cgAgentOwner/.ssh/.sshagent.env

SEE ALSO

ssh-agent, ssh-add, sshagent-test, ssh-askpass, shunit2

NOTES

Security

  • DO NOT USE PASSWORDLESS KEYS for ssh or gpg! The ONLY exception might be for a production server that might be rebooted when no one would be around to authenticate the keys. If you do use passwordless keys, then make sure the keys are ONLY used on production, the permissions prevent copying the keys, and the keys are "managed". I.e. the keys are not in any non-production user's account, AND they are regularly rotated. Of course, with passwordless keys there is no need for ssh-agent.
  • If the account of the root user or the owner of the ssh-agent is "cracked", then all of the keys on the agent will be compromised.
  • Tip: if ssh keys are "shared", each user should change the password, on their copy of the key, to one that only they know.

Help Text Format

You can output this help text in different formats, if you have these other pod programs. For example:

pod2html --title="sshagent" sshagent >sshagent.html
pod2markdown sshagent >sshagent.md
pod2man sshagent >sshagent.man
pod2pdf --margins=36 --outlines sshagent >sshagent.pdf

Test Driven Development

For TDD you can find the latest versions of sshagent and sshagent-test at: github

CAVEATS

  • There could be conflicts with an ssh-agent that started by an X11 session manager. This script is designed to work on headless services or workstations, across sessions. So if an ssh-agent process already exists, before using this script, you'll need to track down where it is being started, and prevent it from starting. For example, on my Linux laptop I removed "use-ssh-agent" from file /etc/X11/Xsession.options
  • The $cgAgentOwner option for using another user's agent will only work for the root user, because ssh will only work if the ~/.ssh/ directory is only readable by its owner. If non-root users need to share the ssh-agent, then put put the .sshagent.env in a location that only those users can read, using "group" permissions. See the cgEnvFile variable.
  • The weird coding style of using functions, gErr, and returns, is done to avoid using "exit," which would exit the active process (i.e. the terminal or a calling script).

BUGS

When ssh-agent is active it will send each of the keys to a ssh command, until one works. This could cause problems. For example what if you have a rate limit of only 3 login attempts over a one minute period. If the "correct" key is not one of the first 3 on the agent, then ssh will always fail.

RESTRICTIONS

sshagent only works well with bash.

AUTHOR

TurtleEngr

HISTORY

$Revision: 1.17 $

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment