Closed ianlewis closed 2 years ago
If you can provide some suggestions on what such policies should check for, I can create and contribute them from Kyverno's side.
@chipzoller I think that, for starters, we need an example policy that verifies the invocation.configSource.uri
is a URI for the user's repo with the expected branch.
Some other nice to haves would be:
invocation.configSource.entryPoint
is the user's expected workflow.builder.id
is a URI for the generic workflow.I'm working on figuring out how verification will work when using the generic workflow w/ containers. Based on the Kyverno docs it seems like Kyverno supports a verifyImage
rule that can verify image signatures or image provenance and that it verifies it in whatever format cosign attest
saves it in.
I'm guessing that a user's GH Actions workflow would look something like the following:
RepoDigest
to the generic workflowcosign upload blob
...but I need to check what the cosign attest
API does and if I can mimic it with the provenance generated by the generic workflow and cosign upload blob
.
Take a look at the work I've already done here. The first of three attestations happens here. When using cosign attest
on an image, as you've noted in another issue, it'll set the subject to something like
"subject": [
{
"name": "ghcr.io/chipzoller/zulu",
"digest": {
"sha256": "5365f368065f3e93629b71b404d75c03154be84ed9853a2ea6739d6994a1993e"
}
}
],
You therefore want all attestations to have the same subject, and that's why I'm stripping just the raw predicate out of the in-toto attested form here. If that isn't done, the subjects will be different on account of the generic provenance generator producing a different subject. You can see here where I even customize the input name (for experimentation) knowing that it'll get stripped and replaced in the later step.
So, ideally, whenever the provenance generator for container images is made available, it should produce either the raw predicate (unattested form) allowing to be used as an input to cosign attest --type slsaprovenance
, and/or in in-toto attested form the subject should be in alignment with what cosign would itself set if it handled the attestation.
All the below Kyverno policies are set to enforce
mode under spec.validationFailureAction
meaning if the validation does not pass, it will block the creation or update of the Pod. Also, by default, these policies will work similarly for Pod controllers (Deployments, DaemonSets, StatefulSets, Jobs, and CronJobs) as they do for "bare" Pods. No other work is required. These policies will also resolve mutable tags to immutable digest format. Please see comment blocks for portions of the policies which need user customization.
Example policy that verifies the invocation.configSource.uri
is a URI for the user's repo with the expected branch.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-slsa-attestations
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
rules:
- name: check-uri-keyless
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
# Replace with your image. Wildcard values are supported.
- "myreg.org/path/repo:*"
attestors:
- entries:
- keyless:
# In the case of GitHub Actions, the subject will be set to your Actions workflow responsible
# for kicking off the process. Note that this will not be set to the external action
# responsible for the provenance generation.
subject: "https://github.com/myname/myrepo/.github/workflows/my-workflow.yaml@refs/heads/mybranch"
issuer: "https://token.actions.githubusercontent.com"
attestations:
- predicateType: https://slsa.dev/provenance/v0.2
conditions:
- all:
# The invocation.configSource.uri in the attestation will be set to the repository
# and branch where your workflow, in the case of GitHub Actions, is located.
# Replace with your values.
- key: "{{ invocation.configSource.uri }}"
operator: Equals
value: "git+https://github.com/myname/myrepo@refs/heads/mybranch"
Check that invocation.configSource.entryPoint
is the user's expected workflow.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-slsa-attestations
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
rules:
- name: check-entrypoint-keyless
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
# Replace with your image. Wildcard values are supported.
- "myreg.org/path/repo:*"
attestors:
- entries:
- keyless:
# In the case of GitHub Actions, the subject will be set to your Actions workflow responsible
# for kicking off the process. Note that this will not be set to the external action
# responsible for the provenance generation.
subject: "https://github.com/myname/myrepo/.github/workflows/my-workflow.yaml@refs/heads/mybranch"
issuer: "https://token.actions.githubusercontent.com"
attestations:
- predicateType: https://slsa.dev/provenance/v0.2
conditions:
- all:
# The invocation.configSource.entryPoint in the attestation will be set to the full
# path to your workflow. With GitHub Actions, this will begin with .github/
# Replace with your values.
- key: "{{ invocation.configSource.entryPoint }}"
operator: Equals
value: ".github/workflows/my-workflow.yaml"
builder.id
is a URI for the generic workflow.apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-slsa-attestations
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
rules:
- name: check-builder-id-keyless
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
# Replace with your image. Wildcard values are supported.
- "myreg.org/path/repo:*"
attestors:
- entries:
- keyless:
# In the case of GitHub Actions, the subject will be set to your Actions workflow responsible
# for kicking off the process. Note that this will not be set to the external action
# responsible for the provenance generation.
subject: "https://github.com/myname/myrepo/.github/workflows/my-workflow.yaml@refs/heads/mybranch"
issuer: "https://token.actions.githubusercontent.com"
attestations:
- predicateType: https://slsa.dev/provenance/v0.2
conditions:
- all:
# This expression uses a regex pattern to ensure the builder.id in the attestation is equal to the official
# SLSA provenance generator workflow and uses a tagged release in semver format. If using a specific SLSA
# provenance generation workflow, you may need to adjust the first input as necessary.
- key: "{{ regex_match('^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v[0-9].[0-9].[0-9]$','{{ builder.id}}') }}"
operator: Equals
value: true
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-slsa-attestations
spec:
validationFailureAction: enforce
webhookTimeoutSeconds: 30
rules:
- name: check-all-keyless
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
# Replace with your image. Wildcard values are supported.
- "myreg.org/path/repo:*"
attestors:
- entries:
- keyless:
# In the case of GitHub Actions, the subject will be set to your Actions workflow responsible
# for kicking off the process. Note that this will not be set to the external action
# responsible for the provenance generation.
subject: "https://github.com/myname/myrepo/.github/workflows/my-workflow.yaml@refs/heads/mybranch"
issuer: "https://token.actions.githubusercontent.com"
attestations:
- predicateType: https://slsa.dev/provenance/v0.2
conditions:
- all:
- key: "{{ invocation.configSource.uri }}"
operator: Equals
value: "git+https://github.com/myname/myrepo@refs/heads/mybranch"
- key: "{{ invocation.configSource.entryPoint }}"
operator: Equals
value: ".github/workflows/my-workflow.yaml"
- key: "{{ regex_match('^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v[0-9].[0-9].[0-9]$','{{ builder.id}}') }}"
operator: Equals
value: true
For any of the above policies, if a user wishes to verify them with key-based signing as opposed to keyless, they can simply substitute the attestors[].entries[].keyless
block with the keys[]
block below. There are no modifications needed to the attestations
object.
- keys:
publicKeys: |
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2kCXNpRME7TtCbzLNyRhdZY5JvmC
sAeNYYYg5LYpLdYjqtdfly54yEevhBGQUy0vn/tbSOptTnUNcNgnNaBlPg==
-----END PUBLIC KEY-----
@chipzoller Thanks! Those policies look good! I totally forgot about checking the subject from the cert. It's good you added that. Could you create these as all just one ClusterPolicy
with multiple rules rather than three different objects?
As for the subjects, since they are an input into the generic workflow we should be able to set that to the correct value I would think. Does the base64-subjects
you create here not work? Off hand, I think you need to strip the part to the left of the '@' from $IMAGE
before you put it in the base64-subjects
. i.e. echo $IMAGE | cut -d '@' -f1
# gcr.io/my-project/app@sha256:6e398316742b7aa4a93161dce4a23bc5c545700b862b43347b941000b112ec3e
# becomes
# 6e398316742b7aa4a93161dce4a23bc5c545700b862b43347b941000b112ec3e gcr.io/my-project/app
BASE64_DIGEST=$(echo "$(echo -n $IMAGE | cut -d ':' -f2) $(echo $IMAGE | cut -d '@' -f1)" | base64 -w0)
Once you do that I think you should be able to use the attestation returned from the generic workflow as is. So you wouldn't need to strip the predicate out or run cosign attest
which would sign it over again. Rather you should be able to just use cosign upload blob
to upload the attestation file as is. I still haven't tested it yet but I think it should work. I'm just not totally sure yet if there is anything special cosign does when uploading attestations (vs. sbom etc.). It might need a specific <blob ref>
name or something.
@chipzoller BTW, it's still buggy and not working yet but the workflow I had been using for testing is here. This is kind of what I was thinking in terms of an example user workflow. https://github.com/ianlewis/actions-test/blob/main/.github/workflows/generic-container.yml
Could you create these as all just one
ClusterPolicy
with multiple rules rather than three different objects?
I can, or this can just be one rule with multiple of those expressions falling under the attestations[].conditions[].all[]
block. If the intent is that all of these belong together, then one rule would do it. EDIT: See policy number four above for a single ClusterPolicy with single rule having all three checks.
As for the subjects, since they are an input into the generic workflow we should be able to set that to the correct value I would think. Does the
base64-subjects
you create here not work?
It does work, but it just sets the subject as the following:
"subject": [
{
"name": "ghcr.io/chipzoller/zulu@sha256:5365f368065f3e93629b71b404d75c03154be84ed9853a2ea6739d6994a1993e",
"digest": {
"sha256": "5365f368065f3e93629b71b404d75c03154be84ed9853a2ea6739d6994a1993e"
}
}
]
I know this can be adapted by emulate what cosign attest
places there, I just hadn't messed with it. I can test this out in a bit (without stripping out the predicate after the attestation is created). It'd be nice if the container generator workflow would produce the subject like this so it's consistent with what cosign attest
uses.
Your workflow is basically the same as mine just minus the blob upload part. It would be good to provide an input to the finished container workflow which allows users to control the name of the attestation file that gets uploaded.
It does work, but it just sets the subject as the following:
Yeah, I'll need to check what Kyverno does when validating. It might use the name & digest from the subject, though I'm not sure if that's the right practice. slsa-verifier
hashes the binary and uses the hash to look up the cert via the rekor index instead. I'm not sure what's better and for what reasons tbh. @asraa and @laurentsimon might know more. I think in the case of containers the image name might matter, whereas it doesn't really matter in the case of binary executable files.
In any case, at least for our examples in the docs I think we would want to emulate what cosign attest
does.
Your workflow is basically the same as mine just minus the blob upload part. It would be good to provide an input to the finished container workflow which allows users to control the name of the attestation file that gets uploaded.
I'm not sure that the file name really makes a difference since the container registry isn't really a file server. I think it's more like a blob store with labels and we just need to make sure that it's uploaded with the same tags/labels but I'm not a container registry protocol expert. BTW, some folks kindly pointed out that cosign attach attestation
is the command we need to use. That's what we'll likely use as it will (hopefully) ensure compatibility with cosign attest
.
Note that for keyless, you need to verify that the certificate contains the trusted builder (see image https://github.com/slsa-framework/slsa-github-generator/blob/main/SPECIFICATIONS.md#detailed-steps).
subject: "https://github.com/myname/myrepo/.github/workflows/my-workflow.yaml@refs/heads/mybranch"
should be replaced by the trusted builder. Once we have verified that the OIDC provider is GitHub (as in the example) and the workflow is the trusted builder, we can trust the content of the provenance and use keys
to match on provenance fields.
- key: "{{ regex_match('^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v[0-9].[0-9].[0-9]$','{{ builder.id}}') }}"
is not enough, because it looks up the content of the provenance without verification of the "trusted builder"'s field in the certificate.
Please let me know if that's not making sense.
cosign attach attestation
yes we will use this either via API or via CLI (like we do in the ko PoC)
verification
most of the cosign uses with containers currently do not query rekor and rely on a bundle that contains a signature from rekor. I think it's up to to the verification engine to do this, and our provenance need not change. We could ask the maintainers of Kyverno to add an option if it does not exist.
/cc @asraa
- key: "{{ regex_match('^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v[0-9].[0-9].[0-9]$','{{ builder.id}}') }}" is not enough, because it looks up the content of the provenance without verification of the "trusted builder"'s field in the certificate.
But it does because it will only proceed to verify the provenance if the attestors are verified. And as I stated in the comment for these policies, it is expected that the subject is replaced by the user's "parent" (trusted) builder workflow.
gotcha, sorry I missed the comment then. I only saw
# In the case of GitHub Actions, the subject will be set to your Actions workflow responsible
# for kicking off the process. Note that this will not be set to the external action
# responsible for the provenance generation.
Can you link to the comment?
That is the comment I meant. The subject gets set to the workflow the user has as the parent workflow making the call to the SLSA generator workflow. It doesn't get set to the SLSA workflow. Or am I misunderstanding your comment?
Verifying the user workflow does not prove that the trusted builder was called. We need to verify the certificate field that attests to the trusted builder instead. I was assuming it was "subject", but it may be "Subject Alternative Name"'s URI then (given the image in https://github.com/slsa-framework/slsa-github-generator/blob/main/SPECIFICATIONS.md#detailed-steps).
You can see it used in our verifier in https://github.com/slsa-framework/slsa-verifier/blob/main/pkg/provenance.go#L496
Ok, I see what you mean. Yes, this is a SAN field.
$ COSIGN_EXPERIMENTAL=1 cosign verify ghcr.io/chipzoller/zulu:latest | jq -r .[].optional.Bundle.Payload.body | base64 --decode | jq -r .spec.signature.publicKey.content | base64 --decode | step certificate inspect -
Verification for ghcr.io/chipzoller/zulu:latest --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- Any certificates were verified against the Fulcio roots.
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 522184968399268816520144771357179810204483628558 (0x5b77957518196e2902a8b833417f7c595f90ae0e)
Signature Algorithm: ECDSA-SHA384
Issuer: O=sigstore.dev,CN=sigstore-intermediate
Validity
Not Before: Jun 21 14:00:16 2022 UTC
Not After : Jun 21 14:10:16 2022 UTC
Subject:
Subject Public Key Info:
Public Key Algorithm: ECDSA
Public-Key: (256 bit)
X:
18:0b:78:5d:ca:df:8e:01:e6:d3:fc:9d:bb:34:54:
a8:4a:be:d8:c8:b8:c4:bf:7e:4b:8f:2a:89:ec:76:
77:f3
Y:
55:b1:ff:a7:66:1d:2f:9f:d4:91:23:00:a8:85:0d:
9a:af:d1:b0:b3:99:05:62:43:c7:35:15:e1:35:89:
a7:19
Curve: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 Subject Key Identifier:
C5:66:26:3F:34:96:BF:B6:45:8D:E6:49:A2:80:94:81:6A:3E:12:79
X509v3 Authority Key Identifier:
keyid:DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
X509v3 Subject Alternative Name: critical
URI:https://github.com/chipzoller/zulu/.github/workflows/slsa-generic-keyless.yaml@refs/heads/main
1.3.6.1.4.1.57264.1.1:
https://token.actions.githubusercontent.com
1.3.6.1.4.1.57264.1.2:
push
1.3.6.1.4.1.57264.1.3:
836faeca5cf505bd6d9e35a6564847bd53aafd51
1.3.6.1.4.1.57264.1.4:
slsa-generic-keyless
1.3.6.1.4.1.57264.1.5:
chipzoller/zulu
1.3.6.1.4.1.57264.1.6:
refs/heads/main
RFC6962 Certificate Transparency SCT:
SCT [0]:
Version: V1 (0x0)
LogID: CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=
Timestamp: Jun 21 14:00:16.286 2022 UTC
Signature Algorithm: SHA256-ECDSA
30:46:02:21:00:a4:2b:e3:17:31:1e:aa:51:3c:93:00:0b:09:
36:a7:c5:c7:8d:99:f5:7e:14:40:de:19:10:bb:cf:2e:53:88:
0d:02:21:00:95:93:89:9f:7a:59:d0:cf:a5:eb:12:c4:8d:e8:
98:53:e0:c9:a4:a8:0c:b3:90:1f:70:29:74:37:45:68:c4:d3
Signature Algorithm: ECDSA-SHA384
30:66:02:31:00:98:03:86:aa:a1:a1:79:f1:b5:0a:96:c8:c2:
8c:3f:fc:e7:0f:3a:dc:0a:33:df:24:a6:90:0f:dc:d5:8f:19:
d0:0f:e0:8d:29:c7:e0:be:4b:3f:11:b7:18:12:06:ee:5f:02:
31:00:e4:89:6d:6f:9e:92:bc:a9:15:8c:a1:3e:43:af:c5:e5:
aa:0a:9a:ed:17:d2:1d:d7:ea:9b:2c:a9:33:01:96:80:6e:ba:
16:03:37:60:e9:03:27:08:af:86:6b:4e:f2:bd
Yeah in @chipzoller 's example he's stripping out the predicate and resigning the provenance which I assume updates the subject in the key because in his case cosign is being executed in the context of the user workflow.
BTW, This example for the generic workflow works now. I think I'm going to use this as an example workflow for building and generating provenance for containers using the generic reusable workflow. https://github.com/ianlewis/actions-test/blob/main/.github/workflows/generic-container.yml
I'm planning on testing how the attestation is verified by Kyverno on Monday but I think it should work as advertised.
I just tested by modifying my workflow, and I'm not seeing the provenance returned with a cosign verify-attestations
command. cosign tree
shows there are three attestations present, but the only ones returned by cosign verify-attestations
are those created in my workflow from the cosign attest
command.
Decoded attestation provided below from the prov. generator after modification:
@laurentsimon per comment from @JimBugwadia here, it looks like the subject
field in the Kyverno is actually the contents of that URI
attribute in the SAN.
I just tested by modifying my workflow, and I'm not seeing the provenance returned with a
cosign verify-attestations
command.cosign tree
shows there are three attestations present, but the only ones returned bycosign verify-attestations
are those created in my workflow from thecosign attest
command.
Hmm, I'll need to investigate later 📝
Manual verification may be tricky since I'm using keyless in the generic workflow. cosign verify-attestation
seems to need the key and I'm not sure of an easy way to retrieve it from rekor to verify unless I use slsa-verifier
but that currently only supports file artifacts.
I do see that cosign download attestation
does return all three.
I ended up logging https://github.com/sigstore/cosign/issues/2027 for this.
@chipzoller BTW, I was able to verify an attestation that I created with a local key using cosign verify-attestation
. Were you doing something different?
# have existing SLSA predicate in predicate.json
# create cosign.key and cosign.pub
$ cosign generate-key-pair
# Create attestation
$ cosign attest --predicate predicate.json --no-upload --type slsaprovenance --key cosign.key ghcr.io/ianlewis/actions-test > attestation.intoto.json
# Attach attestation to the image
$ cosign attach attestation --attestation attestation.intoto.jsonl ghcr.io/ianlewis/actions-test
# Verify it's there
$ cosign tree ghcr.io/ianlewis/actions-test
# Verify the attestation
$ cosign verify-attestation --key cosign.pub ghcr.io/ianlewis/actions-test
- The cosign claims were validated
- The signatures were verified against the specified public key
Yes, the problem is with keyless mode.
other examples to provide:
other examples to provide:
- [ ] using ko, jib, etc
We should definitely have an example using ko, jib, etc. Added #947 and #948 for ko and jib examples.
- [ ] using docker-build-and-push Action https://github.com/docker/build-push-action
The current getting started doc demonstrates using build-push-action
.
https://github.com/slsa-framework/slsa-github-generator/blob/fc6500582079e2a1c41f181e7f04189b2a94816f/internal/builders/container/README.md#getting-started
- [ ] using simple docker build commands
I'm not sure I really want to recommend going this route as getting the image digest is not a good experience. build-push-action
is just strictly better. Created #949 to track but I'm not sure we really needed it for the GA milestone.
I think I want to track this on individual issues for each example so I'm going to close this issue.
Outstanding tasks to write examples are tracked in these issues: