Is it possible using "su -c" with multiple commands but in one session? - bash

I am trying run the following two command in one command.
eval "$(ssh-agent)"
ssh add ~/.ssh/id_rsa
I tried with many possible solutions:
su username -c "{ eval $(ssh-agent -s) }; ssh-add ~/.ssh/id_rsa"
su username -c "eval $(ssh-agent -s)" ; ssh-add ~/.ssh/id_rsa
su username -c "eval $(ssh-agent -s)" && ssh-add ~/.ssh/id_rsa
su username -c "eval $(ssh-agent -s)" && "ssh-add ~/.ssh/id_rsa"
It seems like the first command run successfully but the second either response with Permission denied message (means it run with the current user) or cannot connect to the authentication agent (means it probably created a new session for the second command).
Error messages:
Could not open a connection to your authentication agent.
Error connecting to agent: Permission denied
If I run them separately in order, it works:///
The purpose is to create a bash script with these commands with variables, something like this:
folder=/home/q12
username=q12
su $username -c "{ eval $(ssh-agent -s) }; ssh-add $folder/.ssh/id_rsa"
Because of the variables I can not quote the whole command because it will be sensitive to ";" and "&&".
Can anyone help me with this?
Thank you!

You need single quotes. Otherwise the command substitution is evaluated in the current context.
su username -c 'eval "$(ssh-agent -s)"; ssh-add ~/.ssh/id_rsa'
Edit:
To get conditional execution of ssh-add, you can do:
su username -c 'script=$(ssh-agent -s) || exit 1; eval "$script"; ssh-add ~/.ssh/id_rsa'
# or
su username -c 'set -e; script=$(ssh-agent -s); eval "$script"; ssh-add ~/.ssh/id_rsa'
The argument in su -c STRING is a command string, similar to bash -c STRING. We can also nest double quotes inside single quotes.

The first of your tries is closest, but inside double-quotes things like $(ssh-agent -s) get evaluated by your shell before they're passed to su as part of an argument. Net result: the ssh agent is running under the current user, so the other user can't use it.
You need to delay evaluation until the other-user shell. Using single-quotes instead of double would do this, but in the actual command you have $folder, which you clearly want evaluated by your shell (it won't be defined in the other-user shell), so you don't want to delay evaluation of that. The simplest way to do this is to escape the $ that you want to delay evaluation of (your shell will remove the escape, so the other-user shell will see & evaluate it):
su "$username" -c "eval \$(ssh-agent -s); ssh-add $folder/.ssh/id_rsa"
# ^ Note the escape
(BTW, I also added double-quotes around the username, as that's generally-good scripting hygiene. Quoting $folder is more complicated, and shouldn't be necessary as long as it doesn't contain any weird characters, so I skipped it. Also, the { } weren't necessary, and if they were used there needed to be a ; before the }... so I just removed them.)
Another option is to single-quote part of the command and double-quote another part (mixed quoting looks weird, but is perfectly legal in shell syntax):
su "$username" -c 'eval $(ssh-agent -s);'" ssh-add $folder/.ssh/id_rsa"
# ^ single-quoted part ^^ double-quoted part ^
The reason the rest of your attempts didn't work is that the delimiter between the commands (; or &&) wasn't in the quoted string, and hence was treated as a delimiter by your shell, so the second command was run under your user ID rather than as part of the su command.

Related

Ansible Using shell module, how do I ssh and remain in the same directory? [duplicate]

