Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: SLSA attestation support #1374

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
365 changes: 365 additions & 0 deletions rfcs/0000-slsa-attestation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
## Problem
The [Supply Chain Levels for Software Artifacts (SLSA)][slsa-dev] is a framework that aims to attest to the integrity of
the build system that produced an artifact. SLSA is under the Open Source Security Foundation (OpenSSF), meaning it's a
sibling project of Sigstore (cosign, rekor, fulcio) and ultimately a Linux Foundation project.

SLSA is divided into a [series of levels][slsa-levels] that provide increasing assurances about the security of the
build system. It's currently divided into 4 levels (0 to 3 where 0 means no SLSA) although there is work on introducing
a fifth.

> Pedantics: technically SLSA is divided into tracks which is then further divided into levels, but since there's
> (currently) only one track, the rest of this document will implicitly refer to the Build track.

A quick rundown on the promises of each level:

0. No SLSA
1. Provenance regarding the build system is generated and accessible
2. Build runs on dedicated infrastructure and provenance is signed via key only available to infrastructure
3. Builds are isolated from other builds and the signing key can only be accessed by the build platform (inaccessible to
user defined build steps)

SLSA v1 defines a schema for build systems to provide [provenance][slsa-provenance] about a snapshot of the system that
is used to build an artifact. This schema records the user-defined inputs (e.g source revision, build env vars),
system-internal inputs (e.g image digests), build dependencies (e.g SBoMs from the build), and metadata about the build
system itself (e.g kpack version, pod name, node name)

[slsa-dev]: https://slsa.dev/
[slsa-provenance]: https://slsa.dev/spec/v1.0/provenance
[slsa-levels]: https://slsa.dev/spec/v1.0/levels

## Outcome

Define how kpack should generate SLSA compliant attestations during the build process, what this attestations should
look like, and how this provenance will be stored and surfaced to the user.

## Actions to take

A new flag(`--generate-slsa-attestation`) and env var (`GENERATE_SLSA_ATTESTATION`) will be introduced in the controller
deployment. If this flag is set to `"true"`, all images built by kpack in the cluster will have an attestation attached
to it.

### Attestation format

The proposed format for SLSA provenance generated by kpack is the [SLSA recommended suite][slsa-recommended-suite]:

#### Provenance schema

Build metadata and information is stored using the [SLSA provenance schema][slsa-schema]:

- `buildDefinition`:
- `buildType`: Must be `https://github.com/buildpacks-community/kpack/blob/${KPACK_VERSION}/docs/slsa.md`. According
to SLSA, this should resolve to a human readable spec that documents the build definition and run details. The link
should point to the version of the doc that has been compiled into the controller via `cmd/version.go`.
- `externalParameters`: This should be the JSON serialized contents of the `.spec` field from the Build resource.
- `internalParameters`:
- All the image values: `buildInitImage`, `buildInitWindowsImage`, `buildWaiterImage`, `completionImage`,
`completionWindowsImage`, `lifecycleImage`, and `rebaseImage`
- All the controller flags: `enablePriorityClasses`, `maximumPlatformApiVersion`, `injectedSidecarSupport`,
and `insecureSshTrustUnknownHosts`
- `resolvedDependencies`:
- The source URI and the sha
- The builder image, its digest, and the `"id": "version"` of all the buildpacks as annotations
- `runDetails`:
- `builder`:
- `id`: Either `https://kpack.io/signed-build` or `https://kpack.io/unsigned-build` depending on if a signing secret
is found.
- `version`: This should contain versions of kpack and its components
- `kpack`: version of the controller as compiled into `cmd/version.go`
- `lifecycle`: version of the lifecycle as read from the lifecycle image metadata
- `builderDependencies`:
- Namespace where the Build occured in.
- Build (name and resource version only) resource that generated the build pod.
- ServiceAccount (name and resource version only) associated with the Build.
- Secrets (name and resource version only) used by the Build.
- `metadata`
- `invocationID`: Follows the format `https://kpack.io/<namespace>/<build_name>/<build_pod_name>@<build_pod_node_name>`
- `startedOn`: Time the build pod was created in RFC3339 format.
- `finishedOn`: Time the build pod (or completion container) finished in RFC3339 format.
- `byproducts`: Empty array for now, but may contain things like link to SBoMs in the future.

