The Build Your Own Builder (BYOB) framework makes it simple to make an existing GitHub Action SLSA3 compliant. Instead of handling the complexity around reuseable workflows, signing, intoto, Sigstore, etc, you can simply delegate orchestration and provenance generation to the BYOB framework.
The diagram below depicts the different components of the BYOB framework. We'll cover the different portions in our overview.
The Project Workflow (PW) is hosted in the repository that wants to build an artifact. As part of a build, the PW invokes the SLSA compliant builder defined by the Tool Reuseable Workflow (TRW):
- uses: npm/builder/.github/workflows/[email protected]
The example snippet shows the invocation of a builder with path .github/workflows/slsa.3.yml
from the GitHub's npm/builder
repository.
The tool repository hosts the builder invoked by PWs. The tool repository MUST be public. The repository contains two components, the Tool Reuseable Workflow and the Tool Callback Action.
The "Tool Reusable Workflow" (TRW) is the SLSA compliant builder that will "wrap" an existing GitHub Action. End users' PWs invoke the TRW to build their artifacts. The TRW workflow file must be created as part of the integration.
The "Tool Callback Action" (TCA) is the GitHub Action that is invoked by the BYOB framework in an isolated GitHub job. The TCA does the following:
- Sets the environment. For example, if the builder wants to build Go projects, the TCA would install the Go compiler.
- Calls your existing GitHub Action. For example, if the builder wants to make the GoReleaser Action SLSA compliant, the TCA would call the existing
goreleaser/goreleaser-action
after it has set up the environment. - Outputs attestation metadata (name, binaries and hashes) that are used by the framework to generate SLSA provenance.
The slsa-github-generator repository hosts the code for the BYOB framework maintained by the OpenSSF SLSA tooling team. There are two main components you will use for your integration, the SLSA Setup Action and the SLSA Reuseable Workflow.
The setup-generic Action is used to initialize the BYOB framework. It returns a so-called "SLSA token" which is used in later steps:
- uses: slsa-framework/slsa-github-generator/actions/delegator/[email protected]
The SLSA Reuseable Workflow (SRW) acts as the build's orchestrator. It calls the TCA, generates provenance, and returns the provenance to its TRW caller. A TRW would typically call the SRW as follows:
- uses: slsa-framework/slsa-github-generator/.github/workflow/[email protected]
with:
slsa-token: ${{ needs.slsa-setup.outputs.slsa-token }}
In this example, we will assume there is an existing GitHub Action which builds an artifact. The Action does the following:
- Echos the parameters into the artifact.
- Takes a username, password and token to retrieve / push information from a remote registry.
- Releases the built artifact to GitHub releases (similarly to popular release Actions).
- Outputs the name of the built artifact and the status of the build.
See the full action.yml.
Some more advanced topics are ommitted for clarity, but can be found in the Section: Hardening. Once you have completed this example and the provenance example, we recommend following the steps in Section: Hardening, as it represents best security practice.
Only the following event types are supported as recommended by the SLSA specifications:
Supported event type | Event description |
---|---|
create |
Creation of a git tag or branch. |
release |
Creation or update of a GitHub release. |
push |
Creation or update of a git tag or branch. |
workflow_dispatch |
Manual trigger of a workflow. |
pull_request
events are currently not supported. If you would like support for
pull_request
, please tell us about your use case on
issue #358. If
you have an issue related to any other triggers please submit a
new issue.
The first step for our integration is to create our TRW file and define its inputs. The inputs should mirror those of the existing Action above that we want to make SLSA compliant.
Inputs that have low entropy are defined under the inputs section. Unlike Action inputs, you may define the type (boolean, number, or string) of each input. You may also provide a default value. The inputs will be attested to in the generated provenance. We will discuss in Section: SRW Setup how to redact certain inputs that might be sensitive, such as username, from the provenance.
We also declare an additional rekor-log-public boolean input. Given that the name of the repository will be available in the provenance and will be uploaded to the public transparency log, we need users to acknowledge that they are aware that private repository names will be made public. We encourage all TRWs to define this option. For public repositories, the value of the input is set to true by default by the SRW. For private repositories, users should set if to true when calling the TRW.
Unlike Actions, secrets are defined under a separate secrets section.
Secrets should only be high-entropy values. Do not set username or other low-entropy PII as secrets, as it may intermittently fail due to this unresolved GitHub issue. Secrets may be marked as optional. Unlike for Actions, secrets cannot have default values. In our example, the token secret has no default value, whereas the original Action had one. We will see in Section: Invocation of Existing Action how to set default values in the TCA.
The outputs from the TCA may be returned to the PW as well. To do this, use the outputs section to define the artifact and the status. Our example uses additional outputs to provide metadata about the built artifacts and their provenance. We will discuss them in Section: Upload Attestations.
One key difference between the Action and reusable workflow is isolation. The SRW runs on a different VM than the TRW; and the TRW runs on a different VM from the PW. This means that the artifact built by the TCA (which is managed by the SRW) is not accessible directly by the TRW. The SRW needs to share these files with the TRW; which may also share them with the PW. How to handle this isolation is discussed in Section: SRW Setup. The TRW outputs provides the metadata necessary to download these files, and we will discuss them in Section: Upload Attestations.
Our next step is to initialize the SRW framework. To do this, the TRW must invoke the setup-generic Action. The relevant code calls the SSA as follows:
uses: slsa-framework/slsa-github-generator/actions/delegator/[email protected]
with:
slsa-workflow-recipient: "delegator_generic_slsa3.yml"
slsa-rekor-log-public: ${{ inputs.rekor-log-public }}
slsa-runner-label: "ubuntu-latest"
slsa-build-action-path: "./internal/callback_action"
slsa-workflow-inputs: ${{ toJson(inputs) }}
slsa-workflow-masked-inputs: username
Let's go through the parameters:
slsa-workflow-recipient
is the name of the SRW we are initializing. This is the workflow that we will call to run the build in our example.slsa-rekor-log-public
is simply the same as the TRW'sslsa-rekor-log-public
input, so we just set the value with the TRW's value.slsa-runner-label
is the runner label to run the build on. We currently only support ubuntu runners, but we will add support for other runners in the future.slsa-build-action-path
is the path to our TCA, relative to the root of the repository.slsa-workflow-inputs
are the inputs to the TRW, which the provenance will attest to. These inputs are also provided to the TCA by the BYOB framework.slsa-workflow-masked-inputs
is a list of comma separated field names that are redacted from the generated SLSA provenance. In this example, we're telling the TRW that the username input should be redacted. Any TRWsecrets
are separate frominputs
and thus are automatically excluded from the provenance.
Once we have initialized the SRW, we call the SRW:
slsa-run:
needs: [slsa-setup]
permissions:
id-token: write # For signing.
contents: write # For asset uploads.
packages: write # For package uploads.
actions: read # For the entrypoint.
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
slsa-token: ${{ needs.slsa-setup.outputs.slsa-token }}
secrets:
secret1: ${{ inputs.password }}
secret2: ${{ inputs.token }}
In addition to the token, we also provide the secrets. Up to 15 secrets are supported. Secrets are simply passed to the TCA. They are not included in provenance.
The call that we constructed in Step 3 will run the SRW and invoke the callback Action, which we will define in this step. The Action code is available under internal/callback_action.
The inputs to the TCA are pre-defined, so you just have to follow their definition:
slsa-workflow-inputs
contains a JSON object with a list of key-value pairs for the inputs provided by the TRW to the SSA during initialization.slsa-layout-file
is a path to a file where we will write a layout for generating the attestation.slsa-workflow-secretX
, where X is the number '1' to '15'. These contain the secrets that the TRW provides to the SRW during invocation. Unused secrets should be clearly marked as unused.
We declare the same outputs as the existing Actions. These outputs are made available to the TRW by the BYOB framework. They may be returned by the TRW to the PW.
We invoke the existing Action by its path and pass it the inputs by extracting them from the slsa-workflow-inputs
argument:
uses: ./../__TOOL_CHECKOUT_DIR__
id: build
with:
artifact: ${{ fromJson(inputs.slsa-workflow-inputs).artifact }}
content: ${{ fromJson(inputs.slsa-workflow-inputs).content }}
username: ${{ fromJson(inputs.slsa-workflow-inputs).username }}
password: ${{ inputs.slsa-workflow-secret1 }}
token: ${{ inputs.slsa-workflow-secret2 || github.token }}
Note that the ./../__TOOL_CHECKOUT_DIR__
is the path where the TRW repository is checked out by the BYOB framework, so it's accessible locally. You can then call your existing action at the path ./../__TOOL_CHECKOUT_DIR__/path/to/action
where /path/to/action
is the path to your action's action.yml
relative to the repository root. In the above example, we are assuming our action.yml
is defined in the repository root.
Notice how we populate the token field: If the user has not passed a value to inputs.slsa-workflow-secret2
, we default to using the GitHub token github.token
.
The last thing to do in the TCA is to generate the metadata layout file to indicate to the BYOB platform which files to attest to, and which attestations to generate. You can ask the platform to generate several attestations, each attestating to one or more artifacts. The snippet below indicates a single attestation attesting to a single built artifact my-artifact
. When the BYOB framework generates the attestation, it will add the .build.slsa
extension to it.
{
"version": 1,
"attestations": [
{
"name": "my-artifact",
"subjects": [
{
"name": "my-artifact",
"digest": {
"sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"
}
}
]
}
]
}
In a final "publish" job of the TRW, we download the attestations and do whatever we'd like with them. In our example, we simply print the filename. You may instead upload them to a GitHub release, a registry, etc.
You may want to return the attestation to the PW in case end-users want to publish the artifacts and attestations themselves. If you do so, we encourage you to create a secure-download-attestation Action for your users under a download folder in your repository. This will improve user experience as they won't have to be aware of the SLSA repository and its framework.
The PW workflow will call your builder as follows:
jobs:
build:
permissions:
id-token: write # For signing
contents: read # For asset release.
actions: read # For getting workflow run info.
uses: laurentsimon/byob-doc/.github/workflows/[email protected]
with:
artifact: my-artifact
content: "hello world"
secrets:
password: ${{ secrets.PASSWORD }}
TODO
If you've made it thus far, congratulations! You have built a SLSA3 compliant builder. In this section, we provide additional guidance and tips to harden your implementation.
In the example of Section: Integration Steps, we assumed that the existing Action released assets on GitHub. This is a common feature across build / release Actions. Depending on the use case, this requires the Action to have access to:
contents: write
: token permissions: to upload GitHub assets to GitHub releases. This also grants the Action the ability to push code to the PW repository.packages: write
: to upload a package on GitHub registry.secrets
: used to log into a registry to publish a package
Building an artifact or a package includes downloading dependencies. Every once in a while, dependencies built into a final package may turn out to be malicious. In these rare cases, the PW maintainers and its downstream users will start an incidence response to determine what systems may have been compromised by a rogue dependency. In certain ecosystems like npm or python, dependencies may run arbitrary code as part of the build process, which means they have access to sensitive passwords and the permissions granted to the TCA. To reduce the consequences of a rogue dependency, we recommend following the principle of least privilege, and only give the minimal permissions to the TCA. Let's see how to update our initial integration to do that.
The first thing to do is to use a "low permission SRW". The SRW we used in our original integration is delegator_generic_slsa3.yml, which calls the TCA with the permissions for pushing release assets and publishing packages. In order to reduce the number of permissions the TCA is called with, we recommend you use delegator_lowperms-generic_slsa3.yml instead. This workflow does not give the TCA the dangerous permissions above, and only gives it contents: read
for repository read access. To update your integration:
- Update the
slsa-workflow-receipient
argument to the SSA todelegator_lowperms-generic_slsa3.yml
. - Update your SRW call to use delegator_lowperms-generic_slsa3.yml.
- Update the permissions you pass to the SRW, by removing
packages: write
and updating the contents permission tocontents: read
.
The next thing to do is to not upload the asset to the GitHub release within the existing Action and update the TCA to securely share the built artifacts with the TRW. (The TRW will later be updated to publish the artifacts). To update the TCA:
- Generate a random value to uniquely name your artifact. This is necessary to avoid name collisions if multiple builders run concurrently. This could be concurrent runs of your builder, or someone else's builder.
- Create a folder with all the generated artifacts. In our case, we build a single artifact.
- Securely share the built artifacts with the TRW. For this you need to use the secure-upload-folder Action. This Action uploads the entire folder and returns the sha256 digest as its output, which we will use during download. It's important to note that the "artifact name" refers to the unique name given to the object shared with the TRW, and can be different from the artifact filename our TCA built.
- Add outputs to return the name and digest of the uploaded artifact.
Now we need to download the artifact and publish it from the TRW. To do that, follow these steps:
- Download the artifacts uploaded by the TCA.
- Publish the artifacts.
It is important you follow best development practices for your code, including your TRW, TCA and existing Action. In particular:
- Harden your CI, e.g., set your top-level workflow permissions to
read-only
. - Pin your depenencies by hash except the delegator workflow, to avoid dependency confusion attacks and speed up incidence response.
- If you download binaries, verify their SLSA provenance before running them. Use the
installer
action to install and useslsa-verifier
. - Install or use a tool like OSSF Scorecard to verify you're comprehensively looking at your SDLC.