Important
Signed pipelines functionality has been built into the Buildkite agent itself making this utility unnecessary. For more information, see the documentation on Signed pipelines. This repo will be archived.
This is a tool that adds some extra security guarantees around Buildkite's jobs. Buildkite security best practices suggest using --no-command-eval
which will only allow local scripts in a checked out repository to be run, preventing arbitrary commands being injected by an intermediary.
The downside of that approach is that it also comes with the recommendation of disabling plugins, or allow listing specifically what plugins and parameters are allowed. This tool is a collaboration between SEEK and Buildkite that attempts to bridge this gap and allow uploaded steps to be signed with a secret shared by all agents, so that plugins can run without any concerns of tampering by third-parties.
Upload is a thin wrapper around buildkite-agent pipeline upload
that adds the required signatures. It behaves much like the command it wraps.
export SIGNED_PIPELINE_SECRET='my secret'
buildkite-signed-pipeline upload
In a global environment
hook, you can include the following to ensure that all jobs that are handed to an agent contain the correct signatures:
export SIGNED_PIPELINE_SECRET='my secret'
if ! buildkite-signed-pipeline verify ; then
echo "Step verification failed"
exit 1
fi
This step will fail if the provided signatures aren't in the environment. The tool allows buildkite-signed-pipeline upload
to be executed without a signature,
this allows the initial upload step to be entered into the Buildkite UI.
Per the examples above, the secret for signing and verification can be provided via an environment variable or command line flag.
This tool also has first-class support for AWS Secrets Manager (AWS SM). A secret id or ARN can be provided, the secret value will then be fetched from AWS SM to be used for signing and verification.
export SIGNED_PIPELINE_AWS_SM_SECRET_ID='arn:aws:secretsmanager:ap-southeast-2:12345:secret:my-signed-pipeline-secret-42a5qP'
buildkite-signed-pipeline upload
Future versions of the tool will add support for secret versioning.
When the tool receives a pipeline for upload, it follows these steps:
- Iterates through each step of a JSON pipeline
- Extracts the
command
orcommands
block - Trims whitespace on resulting command
- Calculates
HMAC(SHA256, command + BUILDKITE_BUILD_ID + canonicalised(BUILDKITE_PLUGINS), shared-secret)
- Add
STEP_SIGNATURE={hash}
to the stepenvironment
block - Pipes the modified JSON pipeline to
buildkite-agent pipeline upload
When the tool is verifying a pipeline:
- Calculates
HMAC(SHA256, BUILDKITE_COMMAND + BUILDKITE_BUILD_ID + canonicalised(BUILDKITE_PLUGINS), shared-secret)
- Compare result with
STEP_SIGNATURE
- Fail if they don't match
Note that in the current version of this tool the secret is symmetric -- it's the same for signing/verifying.
For reference, this tool considers at least the following attack scenarios:
A malicious user gains access to the Buildkite UI (buildkite.com), and updates pipeline settings (adds/modifies a command or plugin)
- Commands cannot be signed without knowing the signing secret ✅
- Only pipelines from your repositories will be signed ✅
- Make sure your agents check
BUILDKITE_REPO
to ensure only known repositories are cloned⚠️
The command (BUILDKITE_COMMAND
) for a job is changed by a man-in-the-middle between Buildkite.com and your agents
- The job signature validation will fail as it will not match the command from the uploaded pipeline ✅
- The job signature validation will fail as it will not match the plugin from the uploaded pipeline ✅
- This tool requires that jobs with plugins are signed, regardless of the allowed command ✅
- If this vector is a concern, you can pin plugins to commit hashes vs versions
⚠️
- This tool will not help in this scenario ❌
- With the right signing secret, any
command
/plugins
combination can be signed (and thus trusted by your agents) ❌
- This tool will not help in this scenario ❌
This is using Golang's 1.11 modules.
export GO111MODULE=on
go run .