Configure apps to use SSH

Stay organized with collections Save and categorize content based on your preferences.

This document describes how to configure apps to programmatically connect between two virtual machine (VM) instances using SSH and OS Login. Enabling apps to use SSH can be useful for automating system management processes.

All code samples used in this guide is hosted on the GoogleCloudPlatform/python-docs-samples GitHub page.

Before you begin

Set up an SSH app

Set up your app to manage SSH keys and initiate SSH connections to Compute Engine VMs. At a high level, your app should do the following:

  1. Import the Google API Discover Service to build client libraries, which enables you to authenticate with the OS Login API.
  2. Initialize the OS Login API object to enable your app to use OS Login.
  3. Implement a create_ssh_key() method that generates an SSH key for the VM's service account and adds the public key to the service account.
  4. Call the getLoginProfile() method from the OS Login API to get the POSIX user name that the service account uses.
  5. Implement a run_ssh() method to execute a remote SSH command.
  6. Remove the temporary SSH key files.

Sample SSH app

The service_account_ssh.py sample app demonstrates a possible implementation of an SSH app. In this example, the app uses the run_ssh() method to execute a command on a remote instance and return the command output.

#!/usr/bin/env python

# Copyright 2018 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Example of using the OS Login API to apply public SSH keys for a service
account, and use that service account to execute commands on a remote
instance over SSH. This example uses zonal DNS names to address instances
on the same internal VPC network.
"""

import argparse
import logging
import subprocess
import time
import uuid

from google.auth.exceptions import RefreshError
import googleapiclient.discovery
import requests

SERVICE_ACCOUNT_METADATA_URL = (
    'http://metadata.google.internal/computeMetadata/v1/instance/'
    'service-accounts/default/email')
HEADERS = {'Metadata-Flavor': 'Google'}



def execute(cmd, cwd=None, capture_output=False, env=None, raise_errors=True):
    """Execute an external command (wrapper for Python subprocess)."""
    logging.info('Executing command: {cmd}'.format(cmd=str(cmd)))
    stdout = subprocess.PIPE if capture_output else None
    process = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=stdout)
    output = process.communicate()[0]
    returncode = process.returncode
    if returncode:
        # Error
        if raise_errors:
            raise subprocess.CalledProcessError(returncode, cmd)
        else:
            logging.info('Command returned error status %s', returncode)
    if output:
        logging.info(output)
    return returncode, output


def create_ssh_key(oslogin, account, private_key_file=None, expire_time=300):
    """Generate an SSH key pair and apply it to the specified account."""
    private_key_file = private_key_file or '/tmp/key-' + str(uuid.uuid4())
    execute(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file])

    with open(private_key_file + '.pub', 'r') as original:
        public_key = original.read().strip()

    # Expiration time is in microseconds.
    expiration = int((time.time() + expire_time) * 1000000)

    body = {
        'key': public_key,
        'expirationTimeUsec': expiration,
    }
    print(f'Creating key {account} and {body}')
    for attempt_no in range(1, 4):
        try:
            # This method sometimes failed to work causing issues like #7277
            # Maybe retrying it with some delay will make things better
            oslogin.users().importSshPublicKey(parent=account, body=body).execute()
        except RefreshError as err:
            if attempt_no == 3:
                raise err
            time.sleep(attempt_no)
        else:
            break

    return private_key_file


def run_ssh(cmd, private_key_file, username, hostname):
    """Run a command on a remote system."""
    ssh_command = [
        'ssh', '-i', private_key_file, '-o', 'StrictHostKeyChecking=no',
        '{username}@{hostname}'.format(username=username, hostname=hostname),
        cmd,
    ]
    ssh = subprocess.Popen(
        ssh_command, shell=False, stdout=subprocess.PIPE,
        stderr=subprocess.PIPE)
    result = ssh.stdout.readlines()
    return result if result else ssh.stderr.readlines()



def main(cmd, project, instance=None, zone=None,
         oslogin=None, account=None, hostname=None):
    """Run a command on a remote system."""

    # Create the OS Login API object.
    oslogin = oslogin or googleapiclient.discovery.build('oslogin', 'v1')

    # Identify the service account ID if it is not already provided.
    account = account or requests.get(
        SERVICE_ACCOUNT_METADATA_URL, headers=HEADERS).text
    if not account.startswith('users/'):
        account = 'users/' + account

    # Create a new SSH key pair and associate it with the service account.
    private_key_file = create_ssh_key(oslogin, account)

    # Using the OS Login API, get the POSIX username from the login profile
    # for the service account.
    for attempt_no in range(1, 4):
        try:
            profile = oslogin.users().getLoginProfile(name=account).execute()
        except RefreshError as err:
            if attempt_no == 3:
                raise err
            time.sleep(attempt_no)
        else:
            username = profile.get('posixAccounts')[0].get('username')
            break

    # Create the hostname of the target instance using the instance name,
    # the zone where the instance is located, and the project that owns the
    # instance.
    hostname = hostname or '{instance}.{zone}.c.{project}.internal'.format(
        instance=instance, zone=zone, project=project)

    # Run a command on the remote instance over SSH.
    result = run_ssh(cmd, private_key_file, username, hostname)

    # Print the command line output from the remote instance.
    # Use .rstrip() rather than end='' for Python 2 compatability.
    for line in result:
        print(line.decode('utf-8').rstrip('\n\r'))

    # Shred the private key and delete the pair.
    execute(['shred', private_key_file])
    execute(['rm', private_key_file])
    execute(['rm', private_key_file + '.pub'])


if __name__ == '__main__':

    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '--cmd', default='uname -a',
        help='The command to run on the remote instance.')
    parser.add_argument(
        '--project',
        help='Your Google Cloud project ID.')
    parser.add_argument(
        '--zone',
        help='The zone where the target instance is located.')
    parser.add_argument(
        '--instance',
        help='The target instance for the ssh command.')
    parser.add_argument(
        '--account',
        help='The service account email.')
    parser.add_argument(
        '--hostname',
        help='The external IP address or hostname for the target instance.')
    args = parser.parse_args()

    main(args.cmd, args.project, instance=args.instance, zone=args.zone,
         account=args.account, hostname=args.hostname)

Run the SSH app

After you create an app that uses SSH, you can run the app by following a process similar to the following example, which installs and runs the service_account_ssh.py sample app. The libraries you install may differ, depending on the programming language the app uses.

Alternatively, you can write an app that imports service_account_ssh.py and runs it directly.

  1. Connect to the VM that hosts the SSH app.

  2. On the VM, install pip and the Python 3 client library:

    sudo apt update && sudo apt install python3-pip -y && pip install --upgrade google-api-python-client
    
  3. Optional: If you are using the service_account_ssh.py sample app, download it from GoogleCloudPlatform/python-docs-samples:

    curl -O https://raw.githubusercontent.com/GoogleCloudPlatform/python-docs-samples/master/compute/oslogin/service_account_ssh.py
    
  4. Run the SSH app. The sample app uses argparse to accept variables from the command line. In this example, instruct the app to install and run cowsay on another VM in your project.

    python3 service_account_ssh.py \
       --cmd 'sudo apt install cowsay -y && cowsay "It works!"' \
       --project=PROJECT_ID --instance=VM_NAME --zone=ZONE
    

    Replace the following:

    • PROJECT_ID: the project ID of the VM that the app is connecting to.
    • VM_NAME: the name of the VM that the app is connecting to.
    • ZONE: the zone of the VM that the app is connecting to.

    The output is similar to the following:

    ⋮
    ___________
     It works!
    -----------
          \   ^__^
           \  (oo)\_______
              (__)\       )\/\
                  ||----w |
                  ||     ||
    

What's next

  • Download and view the full code sample. The full sample includes a small example of using all of these methods together. Feel free to download it, change it, and run it to suit your needs.
  • Learn more about about how SSH connections work in Compute Engine, including SSH key configuration and storage.