Skip to content

Latest commit

 

History

History
347 lines (267 loc) · 16 KB

GUIDE.md

File metadata and controls

347 lines (267 loc) · 16 KB

BastionZero & Google Cloud Run Guide

This guide is a walkthrough of how to leverage the zli and BastionZero service accounts to SSH into a Linux host from Google Cloud Run (GCR). We use the example Node.js server, Dockerfile, and Terraform provided in this repo to demonstrate this use case. Please feel free to mix and match elements of these components with your own custom integration to better fit your specific requirements.

Note: Terraform is not required to implement this Cloud Run use case; it is included in this example repo to make the guide easier to follow.

Before You Begin

  • You must be an administrator of your BastionZero organization in order to create a BastionZero service account.
  • Ensure the zli is installed on your machine as it is used to perform some of the one-time manual steps when creating a BastionZero service account.
  • Ensure gcloud is installed on your machine as it is used to submit a build of the example server to GCR. Don't forget to authorize the gcloud CLI (instructions found here).
  • Ensure terraform is installed on your machine as it is used to automate some of the infrastructure required to deploy the Cloud Run service.
  • Ensure docker is installed on your machine and is currently running.
  • Set up Application Default Credentials (ADC) in order to configure the google Terraform provider used in main.tf: gcloud auth application-default login.
  • Create a BastionZero API key in order to configure the bastionzero Terraform provider used in main.tf. Manage your API keys at the API key panel found here.
  • Clone this repository and change your current working directory (cwd) to the root of the repo; the shell commands in this guide assume you have changed your cwd accordingly.

Create a BastionZero Service Account (SA) via Google Cloud Platform (GCP)

First, we'll create a BastionZero service account in your BastionZero organization. We'll also download its associated public/private key pair and save it, along with some other credentials, in Google Secret Manager for later use in the Node.js server.

Create the GCP Service Account

Google Cloud Platform provides a convenient way of creating the public/private key pair and the JSON Web Key Set(JWKS) URL, both of which are needed for setting up your BastionZero service account.

Please follow steps 1-4 in this guide, which details how to create the SA on GCP.

At the end of the linked guide, you should have downloaded a .json file (provider file) that contains your service account's credentials from GCP. Let's rename this file to provider-file.json, and place it in your current working directory for convenience.

Important: Please keep note of the service account's email address as we'll need it later. The email address should be displayed in the service account creation screen after filling in the details. It can also be found in the table of service accounts under "IAM" -> "Service Accounts" after creation. It should look something like this: <service-account-id>@<gcp-project-id>.iam.gserviceaccount.com.

Create the BastionZero Service Account

Next, let's use these credentials to create the BastionZero service account in your BastionZero organization:

zli login
zli service-account create provider-file.json

The result of the zli service-account create command will be a bzero-credentials.json file created in your current working directory.

Store Credentials in Google Secret Manager

To finish up this section, we'll upload both the provider-file.json file and bzero-credentials.json file as secrets in Google Secret Manager. Open up the Secret Manager on the GCP Console.

Secret #1: cloudrun-example-sa-provider-cred

  1. Click "Create Secret."
  2. In the name field, enter: cloudrun-example-sa-provider-cred. You can use a different name, but you'll have to input your chosen secret name when we apply the Terraform.
  3. Click "Secret value" -> Upload file -> "Browse" and upload the provider-file.json file as the value for this secret.
  4. Click the "Create Secret" button at the bottom to apply your selections and create the secret.

Secret #2: cloudrun-example-sa-bzero-cred

Let's create one more secret. Follow the same instructions in the section above, except this secret's name should be cloudrun-example-sa-bzero-cred and in step #3, you should upload the bzero-credentials.json file instead.

Upload Example Container Image to GCR

Next, we'll use the gcloud CLI to submit a build of the example Node.js server to GCR; our example Cloud Run service will run using this image.

Before uploading, let's explain some of the components in both the Dockerfile and the application code (*.ts files).

Dockerfile

The Dockerfile defines how to build the container image and entrypoint for the Cloud Run service application.

Install zli as a Dependency

