|
#!/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 |