I already have an ssh agent set up, and I can run commands on an external server in Bash script doing stuff like:
ssh blah_server "ls; pwd;"
Now, what I'd really like to do is run a lot of long commands on an external server. Enclosing all of these in between quotation marks would be quite ugly, and I'd really rather avoid ssh'ing multiple times just to avoid this.
So, is there a way I can do this in one go enclosed in parentheses or something? I'm looking for something along the lines of:
ssh blah_server (
ls some_folder;
./someaction.sh;
pwd;
)
Basically, I'll be happy with any solution as long as it's clean.
Edit
To clarify, I'm talking about this being part of a larger bash script. Other people might need to deal with the script down the line, so I'd like to keep it clean. I don't want to have a bash script with one line that looks like:
ssh blah_server "ls some_folder; ./someaction.sh 'some params'; pwd; ./some_other_action 'other params';"
because it is extremely ugly and difficult to read.
How about a Bash Here Document:
ssh otherhost << EOF
ls some_folder;
./someaction.sh 'some params'
pwd
./some_other_action 'other params'
EOF
To avoid the problems mentioned by #Globalz in the comments, you may be able to (depending what you're doing on the remote site) get away with replacing the first line with
ssh otherhost /bin/bash << EOF
Note that you can do variable substitution in the Here document, but you may have to deal with quoting issues. For instance, if you quote the "limit string" (ie. EOF in the above), then you can't do variable substitutions. But without quoting the limit string, variables are substituted. For example, if you have defined $NAME above in your shell script, you could do
ssh otherhost /bin/bash << EOF
touch "/tmp/${NAME}"
EOF
and it would create a file on the destination otherhost with the name of whatever you'd assigned to $NAME. Other rules about shell script quoting also apply, but are too complicated to go into here.
Edit your script locally, then pipe it into ssh, e.g.
cat commands-to-execute-remotely.sh | ssh blah_server
where commands-to-execute-remotely.sh looks like your list above:
ls some_folder
./someaction.sh
pwd;
To match your sample code, you can wrap your commands inside single or double qoutes. For example
ssh blah_server "
ls
pwd
"
I see two ways:
First you make a control socket like this:
ssh -oControlMaster=yes -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip>
and run your commands
ssh -oControlMaster=no -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip> -t <yourcommand>
This way you can write an ssh command without actually reconnecting to the server.
The second would be to dynamically generate the script, scping it and running.
This can also be done as follows.
Put your commands in a script, let's name it commands-inc.sh
#!/bin/bash
ls some_folder
./someaction.sh
pwd
Save the file
Now run it on the remote server.
ssh user#remote 'bash -s' < /path/to/commands-inc.sh
Never failed for me.
Put all the commands on to a script and it can be run like
ssh <remote-user>#<remote-host> "bash -s" <./remote-commands.sh
Not sure if the cleanest for long commands but certainly the easiest:
ssh user#host "cmd1; cmd2; cmd3"
This works well for creating scripts, as you do not have to include other files:
#!/bin/bash
ssh <my_user>#<my_host> "bash -s" << EOF
# here you just type all your commmands, as you can see, i.e.
touch /tmp/test1;
touch /tmp/test2;
touch /tmp/test3;
EOF
# you can use '$(which bash) -s' instead of my "bash -s" as well
# but bash is usually being found in a standard location
# so for easier memorizing it i leave that out
# since i dont fat-finger my $PATH that bad so it cant even find /bin/bash ..
SSH and Run Multiple Commands in Bash.
Separate commands with semicolons within a string, passed to echo, all piped into the ssh command. For example:
echo "df -k;uname -a" | ssh 192.168.79.134
Pseudo-terminal will not be allocated because stdin is not a terminal.
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda2 18274628 2546476 14799848 15% /
tmpfs 183620 72 183548 1% /dev/shm
/dev/sda1 297485 39074 243051 14% /boot
Linux newserv 2.6.32-431.el6.x86_64 #1 SMP Sun Nov 10 22:19:54 EST 2013 x86_64 x86_64 x86_64 GNU/Linux
The posted answers using multiline strings and multiple bash scripts did not work for me.
Long multiline strings are hard to maintain.
Separate bash scripts do not maintain local variables.
Here is a functional way to ssh and run multiple commands while keeping local context.
LOCAL_VARIABLE=test
run_remote() {
echo "$LOCAL_VARIABLE"
ls some_folder;
./someaction.sh 'some params'
./some_other_action 'other params'
}
ssh otherhost "$(set); run_remote"
For anyone stumbling over here like me - I had success with escaping the semicolon and the newline:
First step: the semicolon. This way, we do not break the ssh command:
ssh <host> echo test\;ls
^ backslash!
Listed the remote hosts /home directory (logged in as root), whereas
ssh <host> echo test;ls
^ NO backslash
listed the current working directory.
Next step: breaking up the line:
v another backslash!
ssh <host> echo test\;\
ls
This again listed the remote working directory - improved formatting:
ssh <host>\
echo test\;\
ls
If really nicer than here document or quotes around broken lines - well, not me to decide...
(Using bash, Ubuntu 14.04 LTS.)
The easiest way to configure your system to use single ssh sessions by default with multiplexing.
This can be done by creating a folder for the sockets:
mkdir ~/.ssh/controlmasters
And then adding the following to your .ssh configuration:
Host *
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r#%h:%p.socket
ControlMaster auto
ControlPersist 10m
Now, you do not need to modify any of your code. This allows multiple calls to ssh and scp without creating multiple sessions, which is useful when there needs to be more interaction between your local and remote machines.
Thanks to #terminus's answer, http://www.cyberciti.biz/faq/linux-unix-osx-bsd-ssh-multiplexing-to-speed-up-ssh-connections/ and https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing.
What is the cleanest way to ssh and run multiple commands in Bash?
I recommend using this escaping function. The function takes one argument - a function to escape. Then sshqfunc outputs declare -f of the function and then outputs a string that will call the function with "$#" arguments properly quoted. Then the whole is "%q" quoted and bash -c is added. In case the remote does not have bash, you could change bash to sh.
sshqfunc() { echo "bash -c $(printf "%q" "$(declare -f "$#"); $1 \"\$#\"")"; };
Then define a function with the work you want to do on the remote. The function is defined normally, so it will be properly "clean". You can test such function locally. After defining, properly escaped function is passed to the remote.
work() {
ls
pwd
echo "Some other command"
}
ssh host#something "$(sshqfunc work)"
Passing You can also pass arguments, and they will be passed to your function as positional arguments. The right next argument after the function will be assigned to $0 - usually a placeholder like -- or _ is used to separate arguments from call.
work() {
file=$1
num=$2
ls "$file"
echo "num is $num"
}
ssh host#something "$(sshqfunc work)" -- /this/file 5
But note that arguments should also be properly quoted if there are any magic characters:
ssh host#something "$(sshqfunc work)" -- "$(printf "%q" "$var1" "$var2")"
For simple commands you can use:
ssh <ssh_args> command1 '&&' command2
or
ssh <ssh_args> command1 \&\& command2

