Practical Bazel: Wrapping Run Targets to Provide Additional Context
Practical Bazel bazel
Published: 2020-11-19
Practical Bazel: Wrapping Run Targets to Provide Additional Context

An executable rule which can be executed via bazel run is the natural way to model interactions with external systems in Bazel such as uploading build artifacts to a remote artifact repository. For example, imagine a rules_artifactory ruleset which includes a rule artifactory_push() executable rule which uploads a compiled .dpkg to an Artifactory apt repository, or a rules_docker ruleset which has a rule docker_push() which pushes a Docker image to a remote image repository.

One key design consideration for these rules is “how will users securely pass authentication credentials (e.g. a username and a password) to the push job”? After all, it is impractical to have the rules know about every possible secret management system that a user might use. For example, one user could be using HashiCorp’s Vault, another Azure’s Key Vault, another Thycotic’s Secret Server, and there are probably dozens more.

A commonly-used technique is for these tools to allow users pass in secrets via environment variables. For example, Terraform allows specifying an Azure service principal using the ARM_CLIENT_ID, ARM_CLIENT_SECRET, and ARM_TENANT_ID environment variables. But how to pass these environment variables to the bazel run job?

One option is to use a env rule attribute, as in:

1
2
3
4
5
6
7
# BUILD
my_push(
    name = "my_push",
    # DON'T DO THIS, IT IS NOT SECURE!
    env = { "MY_PUSH_USERNAME": "foo", "MY_PUSH_PASSWORD": "bar" },
    ...
)

But this is obviously insecure, as we’d be storing secrets in source control.

The technique I’ve been using lately is to wrap the executable rule in a sh_binary() and have the wrapping sh_binary() be responsible for retrieving the secrets from the secret management system, setting the appropriate environment variables, and then invoking the original bazel run job.

For example, let’s say you store secrets in Azure Key Vault and you have an authenticated Azure CLI set up (likely using interactive authentication for individual developers and Azure Managed Service Identity in CI). Here’s some example code on how to apply the above technique:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# BUILD
my_push(
    name = "my_push",
    ...
)

sh_binary(
    name = "authenticated_my_push",
    srcs = ["authenticated_my_push.sh"],
    args = ["$(location :my_push)"],
    data = [":my_push"]
)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
#
# authenticated_my_push.sh -- Wraps my_push with code that
# retrieves the correct password from Azure Key Vault

set -euo pipefail

MY_PUSH_EXE=$1

export MY_PUSH_USERNAME=foo
# Retrieve password from Azure Key Vault using az(1)
export MY_PUSH_PASSWORD=$(az keyvault secret show \
    --name "secret_name" --vault-name "vault_name" \
    --query 'value' -o tsv)

$MY_PUSH_EXE

You can then perform an authenticated push using bazel run //:authenticated_my_push.

This technique can be applied to wrap any bazel run action with arbitrary code.