API use cases

AWS registration

Registration of an asset is performed in a user data script. We provide an example script that works with the standard AWS Linux AMI (though it should work for any Linux AMI).

Setup in /login

We configure the endpoints that come online so that all go into the same Jump Group and are accessed via the same Jumpoint. For this example, we use Jumpoint with ID 1 and a shared Jump Group with ID 1. These are referenced in the script below as JUMPOINT_ID and JUMP_GROUP_ID. Configure access to this Jumpoint and Jump Group as needed.

Generate an API account for your AWS scripts to use, and note the CLIENT_ID and CLIENT_SECRET for use in the script below.

The API Account created does not need access to Vault in this example.

Setup SSH credentials in Vault

If you already have a key pair in AWS you want to use, make sure you have the private key available. If not, open the EC2 section and navigate to Network and Security > Key Pairs in the AWS console. Generate a new key pair and save the private key.

In /login, navigate to Vault > Accounts and add a new generic account. Set the type to SSH and add the username you are using on the AMI (AWS defaults this to ec2-user) as well as the private key. This username is the TARGET_USER in the script below.

At the bottom of the account configuration, associate this account with the Jump Group from above by selecting Jump Items Matching Criteria and selecting the desired Jump Group.

Save the new account.

Once the account is saved, configure a Group Policy to grant users permission to inject it.

Deploy the instances in EC2

EC2 instance initialization is performed with user data scripts. The script below registers a Linux AMI as a Shell Jump with the Jumpoint and Jump Group configured.

Prepare and deploy a Linux AMI in EC2. In the user data field, paste this script:

#!/bin/bash

# SRA API Credentials
export BT_CLIENT_ID=XXX
export BT_CLIENT_SECRET=XXX
export BT_API_HOST=XXX

# The Jump Group and Jumpoint to use for the Jump Item we create
JUMP_GROUP_ID=1
JUMP_GROUP_TYPE=shared
JUMPOINT_ID=1