Navigating in ssh server through a local bash script [duplicate]

I already have an ssh agent set up, and I can run commands on an external server in Bash script doing stuff like:
ssh blah_server "ls; pwd;"
Now, what I'd really like to do is run a lot of long commands on an external server. Enclosing all of these in between quotation marks would be quite ugly, and I'd really rather avoid ssh'ing multiple times just to avoid this.
So, is there a way I can do this in one go enclosed in parentheses or something? I'm looking for something along the lines of:
ssh blah_server (
ls some_folder;
./someaction.sh;
pwd;
)
Basically, I'll be happy with any solution as long as it's clean.
Edit
To clarify, I'm talking about this being part of a larger bash script. Other people might need to deal with the script down the line, so I'd like to keep it clean. I don't want to have a bash script with one line that looks like:
ssh blah_server "ls some_folder; ./someaction.sh 'some params'; pwd; ./some_other_action 'other params';"
because it is extremely ugly and difficult to read.
How about a Bash Here Document:
ssh otherhost << EOF
ls some_folder;
./someaction.sh 'some params'
pwd
./some_other_action 'other params'
EOF
To avoid the problems mentioned by #Globalz in the comments, you may be able to (depending what you're doing on the remote site) get away with replacing the first line with
ssh otherhost /bin/bash << EOF
Note that you can do variable substitution in the Here document, but you may have to deal with quoting issues. For instance, if you quote the "limit string" (ie. EOF in the above), then you can't do variable substitutions. But without quoting the limit string, variables are substituted. For example, if you have defined $NAME above in your shell script, you could do
ssh otherhost /bin/bash << EOF
touch "/tmp/${NAME}"
EOF
and it would create a file on the destination otherhost with the name of whatever you'd assigned to $NAME. Other rules about shell script quoting also apply, but are too complicated to go into here.
Edit your script locally, then pipe it into ssh, e.g.
cat commands-to-execute-remotely.sh | ssh blah_server
where commands-to-execute-remotely.sh looks like your list above:
ls some_folder
./someaction.sh
pwd;
To match your sample code, you can wrap your commands inside single or double qoutes. For example
ssh blah_server "
ls
pwd
"
I see two ways:
First you make a control socket like this:
ssh -oControlMaster=yes -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip>
and run your commands
ssh -oControlMaster=no -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip> -t <yourcommand>
This way you can write an ssh command without actually reconnecting to the server.
The second would be to dynamically generate the script, scping it and running.
This can also be done as follows.
Put your commands in a script, let's name it commands-inc.sh
#!/bin/bash
ls some_folder
./someaction.sh
pwd
Save the file
Now run it on the remote server.
ssh user#remote 'bash -s' < /path/to/commands-inc.sh
Never failed for me.
Put all the commands on to a script and it can be run like
ssh <remote-user>#<remote-host> "bash -s" <./remote-commands.sh
Not sure if the cleanest for long commands but certainly the easiest:
ssh user#host "cmd1; cmd2; cmd3"
This works well for creating scripts, as you do not have to include other files:
#!/bin/bash
ssh <my_user>#<my_host> "bash -s" << EOF
# here you just type all your commmands, as you can see, i.e.
touch /tmp/test1;
touch /tmp/test2;
touch /tmp/test3;
EOF
# you can use '$(which bash) -s' instead of my "bash -s" as well
# but bash is usually being found in a standard location
# so for easier memorizing it i leave that out
# since i dont fat-finger my $PATH that bad so it cant even find /bin/bash ..
SSH and Run Multiple Commands in Bash.
Separate commands with semicolons within a string, passed to echo, all piped into the ssh command. For example:
echo "df -k;uname -a" | ssh 192.168.79.134
Pseudo-terminal will not be allocated because stdin is not a terminal.
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda2 18274628 2546476 14799848 15% /
tmpfs 183620 72 183548 1% /dev/shm
/dev/sda1 297485 39074 243051 14% /boot
Linux newserv 2.6.32-431.el6.x86_64 #1 SMP Sun Nov 10 22:19:54 EST 2013 x86_64 x86_64 x86_64 GNU/Linux
The posted answers using multiline strings and multiple bash scripts did not work for me.
Long multiline strings are hard to maintain.
Separate bash scripts do not maintain local variables.
Here is a functional way to ssh and run multiple commands while keeping local context.
LOCAL_VARIABLE=test
run_remote() {
echo "$LOCAL_VARIABLE"
ls some_folder;
./someaction.sh 'some params'
./some_other_action 'other params'
}
ssh otherhost "$(set); run_remote"
For anyone stumbling over here like me - I had success with escaping the semicolon and the newline:
First step: the semicolon. This way, we do not break the ssh command:
ssh <host> echo test\;ls
^ backslash!
Listed the remote hosts /home directory (logged in as root), whereas
ssh <host> echo test;ls
^ NO backslash
listed the current working directory.
Next step: breaking up the line:
v another backslash!
ssh <host> echo test\;\
ls
This again listed the remote working directory - improved formatting:
ssh <host>\
echo test\;\
ls
If really nicer than here document or quotes around broken lines - well, not me to decide...
(Using bash, Ubuntu 14.04 LTS.)
The easiest way to configure your system to use single ssh sessions by default with multiplexing.
This can be done by creating a folder for the sockets:
mkdir ~/.ssh/controlmasters
And then adding the following to your .ssh configuration:
Host *
ControlMaster auto
ControlPath ~/.ssh/controlmasters/%r#%h:%p.socket
ControlMaster auto
ControlPersist 10m
Now, you do not need to modify any of your code. This allows multiple calls to ssh and scp without creating multiple sessions, which is useful when there needs to be more interaction between your local and remote machines.
Thanks to #terminus's answer, http://www.cyberciti.biz/faq/linux-unix-osx-bsd-ssh-multiplexing-to-speed-up-ssh-connections/ and https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing.
What is the cleanest way to ssh and run multiple commands in Bash?
I recommend using this escaping function. The function takes one argument - a function to escape. Then sshqfunc outputs declare -f of the function and then outputs a string that will call the function with "$#" arguments properly quoted. Then the whole is "%q" quoted and bash -c is added. In case the remote does not have bash, you could change bash to sh.
sshqfunc() { echo "bash -c $(printf "%q" "$(declare -f "$#"); $1 \"\$#\"")"; };
Then define a function with the work you want to do on the remote. The function is defined normally, so it will be properly "clean". You can test such function locally. After defining, properly escaped function is passed to the remote.
work() {
ls
pwd
echo "Some other command"
}
ssh host#something "$(sshqfunc work)"
Passing You can also pass arguments, and they will be passed to your function as positional arguments. The right next argument after the function will be assigned to $0 - usually a placeholder like -- or _ is used to separate arguments from call.
work() {
file=$1
num=$2
ls "$file"
echo "num is $num"
}
ssh host#something "$(sshqfunc work)" -- /this/file 5
But note that arguments should also be properly quoted if there are any magic characters:
ssh host#something "$(sshqfunc work)" -- "$(printf "%q" "$var1" "$var2")"
For simple commands you can use:
ssh <ssh_args> command1 '&&' command2
or
ssh <ssh_args> command1 \&\& command2