Example:
```json
{
"buildDefinition": {
"buildType": "https://github.com/buildpacks-community/kpack/blob/v0.12.2/docs/slsa.md",
"externalParameters": {
"builder": {
"image": "my.registry.com/my/builder@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73"
},
"cache": {
"volume": {
"persistentVolumeClaimName": "test-cache"
}
},
"env": [
{
"name": "BP_GO_TARGETS",
"value": "./cmd/controller"
}
],
"resources": {},
"runImage": {
"image": "index.docker.io/paketobuildpacks/run-jammy-base@sha256:e817bca35911221677b678bf8bf29a18c17ce867b29bd9d0b0c3342c063854e5"
},
"serviceAccountName": "default",
"source": {
"git": {
"revision": "82cb521d636b282340378d80a6307a08e3d4a4c4",
"url": "https://github.com/buildpacks-community/kpack"
}
},
"tags": [
"my.registry.com/my/app",
"my.registry.com/my/app:b1.20231108.210915"
]
},
"internalParameters": {
"builderImage": "my.registry.com/my/builder@sha256:de9964b5f501a77b8cf549659f81e29dbac4f8df7f1890ddc2b568dbed428b73",
"buildInitImage": "gcr.io/cf-build-service-public/kpack/build-init@sha256:1791a66ce28ab4ac707185a088b42e990088e0d824f8688056c60fee511109c6",
"buildInitWindowsImage": "gcr.io/cf-build-service-public/kpack/build-init-windows@sha256:6b0093996e5db36580843ca394c249acc19883375c7d7b925b2bed5d8a64bd8a",
"buildWaiterImage": "gcr.io/cf-build-service-public/kpack/build-waiter@sha256:e80c75eff83fc530701e82f5fa6aecff8f09dfc8f64ae7b0c155bd42504d5437",
"completionImage": "gcr.io/cf-build-service-public/kpack/completion@sha256:082433ee02227d8cb77380fa24e1ee09a878d822232b74f822f6cbd6fc059876",
"completionWindowsImage": "gcr.io/cf-build-service-public/kpack/completion-windows@sha256:15171135840442d4e9a8c9c64e97efefc698584ee6299d2de6a8fcafe7ade69e",
"lifecycleImage": "gcr.io/cf-build-service-public/kpack/lifecycle@sha256:0b1cd35012f7152053c42e0d6835cbb5b7c9c24207a0627f556bd931e678f8d7",
"rebaseImage": "gcr.io/cf-build-service-public/kpack/rebase@sha256:c0b3bbcf55750ddda69c48b1b0554470536af05556a0d639f8eb65e4a6265745",
"enablePriorityClasses": "false",
"maximumPlatformApiVersion": "",
"injectedSidecarSupport": "false",
"insecureSshTrustUnknownHosts": "true"
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/buildpacks-community/kpack@refs/heads/main",
"digest": {
"sha1": "759bd290a5310bef5069385b649883e586024dae"
}
},
{
"uri": "paketobuildpacks/builder-jammy-tiny",
"digest": {
"sha256": "78d74bd1c27f633341045f1c5f7f33209f6af0a5dc5700fdfd71200b5b5a0b9a"
},
"annotations": {
"paketo-buildpacks/go-build": "2.1.0",
"paketo-buildpacks/go-mod-vendor": "1.0.25",
"paketo-buildpacks/node-engine": "3.0.1",
"paketo-buildpacks/go-dist": "2.4.2",
"paketo-buildpacks/yarn": "1.2.0",
"paketo-buildpacks/liberty": "3.8.9",
"paketo-buildpacks/bellsoft-liberica": "10.4.2",
"paketo-buildpacks/spring-boot": "5.27.5",
"paketo-buildpacks/google-stackdriver": "8.0.3",
"paketo-buildpacks/ca-certificates": "3.6.6",
"paketo-buildpacks/ca-certificates": "3.6.5",
"paketo-buildpacks/apache-tomee": "1.7.7",
"paketo-buildpacks/apache-tomcat": "7.13.15",
"paketo-buildpacks/azure-application-insights": "5.17.1",
"paketo-buildpacks/procfile": "5.6.7",
"paketo-buildpacks/procfile": "5.6.6",
"paketo-buildpacks/java-memory-assistant": "1.4.8",
"paketo-buildpacks/datadog": "4.3.0",
"paketo-buildpacks/encrypt-at-rest": "4.5.9",
"paketo-buildpacks/maven": "6.15.11",
"paketo-buildpacks/gradle": "7.6.1",
"paketo-buildpacks/sbt": "6.12.9",
"paketo-buildpacks/clojure-tools": "2.8.12",
"paketo-buildpacks/leiningen": "4.6.8",
"paketo-buildpacks/watchexec": "2.8.6",
"paketo-buildpacks/watchexec": "2.8.5",
"paketo-buildpacks/syft": "1.39.0",
"paketo-buildpacks/jattach": "1.4.8",
"paketo-buildpacks/executable-jar": "6.8.2",
"paketo-buildpacks/git": "1.0.7",
"paketo-buildpacks/dist-zip": "5.6.7",
"paketo-buildpacks/environment-variables": "4.5.6",
"paketo-buildpacks/environment-variables": "4.5.5",
"paketo-buildpacks/image-labels": "4.5.5",
"paketo-buildpacks/image-labels": "4.5.4",
"paketo-buildpacks/java": "10.3.1",
"paketo-buildpacks/go": "4.6.0"
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this records the version of buildpacks that was used in the builder, it doesn't record the order. I feel like if you have the builder image's digest it shouldn't matter too much since you're more interested in the integrity of the build rather than reproducing it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect consumers of the SLSA provenance to have access to the builder?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to interpret your question as "do we expect them to have access to the registry that the builder and app images are pushed to" and not "do we expect them to have permissions to pull the builder image". I consider permissions a human/procedural problem and not an infrastructure concern.

If they don't care about build reproducibility then it shouldn't matter, simply recording the digest is enough to verify the integrity of the build.

If they do want to be able to reproduce the build bit-for-bit, then there's really no way to do it without having access to the registry. Because if you want to completely recreate the builder from yaml, then at some point you would need the buildpackage images (and the stack images) which also requires you to have the access to the registry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like if you have the builder image's digest it shouldn't matter too much since you're more interested in the integrity of the build rather than reproducing it.

How do we define the integrity of the build? I asked because this provides the buildpacks under the assumption that the builder digest and the buildpacks are sufficient. But, can't the output of the build be different with different orders of buildpacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the core of my thinking is that anything with a checksum can be treated as a immutable basic dependency and verifying it is outside the scope of SLSA (or at least kpack's involvement in SLSA). And from my understanding, a different order would result in a different image config which would result in a different digest.

To put it differently, I think kpack should treat the builder image as a dependency from an external system, not an artifact that was generated by kpack itself. This is to further emphasize that SLSA should apply to only the Build resource, not the entire system that kpack provides (ClusterStack, Buildpacks, Builder, Image).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the core of my thinking is that anything with a checksum can be treated as a immutable basic dependency and verifying it is outside the scope of SLSA (or at least kpack's involvement in SLSA)

The reason I hold this axiom is that it would get really ugly if we have to start considering "how can I verify that the checksums in the go.sum of my app is correct?"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it a bit more, I think I'm going to generalize the annotations section to be "the labels associated with the builder image". This would save us from doing any special parsing of the image (like figuring out buildpack version or order grouping), while surfacing more metadata (stack id, release date, distro, etc) and being more future-proof.

}
]
},
"runDetails": {
"builder": {
"id": "https://kpack.io/signed-build",
"version": {
"kpack": "0.12.2"
"lifecycle": "0.17.0"
},
"builderDependencies": [
{
"name": "Namespace",
"content": "{\"name\":\"default\",\"resourceVersion\":\"195\"}"
},
{
"name": "Build",
"content": "{\"name\":\"test-build-1\",\"resourceVersion\":\"5\"}"
},
{
"name": "ServiceAccount",
"content": "{\"name\":\"default\",\"resourceVersion\":\"269\"}"
},
{
"name": "Secrets",
"content": "[{\"name\":\"my-registry-creds\",\"resourceVersion\":\"53\"},{\"name\":\"my-git-creds\"}]"
},
chenbh marked this conversation as resolved.
Show resolved Hide resolved
]
},
"metadata": {
"invocationID": "https://kpack.io/default/test-build-1/test-build-1-build-pod@gke-test-default-pool-0582cba3-2dp4",
"startedOn": "2023-11-09T16:47:17.215551-05:00",
"finishedOn": "2023-11-09T16:54:17.215551-05:00"
},
"byproducts": []
}
}
```

