Bash Scripting: Remote Execution, SSH Keys, and Multi-Server Web Deployment

By Mashrur Hossain Khan · DevOps Learning Notes

Bash SSH Linux Automation DevOps
← Back to Blogs

Writing scripts on a single machine to controlling multiple servers from one central machine. That was the point where Bash started feeling less like a beginner topic and more like real DevOps work.

The setup had one main machine called scriptbox and three target machines: web01, web02, and web03. The first two were CentOS-based and the third one was Ubuntu. The goal was to access them from scriptbox, solve the password problem, and finally automate web server deployment across all of them.

Starting from scriptbox to web01, web02, and web03

The first important idea was that scriptbox acted as the control machine. Instead of manually logging into every server one by one, I could execute commands remotely from scriptbox using SSH. Hostname mapping was added so the machines could be accessed by names like web01 and web03 rather than raw IP addresses

ssh vagrant@web01

That worked, but there was a problem: it kept asking for a password. For quick testing that is fine, but for automation it becomes a blocker. If a script pauses every time and waits for a password, it is no longer true automation.

The password issue

When the remote command was executed from scriptbox, the command itself worked, but authentication still needed manual input. The same pattern appeared when trying to move files with scp or run commands with ssh remotely. This exposed a basic truth: remote execution only becomes scalable when authentication is non-interactive

ssh devops@web01 uptime

Another interesting issue appeared with web03, which was Ubuntu-based. It was not allowing password login in the same way as the other machines. The reason turned out to be the SSH server configuration. In /etc/ssh/sshd_config, password authentication was disabled, so it needed to be enabled before password-based login could even be tested from scriptbox

When SSH says something like public key only or permission denied, the issue may not be the username or password first. It may simply be SSH server configuration.

Creating a proper automation user

Instead of continuing with the default Vagrant user, a separate user called devops was created on the remote machines. That user was then added to sudoers with NOPASSWD so system-level actions such as installing packages and starting services could be executed without interactive prompts

This felt like a more realistic setup. In real environments, automation usually should not depend on a default user account. It is better to create a dedicated account with just enough permissions to perform the required actions.

From password-based SSH to key-based SSH

The real improvement came with SSH key-based authentication. Instead of entering a password again and again, a key pair was generated with ssh-keygen. Then the public key was copied to each remote machine with ssh-copy-id.

ssh-keygen ssh-copy-id devops@web01 ssh-copy-id devops@web02 ssh-copy-id devops@web03

After that, remote commands could run without asking for a password. This was the turning point. Once SSH stopped being interactive, Bash loops and remote execution became truly useful.

That also helped me understand the practical difference between a public key and a private key. The public key is placed on the remote server, and the private key stays on the control machine. If they match, authentication succeeds. It is cleaner, faster, and safer than repeatedly typing passwords.

Using a loop to target multiple servers

The next big step was creating a host file that listed all target machines. Then a loop could go through each host one by one and run commands remotely. This made the script feel like a lightweight orchestration system

for host in $(cat remote_hosts) do ssh devops@$host hostname done

A single loop can target 3 machines, 30 machines, or even hundreds, depending on what is listed in the inventory file.

The CentOS vs Ubuntu difference

When trying package installation remotely, another useful problem appeared. The same command did not work everywhere. CentOS-based machines accepted yum, while Ubuntu required apt. So the script had to detect the operating system and decide which commands, package names, and service names to use

That taught me an important DevOps lesson: automation is not just about sending commands remotely. It is also about making scripts smart enough to handle differences between environments.

Pushing and executing the deployment script

The final step was putting it all together. Instead of just executing one-liner commands, the deployment script itself was copied to each machine using scp, executed remotely with ssh, and then cleaned up from the remote machine afterward

scp multios_websetup.sh devops@$host:/tmp/ ssh devops@$host "sudo /tmp/multios_websetup.sh" ssh devops@$host "rm -f /tmp/multios_websetup.sh"

That script handled the web server setup differently depending on the OS. On CentOS, it used the right package and service names for Apache. On Ubuntu, it switched to the Ubuntu equivalents. The result was that web01, web02, and web03 all got web setup completed from a single control point

Why I think this matters

What I liked most about this section is that it shows how tools like Ansible do not come out of nowhere. Behind the nicer syntax and abstraction, the core ideas are still here: inventory, remote execution, authentication, OS-aware logic, file transfer, and command orchestration.

Learning this gave me more confidence. It made me feel that if I can understand and build this flow manually, then I will be in a much better position to learn more advanced DevOps tools the right way.

For me, this was the moment Bash stopped being “just scripting” and started feeling like the base layer of automation.
Back to Blogs