Safely pass arguments to a command executed via su

With sudo, it is possible to execute a command as an other user and really safely pass arguments to that command.
Example nasty argument:
nastyArg='"double quoted" `whoami` $(whoami) '"'simple quoted "'$(whoami)'"'"
Expected output, run via a termninal as congelli501:
% echo "$nastyArg"
"double quoted" `whoami` $(whoami) 'simple quoted $(whoami)'
Execute as congelli501, via sudo:
# sudo -u congelli501 -- echo "$nastyArg"
"double quoted" `whoami` $(whoami) 'simple quoted $(whoami)'
Execute as congelli501, via su (usual escape method):
# su congelli501 -c "echo '$nastyArg'"
"double quoted" `whoami` $(whoami) simple quoted congelli501
As you can see, the argument is not safely passed as it is re-interpreted by a shell.
Is there a way to launch a command via su and pass its arguments directly, as you can do with sudo ?
Passing the command as the shell script argument in su seems to work:
# su congelli501 -s "$(which echo)" -- "$nastyArg" "'another arg'"
"double quoted" `whoami` $(whoami) 'simple quoted $(whoami)' 'another arg'
Example usage:
# Safely execute a command as an other user, via su
#
# $1 -> username
# $2 -> program to run
# $3 .. n -> arguments
function execAs() {
user="$1"; shift
cmd="$1"; shift
su "$user" -s "$(which -- "$cmd")" -- "$#"
}
execAs congelli501 echo "$nastyArg" "'another arg'"
The fundamental problem here is that you are trying to nest quotes. You would hope su -c "stuff "with" double "quotes" to be parsed as |su|, |-c|, |stuff "with" double "quotes"| but it actually gets parsed as |su|, |-c|, |stuff |, |with|, | double|, |quotes| where however the last four tokens are pasted together as one string after evaluation (notice the spaces where the "inner" quotes terminated the ostensible "outer" quotes instead of wrapping inside them).
Within double quotes, `whoami` gets expanded by the shell before anything gets passed to su.
What you can do instead is either (a) add yet more quoting around the values in $nastyArg (pretty much doomed) or (b) define it in the context of the shell executed by su. A distinct third option is to (c) pass in the value in a way which disarms it, such as on standard input.
printf '%s\n' "$nastyArg" | su -c 'cat' congelli501
This may seem overtly simplistic, but is really the way to go when you do not have complete trust over what will possibly be evaluated by root somehow.
This is one of the reasons sudo is preferred over su.