#### Statement
The provenance is wrapped in an [in-toto statement][in-toto-statement] and formated as JSON. The `subject.name` and
`subject.digest` should be set to the built image and digest.
```json
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": <provenance from previous step>
"subject": [
{
"name": "gcr.io/cf-build-service-public/kpack/controller",
"digest": {
"sha256": "sha256:5e743affae0df727dce82df3f4fe5256cd66f41d919b3cd2aa91605eea2371f8"
}
}
],
}
```
#### Envelope

The statement is stored in a [DSSE envelope][dsse-envelope], the signatures field may be empty.
```json
{
"payloadType": "application/vnd.in-toto+json",
"payload": <base64 encoded statement base previous step>
"signatures": [
{
"keyid": "default/my-cosign-secret",
"sig": "MEUCIQD/aWTUPVTRhWoGv1jYAvrnmYRJQmHVdy4NrmmLIxUaaAIgSumlFxSX9FG/wfbpYUAQJtE1/vzfVtRXmlr2LwpU670="
}
]
}
```

#### Storage

The attestation is attached to the target image tag (as well as any `additionalTags`) using the
[cosign attestation spec][cosign-attestation], where the envelope becomes a single layer. The attestation image also
follows the [cosign tag-based discovery][cosign-tag] except that the suffix is `.att` instead of `.sig`.

For example,
`gcr.io/cf-build-service-public/kpack/controller@sha256:5e743affae0df727dce82df3f4fe5256cd66f41d919b3cd2aa91605eea2371f8`
will have its corresponding attestation saved to
`gcr.io/cf-build-service-public/kpack/controller:sha256-5e743affae0df727dce82df3f4fe5256cd66f41d919b3cd2aa91605eea2371f8.att`

[slsa-recommended-suite]: https://slsa.dev/attestation-model#recommended-suite
[slsa-schema]: https://slsa.dev/spec/v1.0/provenance#schema
[in-toto-statement]: https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md
[dsse-envelope]: https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md
[cosign-attestation]: https://github.com/sigstore/cosign/blob/main/specs/ATTESTATION_SPEC.md
[cosign-tag]: https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#tag-based-discovery

### Key types

At minimum, the implementation should support the following key types:
- [cosign key][cosign-key-format]. This is to ensure interop with `cosign verify-attestation`.
- ECDSA private keys. This is due to it being the recommended SLSA suite.
- RSA private keys. This is due to it being an extreamly popular key type.
Copy link
Contributor Author

@chenbh chenbh Nov 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only verification tool that works with key-based attestation is cosign, but they don't support non cosign-format keys.

So if there's no (nice) way of verifying ECDSA and RSA key signatures, should we even support it?


Keyless signing via fulcio + rekor will not be part of the initial implementation but may be considered in the future.

[cosign-key-format]: https://github.com/sigstore/cosign/blob/main/doc/cosign_generate-key-pair.md

### Adding provenance generation and signing to build process

Upon a successful completion of the build pod, the build reconciler should look through all the secrets attached to the
Build's ServiceAccount, as well as the secrets attached to the kpack-controller's ServiceAccount. Every key that has the
`type: kubernetes.io/ssh-auth` [built-in secret type][built-in-secret-types], or has the `cosign.key`, `cosign.pub`
fields will be used as signing keys.

Once the provenance has been stamped out according to the template above, these keys will be used to sign the DSSE
envelope. The signatures may be in any arbitrary order, each signature should have the `keyid` set to the
`<namespace>/<secret-name>` that was used.

Any attestation errors (signing errors, push errors, registry errors, etc) should be surfaced to the Build status via a
new condition.

[built-in-secret-types]: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types

### Documentation needed

A core part of SLSA is proper documentation detailing the expected provenance and the security levels a build system
offers. kpack's SLSA documentation must be in `docs/slsa.md` and contain a breakdown of all the fields in the provenance
similar to the [provenance section](#provenance-schema).

Example of documentation for [GitHub Actions][gha-doc] and [Google Cloud Build][gcb-doc].

[gha-doc]: https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1
[gcb-doc]: https://github.com/slsa-framework/gcb-buildtypes/tree/main/triggered-build/v1

## Complexity

One major implementation detail is if the attestation generation and/or signing should occur in the build pod or in the
controller. If it's part of the build pod, it would occur in the completion step after the build metadata is read. If
it's part of the controller, it would occur after the build pod has completed.


### Attestation in controller
Pros:
- Can configure a cluster-wide signing secret
- Easier access to `internalParameters` (i.e. passing all the internal parameters as flags to the completion step is
ugly)

Cons:
- Writing to registry will block reconcilation of other builds
- Failing to sign or push attestation can only be surfaced on the build status, not in the logs


#### Attestation in build pod
Pros:
- No additional scaling problems
- Easier access to SBoM if we expand on this in the future
- May tap into future improvements by the CNB lifecycle
- Any errors can be written to the build log, which is the first place users will look

Cons:
- Much harder to support a cluster-wider signing secret (since the private key might need to be shared across
namespaces)
- Theoretically a cluster with injected sidecars can intercept and change the config (and hence attestation) of the
build pod

## Prior Art

- Docker: https://docs.docker.com/build/attestations/slsa-definitions/
- Tekton: https://tekton.dev/docs/concepts/supply-chain-security/#how-does-tekton-chains-work
- GitHub Actions: https://github.blog/2022-04-07-slsa-3-compliance-with-github-actions/

## Alternatives

There's really no alternative, SLSA is designed as a way for the build platform to attest to the integrity of the build
process and the system itself.

## Risks

- SLSA is relatively early in its life, while it's part of the Open Source Security Foundation (OpenSSF), industry
adoption is still getting started.

## Additional considerations and open questions
- Should the signing key secrets require an additional annotation instead of just matching on the type/field? This would
bring more inline with the [Cluste wide cosign RFC][cluster-wide-cosign-rfc]
- Currently there's no tools on the market that can actually verify generic key-based attestations.
[slsa-verifier][slsa-verifier] uses keyless signing and ony supports GitHub Actions and Google Cloud Build,
`cosign` does key based verification, but only with its own key type.
Comment on lines +512 to +514
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where did we land here? How do we expect users of kpack to validate these attestations?


[cluster-wide-cosign-rfc]: https://github.com/samj1912/kpack/blob/cluster-signing/rfcs/0000-cluster-signing.md#key-generation-and-storage
[slsa-verifier]: https://github.com/slsa-framework/slsa-verifier