The following section is the code that installs the zli as a system package dependency in the container. This step is required so the Node.js server can use the zli to SSH into a BastionZero-secured Linux host:

# Install zli (https://docs.bastionzero.com/docs/deployment/installing-the-zli,
# APT)
RUN apt-get update -y && apt-get install -y gnupg software-properties-common
RUN apt-key adv --keyserver keyserver.ubuntu.com \
--recv-keys E5C358E613982017 && add-apt-repository \
'deb https://download-apt.bastionzero.com/production/apt-repo stable main'
RUN apt-get update -y && apt-get install -y zli && apt-get clean

Install ssh as a Dependency

This section installs the openssh-client so that the Node.js server can execute ssh. It also creates an empty ~/.ssh/config file, which the zli updates to store config information related to connecting to your target over SSH:

# Install ssh client
RUN apt-get install -y openssh-client
# Create empty SSH config file
RUN mkdir -p /home/.ssh && touch /home/.ssh/config

app.ts

Most of the core logic of the Node.js server can be found in app.ts. Let's go over some parts of the code.

Fetch Secrets From Google Secret Manager

We use the @google-cloud/secret-manager npm package to fetch and store the required service account credentials in memory:

// Use secret manager to get credentials required to run `zli service-account
// login`
const secretClient = new SecretManagerServiceClient();
/**
* Load a secret from the secret manager
* @param secretName Name of the secret
* @returns The contents of the secret value
*/
async function loadSecretFromSecretManager(secretName: string) {
const [accessResponse] = await secretClient.accessSecretVersion({
name: secretName,
});
return accessResponse.payload?.data?.toString();
}
// Fetch the credentials
const providerCredentials = await loadSecretFromSecretManager(PROVIDER_FILE_SECRET_NAME);
const bzeroCredentials = await loadSecretFromSecretManager(BZERO_FILE_SECRET_NAME);

Use Service Account Credentials to Log In via the zli Programmatically

We use zli service-account login to perform a headless authentication to the BastionZero platform:

/**
* Runs `zli service-account login` using the fetched credentials
* @returns The stdout and stderr output from the `zli`
*/
async function zliServiceAccountLogin(): Promise<string> {
// Create temp files (cleaned up after done using them) to store credentials
// on disk. `zli service-account login` only takes in filepaths right now
const { path: providerFilePath, cleanup: cleanupProviderFile } = await file();
const { path: bzeroFilePath, cleanup: cleanupBzeroFile } = await file();
try {
await fs.writeFile(providerFilePath, providerCredentials as string);
await fs.writeFile(bzeroFilePath, bzeroCredentials as string);
return await execCommand(`zli service-account login --providerCreds ${providerFilePath} --bzeroCreds ${bzeroFilePath}`);
} finally {
await cleanupBzeroFile();
await cleanupProviderFile();
}
}

Using BastionZero service accounts prevents the need to perform a user-interactive login session with your identity provider (zli login).

SSH

We define a /ssh HTTP endpoint that performs the SSH logic to run an arbitrary command on a Linux host secured by BastionZero:

// SSH example
app.get('/ssh', asyncHandler(async (req, res) => {
if (!loggedIn) {
// Login to BastionZero using SA credentials
const zliLoginOutput = await zliServiceAccountLogin();
console.log(`zli service-account login: ${zliLoginOutput}`);
loggedIn = true;
}
// Build ssh command from query. Set some defaults if query parameters are
// missing
let sshUserString: string = "root";
let sshHostString: string = "";
let sshCommandString: string = "uname -a";
if (req.query.user) {
sshUserString = req.query.user as string;
}
if (req.query.host) {
sshHostString = req.query.host as string;
} else {
throw new Error("Please specify a host in the query parameters");
}
if (req.query.cmd) {
sshCommandString = req.query.cmd as string;
}
const constructedSshCmd = `ssh -F /home/.ssh/config ${sshUserString}@${sshHostString} ${sshCommandString}`;
console.log(`SSH command: ${constructedSshCmd}`);
try {
if (!generatedSshConfig) {
// Generate ssh config
const generateOutput = await execCommand('zli generate sshConfig')
console.log(`zli generate sshConfig: ${generateOutput}`);
generatedSshConfig = true;
}
// SSH!
const sshOutput = await execCommand(constructedSshCmd);
res.send(sshOutput);
} catch (error) {
// It could be that re-login fixes the issue
loggedIn = false;
throw error;
}
}));