ssh + here document + interactive mode

Can I run a here document script over ssh on remote machine with interactive mode?
Code example is:
ssh -t xijing#ggzshop.com 'bash -s' <<EOF
sudo ls
......Other big scripts......
EOF
double -t won't work properly as well.
-----------------------------One possible solution:-------------------
After a lot of tries, I come up with following answers:
Script=`cat <<'EOF'
sudo ls
.....Big scripts.....
EOF`
ssh -t user#host ${Script}
which will allow user to type password in.
Solution of Xijing appears to work ok for me. However, I did a couple of cosmetic changes. First, for readability I used "dollar-parentheses" instead of backticks. For another thing I don't offer any explanation: Semicolons were needed to separate multiple commands in Script snippet even though commands are written on separate lines. My test:
Script=$( cat <<'HERE'
hostname;
cat /etc/issue;
sudo id
HERE
)
ssh -t user#host ${Script}
Sudo password will be asked in a normal manner, no need to omit that.
No, I don't think you can run interactive scripts like that.
To achieve what you want, you could create dedicated users for your common admin tasks that can run admin commands with sudo without password. Next, setup ssh key authentication to login as the dedicated users and perform the necessary tasks.
It is not necessary to use semicolons to separate multiple commands in Script if there are quotes around it.
Script="$( cat <<'HERE'
hostname;
cat /etc/issue;
sudo id
HERE
)"
- ssh -t user#host ${Script}
+ ssh -t user#host "${Script}"
# alternative (not recommended)
# set IFS variable to null string to avoid deletion of newlines \n in unquoted variable expansion
export IFS=''
ssh -t user#host ${Script}

How to define environment variable in input command of su

This command has an empty output.
su user -c "ABC=abc;echo $ABC"
Any idea, how can I define a variable in the input command?
Change your quotes to single quotes. The double quotes allow the variable to be substituted in the current environment where it's not set yet. To see the difference, try your version with $USER and compare it to this one:
su user -c 'ABC=abc; echo $ABC; echo $USER'
If using a bourne shell variant:
ABC=abc su user -c 'echo $ABC'
If not, use env.
env ABC=abc su user -c 'echo $ABC'

Resources