TARGET_USER=ec2-user
# Query the AWS Meta-data service for information about this instance to use
# when creating the Jump Item
INSTANCE_ID=`curl http://169.254.169.254/latest/meta-data/instance-id`
INSTANCE_IP=`curl http://169.254.169.254/latest/meta-data/public-ipv4`
INSTANCE_NAME=$INSTANCE_IP
http_response=$(curl -s -o name.txt -w "%{http_code}" http://169.254.169.254/latest/meta-data/tags/instance/Name)
if [ "$http_response" == "200" ]; then
    INSTANCE_NAME=$(cat name.txt)
fi

apt update
apt install -y unzip
curl -o btapi.zip -L https://$BT_API_HOST/api/config/v1/cli/linux
unzip btapi.zip

echo "
name=\"${INSTANCE_NAME:-$INSTANCE_IP}\"
hostname=$INSTANCE_IP
jump_group_id=$JUMP_GROUP_ID
jump_group_type=$JUMP_GROUP_TYPE
username=$TARGET_USER
protocol=ssh
port=22
terminal=xterm
jumpoint_id=$JUMPOINT_ID
tag=$INSTANCE_ID
" | ./btapi -k add jump-item/shell-jump

rm name.txt
rm btapi
rm btapi.zip

  • Add the client credentials as BT_CLIENT_ID and BT_CLIENT_SECRET.
  • Add the site’s hostname as BT_API_HOST (just the hostname, no HTTPS).
  • Make sure that TARGET_USER, JUMPOINT_ID, and JUMP_GROUP_ID (and type) are the values configured above.

This script downloads the btapi command line tool and pipes the instance’s data to create a new Shell Jump Item. The Jump Item is available for immediate use once the instance shows online.

This script uses the InstanceId as the item’s tag so that you may easily filter it later when performing cleanup. It also attempts to read the instance’s Name tag to use as the Jump Item’s name field for easy identification later. In order for this to work, you must set Allow tags in metadata to Enable when launching the instance in AWS. If the Name is not available, the instance’s IP address is used instead.

AWS cleanup

Cleaning up terminated AWS Jump Items may be automated in multiple ways, depending on the desired behavior. Here, we show two different methods: a script that may be run on-demand to clean up terminated instances, and an AWS Lambda function and EventBridge rule that is triggered automatically.

On-demand script

If you want to clean up Jump Items on demand, the following script can be run as needed or scheduled to run as needed with a tool like chron.

#!/bin/bash

export BT_CLIENT_ID=XXX
export BT_CLIENT_SECRET=XXX
export BT_API_HOST=XXX

export AWS_ACCESS_KEY_ID=XXX
export AWS_SECRET_ACCESS_KEY=XXX

# Note this requires the AWS CLI tool to be installed
INSTANCE_IDS=$(aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId]' --filters 'Name=instance-state-name,Values=[terminated]' --output text)

if [[ -z "$INSTANCE_IDS" ]]; then
  exit
fi

for inst in "${INSTANCE_IDS[@]}"; do
  ID=$(echo "tag=$inst" | btapi --env-file=~/.config/aws-api -kK list jump-item/shell-jump | perl -ne '/^0__id=(\d+)$/ && print $1')
  btapi --env-file=~/.config/aws-api delete jump-item/shell-jump $ID
done

AWS hooks

Setting up the hooks in AWS requires two pieces in AWS:

  • A Lambda function to do the cleanup
  • An EventBridge rule to call the Lambda function

The following example is one way to configure these pieces

Create the Lambda

This example uses Python, but you can use the same logic for any language you prefer.

This example makes use of the requests, requests_oauthlib, and oauthlib python libraries. To use these, you must create and upload a layer with these dependencies to attach to the lambda. This may be performed from a local Linux machine with the same python version installed that the lambda uses, or you may use the AWS Cloud9 service to spin up a compatible environment.

To create the layer, use the following commands:

mkdir tmp
cd tmp
virtualenv v-env
source ./v-env/bin/activate
pip install requests oauthlib requests_oauthlib
deactivate

mkdir python
# Using Python 3.9
cp -r ./v-env/lib64/python3.9/site-packages/* python/.
zip -r requests_oauthlib_layer.zip python

# Or manually upload the zip under AWS Lambda > Layers
aws lambda publish-layer-version --layer-name requests_oauthlib --zip-file fileb://requests_oauthlib_layer.zip --compatible-runtimes python3.9

With the layer added, navigate to AWS Lambda and create a new function. Select Python as the runtime with the same version used above. The function requires Describe* permissions for EC2 as well as the general AWS Lambda role.

Once the function is created, replace the contents of the generated lambda_function.py file with this script:

import boto3
import os
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session

ec2 = boto3.client('ec2', region_name=os.environ.get('AWS_REGION'))

BT_CLIENT_ID = os.environ.get('BT_CLIENT_ID')
BT_CLIENT_SECRET = os.environ.get('BT_CLIENT_SECRET')
BT_API_HOST = os.environ.get('BT_API_HOST')

class API:
    def __init__(self) -> None:
        self.client = BackendApplicationClient(client_id=BT_CLIENT_ID)
        self.oauth = OAuth2Session(client=self.client)
        self.token = 'bad'

    def call(self, method, url, headers=None, data=None, **kwargs):
        def reload_token(r, *args, **kwargs):
            if r.status_code == 401:
                self.refreshToken()
                return self.call(method, url, headers=headers, data=data, **kwargs)
            elif r.status_code > 400:
                r.raise_for_status()

        d = data if method != 'get' else None
        p = data if method == 'get' else None
        resp = self.oauth.request(
            method,
            f"https://{BT_API_HOST}/api/config/v1/{url}",
            headers=headers, json=d, params=p, hooks={'response': reload_token}, **kwargs)

        resp.raise_for_status()

        return resp

    def refreshToken(self) -> None:
        self.token = self.oauth.fetch_token(
            token_url=f"https://{BT_API_HOST}/oauth2/token",
            client_id=BT_CLIENT_ID,
            client_secret=BT_CLIENT_SECRET
        )

client = API()

def lambda_handler(event, context):
    instances = ec2.describe_instances(
        Filters=[
            {'Name': 'instance-state-name', 'Values': ['terminated']}
        ]
    )
    data = []
    
    for r in instances['Reservations']:
        for inst in r['Instances']:
            print(inst)
            d = {
                'id': inst['InstanceId'],
                'state': inst['State'],
                'ip': inst.get('PublicIpAddress'),
                'name': [x['Value'] for x in inst['Tags'] if x['Key'] == 'Name'],
            }
            response = client.call('get', 'jump-item/shell-jump', data={'tag': inst['InstanceId']})
            items = response.json()
            if len(items) > 0:
                item = items[0]
                d['data'] = item
                client.call('delete', f'jump-item/shell-jump/{item["id"]}')
            data.append(d)
            
    return {
        'statusCode': 200,
        'body': data
    }


Next, scroll to the bottom of the page to the Layers panel. Click Add a layer and select the layer that was created above.

This script is designed to read the BT API information from the environment. You must add the BT_API_HOST, BT_CLIENT_ID, and BT_CLIENT_SECRET configuration variables under Configuration -> Environment variables.

Configure EventBridge

Navigate to Amazon EventBridge > Rules and click Create rule. Name the rule, ensure it is enabled, select Rule with an event pattern, and click Next.

To build the event pattern, choose the AWS Events or EventBridge partner events option in the Event source panel, and then scroll down to the Event pattern panel. Select the Custom patterns (JSON Editor) option, paste the following pattern, and click Next.

{
  "source": ["aws.ec2"],
  "detail-type": ["EC2 Instance State-change Notification"],
  "detail": {
    "state": ["terminated"]
  }
}

For the event target, select AWS Service, then pick Lambda function from the dropdown. For function, select the name of the Lambda created above. Finish creating the rule definition.

Finish

Once the rule and lambda are in place, the lambda is invoked when any EC2 instance moves or is moving to terminated status and is removed from the Jump Item list.

Scripting a new setup

The script below runs through a more complicated automated process. This script sets up the given instance to be a Jumpoint for a VPC and creates a new Jump Group and SSH key in Vault for the VPC. It then grants access to these new resources to a given Group Policy.

This script assumes an Ubuntu Server instance.

Amazon Linux AMIs are not supported as Jumpoint hosts. Jumpoint hosts require GLIBC 2.27 and the Amazon Linux AMIs support only 2.26.
#!/bin/bash
set -euo pipefail
set -x

# SRA API Credentials
export BT_CLIENT_ID=XXX
export BT_CLIENT_SECRET=XXX
export BT_API_HOST=XXX

# Set to the ID of the Group Policy to tie everything together
GROUP_POLICY_ID=XXX

# Set this to the user account for this instance
TARGET_USER=ubuntu
# Query AWS metadta for this instance to data needed when creating items later
INSTANCE_IP=`curl http://169.254.169.254/latest/meta-data/public-ipv4`
macid=$(curl http://169.254.169.254/latest/meta-data/network/interfaces/macs/)
# Using the VPC ID as the base for all our names
NAME_BASE=$(curl http://169.254.169.254/latest/meta-data/network/interfaces/macs/${macid}/vpc-id)

HOME=${HOME:=/home/$TARGET_USER}

# For running as a user
JUMPOINT_BASE_DIR="$HOME/.beyondtrust/jumpoint"
SYSTEMD_DIR="$HOME/.config/systemd/user"
SYSTEMD_ARGS=--user
JUMPOINT_USER=""

if [ "$(whoami)" == "root" ]; then
    # For running as root
    JUMPOINT_BASE_DIR="/opt/beyondtrust/jumpoint"
    SYSTEMD_DIR="/etc/systemd/system"
    SYSTEMD_ARGS=""
    JUMPOINT_USER="--user $TARGET_USER"
fi

# Make the command calls a bit easier to write
ORIG_PATH=$PATH
cwd=$(pwd)
export PATH=$cwd:$PATH

# Ubuntu server does not have unzip by default
sudo apt update
sudo apt install -y unzip

# Download jq into the current directory for ease of parsing JSON responses
curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o jq
chmod +x jq
curl -o btapi.zip -L https://$BT_API_HOST/api/config/v1/cli/linux
unzip btapi.zip

# Create a Jumpoint for this VPC
jp=$(echo "
name=$NAME_BASE
platform=linux-x86
shell_jump_enabled=True
" | btapi -k add jumpoint)

jpid=$(echo "$jp" | jq '.id')

echo "Created Jumpoint with id [$jpid]"

# Download and run the Jumpoint installer
installer=$(btapi download "jumpoint/$jpid/installer" | jq -r '.file')
chmod +x "$installer"
# Make sure the base install directory exists
mkdir -p "$JUMPOINT_BASE_DIR"

# IMPORTANT: Make sure your linux distro has all the packages needed to install 
# the Jumpoint. Ubuntu server 22 needs these two
sudo apt install -y libxkbcommon0 fontconfig
sh "$installer" --install-dir "$JUMPOINT_BASE_DIR/$BT_API_HOST" $JUMPOINT_USER

# Make sure the systemd service directory exists (mostly for the user mode directory)
mkdir -p "$SYSTEMD_DIR"

# Create the systemd service file
echo "[Unit]
Description=BeyondTrust Jumpoint Service
Wants=network.target
After=network.target

[Service]
Type=forking
ExecStart=$JUMPOINT_BASE_DIR/$BT_API_HOST/init-script start" > "$SYSTEMD_DIR/jumpoint.$BT_API_HOST.service"

if [ "$(whoami)" != "$TARGET_USER" ]; then
    echo "User=$TARGET_USER" >> "$SYSTEMD_DIR/jumpoint.$BT_API_HOST.service"
fi

echo "
Restart=no
WorkingDirectory=$JUMPOINT_BASE_DIR/$BT_API_HOST

[Install]
WantedBy=default.target
" >> "$SYSTEMD_DIR/jumpoint.$BT_API_HOST.service"

# Load the Jumpoint service and start it
systemctl $SYSTEMD_ARGS daemon-reload
systemctl $SYSTEMD_ARGS start "jumpoint.$BT_API_HOST.service"

# Cleanup the installer file
rm -f "$installer"

# Create a Jump Group for this VPC
jg=$(echo "
name=\"$NAME_BASE Jump Group\"
" | btapi -k add jump-group)

jgid=$(echo "$jg" | jq '.id')

# Create an SSH Key for this VPC and add the private key to Vault
# NOTE, you will need to manually asociate this credential to the 
# Jump Group for this VPC in /login
ssh-keygen -f "./key" -P "" -q -t ed25519
touch "$HOME/.ssh/authorized_keys"
cat ./key.pub >> "/home/$TARGET_USER/.ssh/authorized_keys"
priv=$(cat ./key)

vk=$(echo "
type=ssh
name=\"$NAME_BASE SSH\"
username=$TARGET_USER
private_key=\"$priv\"
" | btapi -k add vault/account)

vkid=$(echo "$vk" | jq '.id')

# Cleanup the key
rm -f ./key
rm -f ./key.pub

# Create an SSH Jump Item back to this instance
echo "
name=\"$NAME_BASE Jumpoint\"
hostname=$INSTANCE_IP
jump_group_id=$jgid
jump_group_type=shared
username=$TARGET_USER
protocol=ssh
port=22
terminal=xterm
jumpoint_id=$jpid
" | btapi -k add jump-item/shell-jump

# Modify the Group Policy to grant access to the Jumpoint, Jump Group and Vault Account
echo "jumpoint_id=$jpid" | btapi -k add group-policy/$GROUP_POLICY_ID/jumpoint
echo "jump_group_id=$jgid" | btapi -k add group-policy/$GROUP_POLICY_ID/jump-group
echo "
account_id=$vkid
role=inject
" | btapi -k add group-policy/$GROUP_POLICY_ID/vault-account

# Cleanup the tools downloaded at the top of this script
rm -f jq
rm -f btapi
rm -f btapi.zip

# Reset PATH
export PATH=$ORIG_PATH