Like the error says, there is an unterminated quoted string. The unpaired single-quote (') is between $server and echo. If you only fix it like this
ssh $server 'echo "new_password\nnew_password" | passwd username'
you will encounter more problems.
Problems I noticed (starting from general ones, not necessarily most severe):
You used a shebang (#!/bin/bash) but then called the script by sh changepasswd.sh. In this case the shebang is totally ignored and the interpreter is sh. Usually it's better to rely on the shebang and run a script as ./the_script; this way the caller does not need to wonder which interpreter to use, the right interpreter is in the shebang.
You made the script executable (with chmod) but sh changepasswd.sh does not need it to be executable. On the other hand calling with ./changepasswd.sh would need it to be executable.
You used sh as the interpreter, but read -p is not portable. Your sh may support it, other implementations may or may not. Using bash will allow you not only to use -p reliably, but also -s (for not echoing the password). One way or another you should use -r and IFS='' (empty string) especially when reading a password. In Bash see help read to learn what these options do.
If you decide to use the shebang, it's better to get rid of the .sh suffix. If you ever port the script to another interpreter (or even compile a binary executable), the name changepasswd will still fit, while changepasswd.sh would need to be changed or it would be confusing.
chmod 777 makes the script writable for anyone. If the script is in a directory where other users have certain rights, they will be able to change the script. E.g. they can inject a line that will send/store $new_password for them. 744 may be the right mode.
It's advisable to ask for a new password twice and abort if the two strings don't match.
for server in 'cat serverlist.txt' will assign cat serverlist.txt (string) to the variable. You meant `cat serverlist.txt` or $(cat serverlist.txt). The latter is better.
In general you should double-quote $( ) to prevent word splitting and filename generation; but in this case you do want word splitting (yet you don't want filename generation). Looping over the lines of a file (serverlist.txt in your case) can be done with read (but there are pitfalls). However if your file contains IP addresses or simple hostnames only then for server in $(cat serverlist.txt) seems fine. Just remember it's not a good general way.
Still you should double-quote variables like $server.
"new_password\nnew_password" is just a string. You want to expand variables: "$new_password\n$new_password". In general, when concatenating, you may want to use ${new_password}.
The "fixed" snippet
ssh $server 'echo "$new_password\n$new_password" | passwd $username'
will not expand $new_password nor $username locally, because these are single-quoted. The remote shell will expand them (most likely to empty strings). To expand locally, the variables should be double-quoted:
ssh "$server" "echo '$new_password\n$new_password' | passwd '$username'"
But this is still error prone, keep reading.
- The SSH server will get the entire command string as a string. A remote shell will interpret it. This means a single-quote character in (locally expanded)
$new_password (or $username) will break the code. This way you can even inject code. It's not about possible code injection (I understand you can execute any code anyway); it's about setting an unexpected password. There are few ways to deal with this:
- In general you can tell
ssh to pass some variables to the remote side, if the server allows it. Then a remote shell could expand them safely. This depends on the server configuration; plus on the client side it's hard (and not elegant) to define the variables for ssh dynamically. You shouldn't do this in this case, but if you want to learn more then investigate PermitUserEnvironment in man 5 sshd_config.
- Bash allows you to expand a variable in a special way, so after the result is parsed in a shell, it becomes the original string again. This is done with
${variable@Q}. The expanded string is properly quoted and/or escaped.
- Since you're reading from a pipe on the remote side, you can pipe to
ssh locally and avoid passing (locally expanded) $new_password as a string that is about to be parsed.
Whatever you choose, remember you need to properly quote for the local shell and separately for the remote shell (in case of ${variable@Q} the local Q operator handles this part).
- Do not use
echo to pass "random" strings, prefer printf. If you decide to pipe a variable to ssh in Bash, then a here string may be a good way. In your case you need to pass $new_password twice (plus newlines), so I would go with printf anyway.
The script may look like this:
#!/bin/bash
IFS= read -rp "Enter username: " username
IFS= read -rsp "Enter new password: " new_password
echo # just to get a newline
IFS= read -rsp "Repeat new password: " new_password_2
echo
[ "$new_password" = "$new_password_2" ] || { echo "Mismatch. Aborting."; exit 1; }
for server in $(cat serverlist.txt)
do echo "Server IP is: $server"
printf '%s\n' "$new_password" "$new_password" | ssh "$server" "passwd -- ${username@Q}"
done
You need to make it executable (chmod u+x changepasswd) and run it with ./changepasswd.
Notes:
- The script assumes no
passwd will ask for the current password. This assumption originates in the script you published.
- The script assumes all remote
passwd support double dash.
ssh "$server" "passwd -- ${username@Q}" could be ssh "$server" "passwd '$username'", if you're sure expanded $username is safe. Basic validation can be done locally.