Some steps are cached to speed-up subsequent calls to /ssh if the same container is still running.

  • zliServiceAccountLogin() logs in to BastionZero using the service account credentials. See the previous section for more details.
  • zli generate sshConfig updates the ~/.ssh/config file with a list of BastionZero targets that the logged-in BastionZero service account has policy access to connect to.
  • ssh -F ... performs the parsed command from the query string against the Linux host via SSH.

Upload to GCR

Run the following command from the root directory of this repository to upload an image of this example application to GCR:

gcloud builds submit --tag gcr.io/<project-id>/bastionzero-cloudrun-example

Please replace <project-id> with the GCP project ID of your choice.

Apply Terraform to Deploy Remaining Infrastructure

With all the previous steps completed, we're now ready to perform the remaining infrastructure tasks to get the example Cloud Run service running. This step is easy as we'll just use the example main.tf Terraform file to apply these infrastructure changes.

Install the Terraform Providers

We use the following providers in main.tf:

Run the following command to install the providers used by main.tf:

terraform init

Configure the Terraform Providers

The google Terraform provider should automatically be configured if you have set up your ADC as described in the first section.

The docker Terraform provider has no additional configuration. However, please ensure docker is running before proceeding.

To configure the bastionzero Terraform provider, you'll need to export an environment variable that holds the API secret of your API key you created earlier.

Set the BASTIONZERO_API_SECRET environment variable to the API key's secret that you created in the first section:

export BASTIONZERO_API_SECRET=api-secret

Apply the Terraform

We're now ready to run terraform apply to apply the remaining infrastructure and deploy the Cloud Run service.

Before applying, please read over the main.tf file and the comments to better understand what it is doing.

Here is a quick summary:

  • A new GCP service account is created. This service account is given minimal permissions, namely only read access to the secrets created earlier.
  • The Cloud Run service is created. It is configured to run with the least privileged service account created in the previous step. It is also configured to run using the image we uploaded to GCR.
  • A BastionZero target connect policy is created, which gives the BastionZero service account SSH access (as the root user) to targets in the Default and AWS environments in your BastionZero organization. Please modify accordingly to better fit your infrastructure requirements (e.g. use different target_user than root or other envs than Default and AWS).

Run the following command to apply the remaining infrastructure via Terraform:

terraform apply

Terraform will prompt you to fill in some input variables:

  • var.bastionzero_service_account_email: Enter in the email of the service account we created in the beginning of this guide.
  • var.project_id: Enter in the same <project-id> you selected when building the image. This same project will contain the Cloud Run service that we're about to deploy.

If you picked different secret names than the ones described earlier, then you will also need to override the defaults and pass different values for var.provider_creds_file_secret_name and var.bzero_creds_file_secret_name.

Review the returned Terraform plan. Type in "yes" and press enter to apply the plan.

Demo via Proxy

Let's demo the example by proxying the Cloud Run service to localhost and authenticating as the active account (i.e. the account you are logged in as via gcloud). This step is required because by default the Cloud Run service requires authenticated access in order to invoke its public endpoints. See more details about Cloud Run authentication here.

Run the following command to start the proxy on localhost:

gcloud run services proxy bzero-cloudrun

There should now be a proxy server running on localhost:8080 that proxies your requests to the Cloud Run service.

  • /: Returns the version of the zli executable installed on the container.
  • /ssh?host=example-target: Executes the default command against the target example-target. In this example, ssh logs in as root to execute the command.
  • /ssh?host=example-target&cmd=whoami: Executes the whoami command against the target example-target. In this example, ssh logs in as root to execute the command.
  • /ssh?host=example-target&cmd=whoami&user=foo: Executes the whoami command against the target example-target. In this example, ssh logs in as foo to execute the command. You may receive an error if the service account does not have BastionZero policy access to SSH as the foo user or if the user foo does not exist on the Linux host.

Cleanup

Below are some optional cleanup steps: