Remote commands over SSH from WordPress (Using phpseclib)

I commonly want WordPress to run code, like a bash script, directly on the command line. In PHP there is a set of functions which is commonly disabled for security purposes. That includes exec which is PHP’s command to do just that. Rather then running the bash script directly on the web server, an alternative is to use phpseclib to run the bash command on a remote server.

Phpseclib works as long as you have PHP

This is huge, as you don’t need to do any extra configuration to the web server in order to get this working. You just need a remote server which is SSH enabled. With phpseclib you can do things like ssh user@remoteserver "Script/Run/remote.sh" right from PHP. For my example I’m using phpseclib v2 release via Composer.  Here is a basic example of doing SSH from PHP.

<?php

$post_id = intval( $_POST['post_id'] );
$install = get_field('install',$post_id);
$command = "Scripts/Run/activate.sh $install";

// Runs command on remote on production
require_once(ABSPATH . '/vendor/autoload.php');

$key = new \phpseclib\Crypt\RSA();

// Loads private SSH key from the file: privatekey
$key->loadKey(file_get_contents(ABSPATH . 'privatekey'));

// Connects to remote server
$ssh = new \phpseclib\Net\SSH2(ANCHOR_REMOTE_ADDRESS, ANCHOR_REMOTE_PORT);

if (!$ssh->login(ANCHOR_REMOTE_USER, $key)) {
  exit('Login Failed');
}

echo $ssh->exec( $command );

Long commands need put in the background

Phpseclib works great for short commands. In fact if you attempt a long running command like a backup script it will exit prematurely as the SSH session is closed when the PHP web request times out. To work around this limit you can use some clever bash scripting to put the long running task in the background. I use the following trick to capture the first 5 secs of the script output and return back to WordPress.

<?php

$cmd = $_POST['command'];

if ($cmd == "backup") {
    date_default_timezone_set('America/New_York');
    $t=time();
    $timestamp = date("Y-m-d-hms",$t);
    $command = "Scripts/Run/backup.sh $install > ~/Tmp/$timestamp-backup_$install.txt 2>&1 & sleep 5; head ~/Tmp/$timestamp-backup_$install.txt";
}

echo $ssh->exec( $command );

Remote SSH is quite powerful, make sure it’s locked down

Even if your code is locked down to logged in users, it still a good idea to make sure the right users are able to execute the run scripts. I run the last part of the my script behind a custom check which double checks that logged in user should be able to run the commands.

<?php
// Checks permissions
if ( anchor_verify_permissions( $post_id ) ) {

    // Runs command on remote on production

}

Putting it all together, the following is an example of a custom code which runs via a WordPress AJAX request. It handles activating, deactivating, snapshotting and backing my WordPress sites remotely via SSH.

<?php

// Processes commands sent to remote server
add_action( 'wp_ajax_anchor_install', 'anchor_install_action_callback' );

function anchor_install_action_callback() {
    global $wpdb; // this is how you get access to the database

    $post_id = intval( $_POST['post_id'] );
    $cmd = $_POST['command'];
    $value = $_POST['value'];
    $install = get_field('install',$post_id);

    if ($cmd == "activate") {
        $command = "Scripts/Run/activate.sh $install";
    }
    if ($cmd == "deactivate") {
        $command = "Scripts/Run/deactivate.sh $install";
    }
    if ($cmd == "backup") {
        date_default_timezone_set('America/New_York');
        $t=time();
        $timestamp = date("Y-m-d-hms",$t);
        $command = "Scripts/Run/backup.sh $install > ~/Tmp/$timestamp-backup_$install.txt 2>&1 & sleep 5; head ~/Tmp/$timestamp-backup_$install.txt";
    }
    if ($cmd == "snapshot") {
        date_default_timezone_set('America/New_York');
        $t=time();
        $timestamp = date("Y-m-d-hms",$t);
        if($value) {
            $command = "Scripts/Run/snapshot.sh $domain --email=$value > ~/Tmp/$timestamp-snapshot_$install.txt 2>&1 & sleep 5; head ~/Tmp/$timestamp-snapshot_$install.txt";
        } else {
            $command = "Scripts/Run/snapshot.sh $domain > ~/Tmp/$timestamp-snapshot_$install.txt 2>&1 & sleep 5; head ~/Tmp/$timestamp-snapshot_$install.txt";
        }
    }

    // Checks permissions
    if ( anchor_verify_permissions( $post_id ) ) {

        // Runs command on remote on production
        require_once(ABSPATH . '/vendor/autoload.php');

        $key = new \phpseclib\Crypt\RSA();

        // Loads private SSH key from the file: privatekey
        $key->loadKey(file_get_contents(ABSPATH . 'privatekey'));

        // Connects to remote server
        $ssh = new \phpseclib\Net\SSH2(ANCHOR_REMOTE_ADDRESS, ANCHOR_REMOTE_PORT);

        if (!$ssh->login(ANCHOR_REMOTE_USER, $key)) {
        exit('Login Failed');
        }
        echo $ssh->exec( $command );

    } else {
        echo "Permission denied";
    }

    wp_die(); // this is required to terminate immediately and return a proper response
}