├── .gitignore ├── assets ├── workflows.png ├── fulcio.mmd └── fulcio.svg ├── go.mod ├── go.sum ├── Dockerfile ├── renovate.json ├── main.go ├── .github └── workflows │ └── app.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /assets/workflows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liatrio/gh-trusted-builds-app/HEAD/assets/workflows.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/liatrio/gh-trusted-builds-app 2 | 3 | go 1.22 4 | 5 | toolchain go1.22.1 6 | 7 | require github.com/google/uuid v1.6.0 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | COPY main.go go.mod go.sum ./ 6 | RUN go build -o server ./ 7 | 8 | ### 9 | FROM scratch 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=builder /app/server . 14 | 15 | ENTRYPOINT ["/app/server"] 16 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommits", 6 | ":semanticCommitTypeAll(build)", 7 | ":semanticCommitScopeDisabled" 8 | ], 9 | "postUpdateOptions": [ 10 | "gomodTidy" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func main() { 13 | http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { 14 | _, _ = w.Write([]byte("PONG")) 15 | }) 16 | 17 | http.HandleFunc("/id", func(w http.ResponseWriter, r *http.Request) { 18 | id := uuid.New() 19 | _, _ = w.Write([]byte(fmt.Sprintf(`{"id": "%s"}`, id.String()))) 20 | }) 21 | 22 | http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 23 | _, _ = w.Write([]byte("OK")) 24 | }) 25 | 26 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 27 | w.WriteHeader(http.StatusNotFound) 28 | }) 29 | 30 | log.Println("Starting server on :8080") 31 | err := http.ListenAndServe(":8080", nil) 32 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 33 | log.Fatalln("error starting server:", err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/app.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-and-push: 10 | uses: liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@v2.1.0 11 | permissions: 12 | actions: read 13 | id-token: write 14 | contents: read 15 | packages: write 16 | pull-requests: read 17 | security-scan: 18 | needs: [ build-and-push ] 19 | uses: liatrio/gh-trusted-builds-workflows/.github/workflows/scan-image.yaml@v2.1.0 20 | permissions: 21 | id-token: write 22 | contents: read 23 | packages: write 24 | with: 25 | digest: ${{ needs.build-and-push.outputs.digest }} 26 | policy-verification: 27 | needs: 28 | - build-and-push 29 | - security-scan 30 | uses: liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@v2.1.0 31 | permissions: 32 | id-token: write 33 | contents: read 34 | packages: write 35 | with: 36 | digest: ${{ needs.build-and-push.outputs.digest }} 37 | deploy: 38 | needs: 39 | - build-and-push 40 | - policy-verification 41 | uses: liatrio/gh-trusted-builds-workflows/.github/workflows/demo-deploy.yaml@v2.1.0 42 | permissions: 43 | id-token: write 44 | packages: read 45 | contents: read 46 | with: 47 | digest: ${{ needs.build-and-push.outputs.digest }} 48 | -------------------------------------------------------------------------------- /assets/fulcio.mmd: -------------------------------------------------------------------------------- 1 | %% npm install -g @mermaid-js/mermaid-cli 2 | %% mmdc -t dark -i fulcio.mmd -o fulcio.svg -b black 3 | 4 | sequenceDiagram 5 | autonumber 6 | participant CR as Container Registry 7 | participant Rekor 8 | participant Workflow 9 | participant GHA as GitHub Actions Issuer 10 | participant Fulcio 11 | participant CTL as Certificate Transparency Log 12 | Workflow->>Workflow: Build container image 13 | Workflow->>GHA: HTTP request to $ACTIONS_ID_TOKEN_REQUEST_URL with $ACTIONS_RUNTIME_TOKEN 14 | GHA-->>Workflow: ID token response 15 | Workflow->>Workflow: Generate public/private keypair 16 | Workflow->>Workflow: Create CSR or sign subject claim 17 | Workflow->>Fulcio: Signing certificate request 18 | Fulcio->>GHA: OpenID Connect discovery request 19 | GHA-->>Fulcio: OpenID Connect discovery response 20 | Fulcio->>GHA: JWKS request 21 | GHA-->>Fulcio: JWKS response 22 | Note over Fulcio,GHA: Note: these responses would likely be cached 23 | Fulcio->>Fulcio: Verify id token signature & claims 24 | Fulcio->>Fulcio: Create certificate from id token claims 25 | Fulcio->>CTL: Precertificate request for signed certificate timestamp (SCT) 26 | CTL-->>Fulcio: SCT response 27 | Fulcio-->>Workflow: Signing certificate response 28 | Workflow->>CR: Push Image & signature 29 | CR-->>Workflow: Registry response 30 | Workflow->>Rekor: Log entry request to upload signature & signing certificate 31 | Rekor-->>Workflow: Upload response 32 | -------------------------------------------------------------------------------- /assets/fulcio.svg: -------------------------------------------------------------------------------- 1 | Container RegistryRekorWorkflowGitHub Actions IssuerFulcioCertificate Transparency LogNote: these responses would likely be cachedBuild container image1HTTP request to $ACTIONS_ID_TOKEN_REQUEST_URL with $ACTIONS_RUNTIME_TOKEN2ID token response3Generate public/private keypair4Create CSR or sign subject claim5Signing certificate request6OpenID Connect discovery request7OpenID Connect discovery response8JWKS request9JWKS response10Verify id token signature & claims11Create certificate from id token claims12Precertificate request for signed certificate timestamp (SCT)13SCT response14Signing certificate response15Push Image & signature16Registry response17Log entry request to upload signature & signing certificate18Upload response19Container RegistryRekorWorkflowGitHub Actions IssuerFulcioCertificate Transparency Log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gh-trusted-builds-app 2 | 3 | An example of using GitHub Actions [reusable workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) to create and deploy a trusted software artifact. 4 | 5 | ## Background 6 | 7 | These days, companies of all sizes and industries create software, from small scripts to complicated back-of-house inventory forecasting systems. 8 | For both security and compliance reasons, companies want to be able to demonstrate that the software they're using is secure. 9 | One way to achieve this goal is to have a trusted build system that can produce signed statements about the software artifacts it produces. 10 | An authenticated statement like this is called an attestation. 11 | Throughout the lifecycle of the artifact, the build system can produce attestations of different types in order to demonstrate that certain 12 | events occurred. For example: 13 | 14 | - multiple reviewers approved a particular code change 15 | - this container image was built from this code commit 16 | - a container image was scanned for vulnerabilities 17 | 18 | Later on, when the software is deployed, the deployment system can check for these attestations and verify that they meet the company's policy for 19 | software. Any attempted deployments that fail to meet the policy can be rejected, thereby preventing low quality or malicious software from entering the 20 | production environment. 21 | 22 | This repository is a demonstration of using GitHub Actions as that trusted build system. It's part of a number of repositories that you can find using the 23 | [automated-governance](https://github.com/search?q=topic%3Aautomated-governance+org%3Aliatrio&type=repositories) topic: 24 | 25 | - [`liatrio/github-trusted-builds-app`](https://github.com/liatrio/gh-trusted-builds-app): a very simple Go application 26 | - [`liatrio/github-trusted-builds-workflows`](https://github.com/liatrio/gh-trusted-builds-workflows): reusable workflows designed to be used by application teams 27 | - [`liatrio/github-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations): a tool for creating attestations of different types 28 | - [`liatrio/github-trusted-builds-policy`](https://github.com/liatrio/gh-trusted-builds-policy): Rego policy used to evaluate attestations 29 | 30 | ### Scenario 31 | 32 | In this demonstration, multiple teams must approve a software artifact for deployment to production. 33 | As a shorthand, we'll call these teams the "central" teams for their particular domain; as opposed to "application" teams, who are responsible for the development and maintenance of one or more applications. 34 | 35 | First up is the platform team, which is responsible for providing a Kubernetes cluster and deployment tooling. 36 | To facilitate automatic approvals, the platform team maintains a reusable GitHub Actions workflow that builds container images from source and pushes them to GitHub Container Registry. 37 | The images are annotated and signed by the workflow; in addition, a record of the signature is also 38 | uploaded to [Rekor](https://docs.sigstore.dev/rekor/overview/), which is a transparency log for supply chain security. In order to use the workflow, application teams only have to specify the reference to it in a job: 39 | 40 | ```yaml 41 | jobs: 42 | build-and-push: 43 | uses: liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@main 44 | ``` 45 | 46 | The security team also needs to approve which artifacts are deployed to production. 47 | Like the platform team, they provide a reusable workflow that scans container images for vulnerabilities and another workflow that evaluates if an 48 | artifact meets enterprise policy. This latter workflow produces a [verification summary attestation](https://slsa.dev/verification_summary/v1) (VSA) which 49 | indicates if the artifact passed or failed policy. Later on, the platform team can check for the existence of this VSA and approve or deny deployment accordingly. 50 | 51 | ### Technologies 52 | 53 | Before diving into the workflows, it may be helpful to briefly review the core technologies used by the demo. 54 | 55 | #### in-toto Attestations 56 | 57 | Much of this demo is involved with producing attestations, or signed statements about a software artifact. 58 | A popular format for attestations comes from the [in-toto](https://in-toto.io/) project, which is focused on software supply-chain security. 59 | While this demonstration does not use in-toto directly, many open source projects outside in-toto use the in-toto attestation format; this includes all the attestations created in this demo. 60 | 61 | The in-toto format consists of several pieces: 62 | - predicate: structured information about a software artifact. The predicate type determines the structure and type of data available. 63 | - statement: contains the predicate and provides a list of subjects (software artifacts) described by the predicate. 64 | - envelope: the wrapper for the statement and any signatures. 65 | 66 | Here's an example of an in-toto attestation: 67 | ```json 68 | { 69 | "payloadType": "application/vnd.in-toto+json", 70 | "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiZ2hjci5pby9saWF0cmlvL2doLXRydXN0ZWQtYnVpbGRzLWFwcCIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0ZDllNzdmZWRjYTI5MzkzZGZmOWEzOWIwNjVkMTU4YWFhOTY0NGMxOGIyYzUxZjc1ZmNhZjg0YjhjOTQxYWJmIn19XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2xzYS5kZXYvdmVyaWZpY2F0aW9uX3N1bW1hcnkvdjAuMiIsInByZWRpY2F0ZSI6eyJpbnB1dF9hdHRlc3RhdGlvbnMiOlt7ImRpZ2VzdCI6eyJzaGEyNTYiOiJmMTQwZjg4NjA5NzQ5YWQ5OWUwNTgzYzUyZWNkNWNmMTZjMmU1ZTJjNTBjZDYxNDAzNjRhOTkxY2MxYWYyMmRhIn0sInVyaSI6Imh0dHBzOi8vcmVrb3Iuc2lnc3RvcmUuZGV2L2FwaS92MS9sb2cvZW50cmllcz9sb2dJbmRleD0yMTQ0OTgzMSJ9LHsiZGlnZXN0Ijp7InNoYTI1NiI6ImU3Y2M5ODk2OGU5MDJmMDA2M2I5YWQ0OTJiMjJlYzJkMGE2NDdmMzQzZWQxNjg5YzBhMzdjYjNlZjc2ZjljMzEifSwidXJpIjoiaHR0cHM6Ly9yZWtvci5zaWdzdG9yZS5kZXYvYXBpL3YxL2xvZy9lbnRyaWVzP2xvZ0luZGV4PTIxNDQ5ODM0In0seyJkaWdlc3QiOnsic2hhMjU2IjoiOTk1ZWY1ZjYwODkzOGYwM2JmNzQxNWI2NTIzYTVlYzFjYTIwZDFmMWNkMzdkNjUwYzIzM2FlYjM2NThmYjFmYSJ9LCJ1cmkiOiJodHRwczovL3Jla29yLnNpZ3N0b3JlLmRldi9hcGkvdjEvbG9nL2VudHJpZXM/bG9nSW5kZXg9MjE0NDk4NjEifV0sInBvbGljeSI6eyJ1cmkiOiJodHRwczovL2dpdGh1Yi5jb20vbGlhdHJpby9naC10cnVzdGVkLWJ1aWxkcy1wb2xpY3kvcmVsZWFzZXMvZG93bmxvYWQvdjEuMS4xL2J1bmRsZS50YXIuZ3oifSwicG9saWN5X2xldmVsIjoiU0xTQV9MRVZFTF8zIiwicmVzb3VyY2VfdXJpIjoiZ2hjci5pby9saWF0cmlvL2doLXRydXN0ZWQtYnVpbGRzLWFwcCIsInRpbWVfdmVyaWZpZWQiOiIyMDIzLTA1LTIzVDE3OjUyOjE0LjE4NjE2MTE2NVoiLCJ2ZXJpZmljYXRpb25fcmVzdWx0IjoiUEFTU0VEIiwidmVyaWZpZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vbGlhdHJpby9naC10cnVzdGVkLWJ1aWxkcy13b3JrZmxvd3MvLmdpdGh1Yi93b3JrZmxvd3MvcG9saWN5LXZlcmlmaWNhdGlvbi55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9fX0=", 71 | "signatures": [ 72 | { 73 | "keyid": "", 74 | "sig": "MEUCICYn68n2eOij6SLpgnzz1lyrW5dSixGRambvA/625DwiAiEAktVa8wx6mqSYpzzsVWUzcaAZcLsQYs/paYFRJGpSx2o=" 75 | } 76 | ] 77 | } 78 | ``` 79 | 80 | This is the envelope, which contains the in-toto statement in the payload field. Decoding the payload field shows the statement: 81 | 82 |
83 | example in-toto statement (click to expand) 84 | 85 | ```json 86 | { 87 | "_type": "https://in-toto.io/Statement/v1", 88 | "subject": [ 89 | { 90 | "name": "ghcr.io/liatrio/gh-trusted-builds-app", 91 | "digest": { 92 | "sha256": "4d9e77fedca29393dff9a39b065d158aaa9644c18b2c51f75fcaf84b8c941abf" 93 | } 94 | } 95 | ], 96 | "predicateType": "https://slsa.dev/verification_summary/v0.2", 97 | "predicate": { 98 | "input_attestations": [ 99 | { 100 | "digest": { 101 | "sha256": "f140f88609749ad99e0583c52ecd5cf16c2e5e2c50cd6140364a991cc1af22da" 102 | }, 103 | "uri": "https://rekor.sigstore.dev/api/v1/log/entries?logIndex=21449831" 104 | }, 105 | { 106 | "digest": { 107 | "sha256": "e7cc98968e902f0063b9ad492b22ec2d0a647f343ed1689c0a37cb3ef76f9c31" 108 | }, 109 | "uri": "https://rekor.sigstore.dev/api/v1/log/entries?logIndex=21449834" 110 | }, 111 | { 112 | "digest": { 113 | "sha256": "995ef5f608938f03bf7415b6523a5ec1ca20d1f1cd37d650c233aeb3658fb1fa" 114 | }, 115 | "uri": "https://rekor.sigstore.dev/api/v1/log/entries?logIndex=21449861" 116 | } 117 | ], 118 | "policy": { 119 | "uri": "https://github.com/liatrio/gh-trusted-builds-policy/releases/download/v1.1.1/bundle.tar.gz" 120 | }, 121 | "policy_level": "SLSA_LEVEL_3", 122 | "resource_uri": "ghcr.io/liatrio/gh-trusted-builds-app", 123 | "time_verified": "2023-05-23T17:52:14.186161165Z", 124 | "verification_result": "PASSED", 125 | "verifier": { 126 | "id": "https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main" 127 | } 128 | } 129 | } 130 | ``` 131 | 132 |
133 | 134 | This statement links a particular container image (`ghcr.io/liatrio/gh-trusted-builds-app@sha256:4d9e77fedca29393dff9a39b065d158aaa9644c18b2c51f75fcaf84b8c941abf`) to a predicate (`https://slsa.dev/verification_summary/v0.2`). 135 | We'll see more on this predicate type later. For more information about in-toto attestations, the [official specification](https://github.com/in-toto/attestation/tree/main/spec) is a good place to start. 136 | 137 | #### SLSA 138 | 139 | Software supply-chain security has been a growing concern in the past few years. 140 | [SLSA](https://slsa.dev/) (supply-chain levels for software artifacts) is an attempt to codify best practices around producing software artifacts by describing a series of standards and levels that a software artifact can obtain. 141 | A higher level indicates a more secure and robust software delivery process. The SLSA website gives a more [comprehensive overview](https://slsa.dev/spec/v1.0/levels) of the levels. 142 | 143 | The initial focus of SLSA is around how software is built, and the framework describes a [provenance attestation](https://slsa.dev/provenance/v1). Provenance links a software artifact to the source code from which it was created, as well as the method and environment in which the build ran. 144 | The project also provides some tools for creating provenance, like the [`slsa-github-generator`](https://github.com/slsa-framework/slsa-github-generator), which provides ways to create provenance attestations for different artifact types built in GitHub Actions. 145 | This demonstration uses the SLSA container generator to create a provenance attestation. 146 | 147 | #### Open Policy Agent 148 | 149 | Open Policy Agent (OPA) is a set of tools for writing and executing policies in a language called Rego. 150 | The policies are used to authorize some request or event, given input data. For example, this is part of a larger policy used in this demo to check that a pull request had at least one reviewer: 151 | 152 | ```rego 153 | package security.pullrequest 154 | 155 | default allow = false 156 | 157 | allow { 158 | count(violation) == 0 159 | } 160 | 161 | violation[msg] { 162 | count(input.predicate.reviewers) < 1 163 | msg := "pull request reviewers is less than 1" 164 | } 165 | ``` 166 | 167 | This demonstration uses Rego in multiple places: first [`liatrio/github-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations) uses [policy](https://github.com/liatrio/gh-trusted-builds-policy) on 168 | the attestations generated by the workflow in order to produce a [verification summary attestation](https://slsa.dev/verification_summary/v1) (VSA). Later on, when the workflow attempts to deploy to a local Kubernetes cluster, the [Sigstore policy controller](https://docs.sigstore.dev/policy-controller/overview/) 169 | is configured with a Rego policy that checks the attributes of the VSA produced earlier. 170 | 171 | #### GitHub Actions: Reusable Workflows 172 | 173 | GitHub Actions has multiple ways to minimize duplication in a workflow -- one method is to create a [custom action](https://docs.github.com/en/actions/creating-actions/about-custom-actions). 174 | With [composite actions](https://docs.github.com/en/actions/creating-actions/creating-a-composite-action), it's even possible to use wrap multiple external actions in a single reusable unit. 175 | 176 | However, for this demonstration, we're relying on a larger unit -- the [reusable workflow](https://docs.github.com/en/actions/using-workflows/reusing-workflows). 177 | A reusable workflow functions similarly to a composite action, in that it allows you to bundle multiple actions together and pass inputs/outputs between them. 178 | Instead of wrapping individual actions, the workflow can include one or more complete jobs, so we can perform separate tasks in parallel. 179 | In addition, a reusable workflow has different security features that make it desirable over other ways to reduce duplication in GitHub Actions. 180 | 181 | First and foremost is the fact that a reusable workflow runs in a separate virtual machine than other jobs in the caller's workflow. 182 | This is in contrast to an individual action, which always runs in the larger context of a job. 183 | Running on the same virtual machine as the workflow caller makes it difficult to access secrets in a way that doesn't also allow the caller to use them. 184 | It also means that a caller can manipulate the environment in order to do things like capture environment variables or snoop on network traffic. 185 | With reusable workflows, we can eliminate these concerns which would otherwise make it difficult for us to trust the attestations produced by the workflows. 186 | 187 | This demonstration also shows the use of least-privilege access with reusable workflows. 188 | Each reusable workflow is specifically granted the token permissions the caller will allow. 189 | This enables the caller repository to have a safer, read-only default to GitHub token permissions. 190 | 191 | #### GitHub Actions: OpenID Connect 192 | 193 | A GitHub Actions feature that complements reusable workflows is the [OpenID Connect (OIDC) integration](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect), 194 | which allows a job to acquire an [OIDC id token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken), which is a [JSON web token (JWT)](https://jwt.io/) signed by GitHub Actions. 195 | 196 | A JWT is a popular token format that contains three sections: a header, claims, and signature. The header contains metadata about the token, while the claims typically contain information 197 | about the entity described by the token, as well as details needed to validate the token, such as the token issuer and expiry. Finally, the signature encompasses the header and claims, so that by verifying the signature, 198 | we know that we can trust the information in the other two sections. 199 | 200 | In the case of GitHub Actions, the claims in the token describe the running workflow: 201 | 202 | ```json 203 | { 204 | "jti": "79ba45c1-a46f-4605-b68d-8207b5a5287f", 205 | "sub": "repo:liatrio/gh-trusted-builds-app:ref:refs/heads/main", 206 | "aud": "https://github.com/liatrio", 207 | "ref": "refs/heads/main", 208 | "sha": "0c960da8e1b1844d14ac4048b60f2ec892ed21a3", 209 | "repository": "liatrio/gh-trusted-builds-app", 210 | "repository_owner": "liatrio", 211 | "repository_owner_id": "5726618", 212 | "run_id": "4724225373", 213 | "run_number": "22", 214 | "run_attempt": "1", 215 | "repository_visibility": "public", 216 | "repository_id": "627556067", 217 | "actor_id": "9082799", 218 | "actor": "alexashley", 219 | "workflow": "app", 220 | "head_ref": "", 221 | "base_ref": "", 222 | "event_name": "workflow_dispatch", 223 | "ref_type": "branch", 224 | "workflow_ref": "liatrio/gh-trusted-builds-app/.github/workflows/app.yaml@refs/heads/main", 225 | "workflow_sha": "0c960da8e1b1844d14ac4048b60f2ec892ed21a3", 226 | "job_workflow_ref": "liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main", 227 | "job_workflow_sha": "f1e5fbbfa4f750e221038adca773bca75ec38c3d", 228 | "runner_environment": "github-hosted", 229 | "iss": "https://token.actions.githubusercontent.com", 230 | "nbf": 1681753278, 231 | "exp": 1681754178, 232 | "iat": 1681753878 233 | } 234 | ``` 235 | 236 | One claim in particular, `job_workflow_ref`, is instrumental to the security properties of this demonstration; it indicates the workflow path and Git reference that were used in this job. You may also notice the similarly-named `workflow_ref` claim, which is the path to the running workflow definition. 237 | When using a normal workflow, the values of these two claims is the same, but when a reusable workflow is called by another workflow, the value of `job_workflow_ref` will be the path and Git ref of the reusable workflow. 238 | Because the token is signed by GitHub Actions, as long as we validate the token signature and claims, we can trust that only a running instance of the workflow is in possession of the token. 239 | This property allows us to create federated trust in order to access [cloud provider resources](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers) or even request [code-signing certificates](https://github.com/sigstore/fulcio). 240 | 241 | In order to validate tokens, we need to know what keys were used to sign the tokens. GitHub Actions hosts an OpenID Connect provider at https://token.actions.githubusercontent.com, which includes an [OIDC discovery document](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) at the `/.well-known/openid-configuration` endpoint: 242 | 243 | ```json 244 | { 245 | "issuer": "https://token.actions.githubusercontent.com", 246 | "jwks_uri": "https://token.actions.githubusercontent.com/.well-known/jwks", 247 | "subject_types_supported": [ 248 | "public", 249 | "pairwise" 250 | ], 251 | "response_types_supported": [ 252 | "id_token" 253 | ], 254 | "claims_supported": [ 255 | "sub", 256 | "aud", 257 | "exp", 258 | "iat", 259 | "iss", 260 | "jti", 261 | "nbf", 262 | "ref", 263 | "repository", 264 | "repository_id", 265 | "repository_owner", 266 | "repository_owner_id", 267 | "run_id", 268 | "run_number", 269 | "run_attempt", 270 | "actor", 271 | "actor_id", 272 | "workflow", 273 | "workflow_ref", 274 | "workflow_sha", 275 | "head_ref", 276 | "base_ref", 277 | "event_name", 278 | "ref_type", 279 | "environment", 280 | "environment_node_id", 281 | "job_workflow_ref", 282 | "job_workflow_sha", 283 | "repository_visibility", 284 | "runner_environment" 285 | ], 286 | "id_token_signing_alg_values_supported": [ 287 | "RS256" 288 | ], 289 | "scopes_supported": [ 290 | "openid" 291 | ] 292 | } 293 | ``` 294 | 295 | The `jwks_uri` field ([JSON web key set](https://datatracker.ietf.org/doc/html/rfc7517#section-1)) contains a URL that can be used to grab the public keys which are needed to verify the signature on the JWTs issued 296 | by GitHub Actions. 297 | 298 | Finally, it's worth mentioning how a job receives an id token, as it's not one of the standard OAuth2 grants. 299 | In order to acquire an id token, a job must first be configured with the `id-token` permission: 300 | 301 | ```yaml 302 | jobs: 303 | demo: 304 | permissions: 305 | id-token: write 306 | ``` 307 | 308 | Later on, once the job is running, it will have access to the following environment variables: 309 | - `ACTIONS_ID_TOKEN_REQUEST_URL` 310 | - `ACTIONS_RUNTIME_TOKEN` 311 | 312 | The job can then make an HTTP `POST` request to `$ACTIONS_ID_TOKEN_REQUEST_URL` with `$ACTIONS_RUNTIME_TOKEN` in the authorization header (using the [bearer token scheme](https://datatracker.ietf.org/doc/html/rfc6750#section-2.1)), and the job will receive an 313 | id token in the `value` field of the JSON response. 314 | 315 | For a more in-depth overview of using GitHub Actions' OIDC feature with reusable workflows, the SLSA GitHub Generator [specifications](https://github.com/slsa-framework/slsa-github-generator/blob/main/SPECIFICATIONS.md) 316 | go into additional detail. 317 | 318 | #### Sigstore: Cosign 319 | 320 | Cosign is a tool from the [Sigstore project](https://www.sigstore.dev/) for signing and verifying container images, blobs, and attestations. It also supports running [OPA](https://www.openpolicyagent.org/docs/latest/) or [CUE](https://cuelang.org/docs/) policies 321 | against those attestations. In this demonstration, we'll use cosign to sign both a container image and a number of attestations about the image. 322 | 323 | #### Sigstore: Policy Controller 324 | 325 | Another Sigstore project, the [policy controller](https://docs.sigstore.dev/policy-controller/overview/), is used to gate Kubernetes deployments with OPA or CUE policy. 326 | It can verify that images are signed and check the attestations against a supplied policy. We'll use it in this demo to check that our container image has an attestation signed by the right identity. 327 | 328 | #### Sigstore: Rekor Transparency Log 329 | 330 | [Rekor](https://docs.sigstore.dev/rekor/overview/) is an open-source software supply chain tool that’s part of the larger Sigstore project. 331 | It consists of a [transparency log](https://transparency.dev/verifiable-data-structures/#verifiable-log) server and a command line tool to interact with the server. While there is a [public instance](https://rekor.sigstore.dev/), enterprises can also deploy and run Rekor on their own infrastructure. 332 | 333 | Rekor has a number of properties that make it useful as an attestation store; first, its underlying storage is a robust, append-only log – new entries can be added, but old entries cannot be updated or deleted. 334 | Rekor’s implementation also provides the ability to demonstrate that an entry is actually included in the log, which users can verify through what’s called an inclusion proof. 335 | Similarly, by recording certain metadata about the log, users can also verify over time that the log hasn’t been tampered with and that only new entries have been created (through a consistency proof). 336 | 337 | Rekor is used to store container image signatures, along with the public key or certificate that's needed to verify the signature. It can also store [in-toto attestations](https://in-toto.io/), along with a few other [built-in record types](https://docs.sigstore.dev/rekor/sign-upload/). 338 | 339 | #### Sigstore: Fulcio Certificate Authority 340 | 341 | Fulcio is a certificate authority for code-signing -- it issues short-lived (10 minute) certificates that anyone can use to sign images or attestations. 342 | The [official documentation](https://docs.sigstore.dev/fulcio/certificate-issuing-overview/) goes into more detail, but at a high level this is the flow for acquiring a code-signing certificate: 343 | 344 | 1. Acquire an id token from an issuer that Fulcio is configured to trust. This demo uses the GitHub Actions id token 345 | 1. Generate a temporary public/private key pair - `cosign` will do this automatically 346 | 1. Prove that you control the key pair by doing one of the following: 347 | - Create a certificate signing request (CSR) 348 | - Grab the `sub` claim from the id token and sign it 349 | 1. Make a request to Fulcio with the id token and the proof from the previous step 350 | 1. Fulcio will validate the id token and check the provided proof 351 | 1. If the request is valid, Fulcio will issue a code-signing certificate and populate the certificate fields with information from the id token 352 | - When using a GitHub Actions OIDC token, the issuer will be `https://token.actions.githubusercontent.com` and the subject will be the value of the `job_workflow_ref` claim 353 | 1. Depending on the deployment type, Fulcio will also submit a request to a certificate transparency log to include the certificate in the logs. The response from the certificate transparency log will include a signed timestamp that will be added to the certificate. 354 | 355 | After receiving the certificate, `cosign` or other tools can sign images or attestations with the private key generated earlier, and then upload the signature and certificate to Rekor. 356 | Once the signing is finished, the key pair can be discarded, as the certificate will be in Rekor for validation later. 357 | 358 | For a more concrete example, this is a sequence diagram that shows how a GitHub Actions workflow might build & sign a container image: 359 | 360 | ![Fulcio certificate issuance](https://raw.githubusercontent.com/liatrio/gh-trusted-builds-app/main/assets/fulcio.svg) 361 | 362 | Later, when someone tries to validate the signature, they can check that the certificate chains up to the Fulcio root, that the signature happened during the window when the certificate was valid, 363 | and, most importantly, verify that the certificate identity matches the expected signer. 364 | Anyone can use the [Sigstore public good Fulcio instance](https://fulcio.sigstore.dev/) to get a certificate, so it's important that you only trust signatures from identities that you trust. 365 | 366 | #### Software Bill of Materials (SBOM) 367 | 368 | A software bill of materials describes the components that make up a software artifact. 369 | The SBOM includes the dependencies of an artifact and may include licensing information about the artifact and its dependencies. 370 | While an SBOM can be useful to enforce internal licensing standards, it's also valuable for vulnerability analysis; this is especially true when there's a need to retroactively look at software running in production and determine if it's vulnerable to a new attack. 371 | If an organization already has SBOMs for the software it's running, it becomes much easier to tell if there's an impact from a new vulnerability. 372 | 373 | There are several standard formats, like [SPDX](https://spdx.dev/) from the Linux Foundation and [CycloneDX](https://cyclonedx.org/) from OWASP. 374 | Fortunately, there are a number of tools for generating and working with SBOMs and most tools understand multiple standards. In this demonstration, we're using SPDX as the format and [Syft](https://github.com/anchore/syft) as the tool to generate SBOMs. 375 | 376 | ## Workflows 377 | 378 | The demo makes use of several reusable workflows defined in [`liatrio/gh-trusted-builds-workflows`](https://github.com/liatrio/gh-trusted-builds-workflows). 379 | Each workflow is owned by either the platform or security teams. 380 | 381 | ![Workflow run](https://raw.githubusercontent.com/liatrio/gh-trusted-builds-app/main/assets/workflows.png) 382 | 383 | ### Platform: Build & Push 384 | 385 | The platform team's `build-and-push` workflow is split into several jobs: 386 | - `detect-workflow` 387 | - `build` 388 | - `push` 389 | - `sign` 390 | - `source-attestations` 391 | - `provenance` 392 | - `sbom` 393 | 394 | The `build` job uses Docker to build a container image, but doesn't push it to a container registry. 395 | Instead, the job outputs a tar file that will be pushed by the next job. 396 | The reason to split build and push is that the build step is executing untrusted code from the application team; consequently, we don't want that job to be able to request the id token that's used for signing or to try to push a malicious image. 397 | It's also often the case that organizations will use a central registry for all teams, and the authenticated machine identity is likely to have access to more repositories than the one used by the application image. 398 | In that case, the `build` job could even push images to other repositories; while the authentication in this demo is scoped to the `liatrio/gh-trusted-builds-app` repository, it's still important to separate these steps for those reasons. 399 | 400 | So the `build` job only has the permissions it needs in order to checkout the repo: 401 | 402 | ```yaml 403 | jobs: 404 | build: 405 | permissions: 406 | contents: read 407 | ``` 408 | 409 | Next up is the push job, which loads the tar file from the build job and pushes it to GitHub Container Registry (GHCR). Like the build job, it only has the permissions it needs to write to the registry: 410 | 411 | ```yaml 412 | jobs: 413 | push: 414 | permissions: 415 | packages: write 416 | ``` 417 | 418 | With the image built and stored in GHCR, we can now sign the image in the `sign` job. 419 | This job has access to the workflow id token so that it can request a signing certificate from Fulcio; the job also needs permission to push the signature to the registry, so it has that access as well: 420 | 421 | ```yaml 422 | jobs: 423 | sign: 424 | permissions: 425 | id-token: write 426 | contents: read 427 | packages: write 428 | ``` 429 | 430 | The `sign` job uses `cosign` to sign the image and annotate it with the workflow run: 431 | 432 | ```yaml 433 | - name: Sign 434 | run: | 435 | cosign sign \ 436 | --annotations liatr.io/github-actions-run-link='${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' \ 437 | --annotations liatr.io/signed-off-by=platform-team \ 438 | --rekor-url ${{ inputs.rekorUrl }} \ 439 | --fulcio-url ${{ inputs.fulcioUrl }} \ 440 | --yes ghcr.io/${{ github.repository }}@${{ needs.push.outputs.digest }} 441 | ``` 442 | 443 | Next, the `provenance` and `source-attestations` job both produce attestations that are signed by the workflow. 444 | The `provenance` job uses the container generator from [`slsa-framework/slsa-github-generator`](https://github.com/slsa-framework/slsa-github-generator) to produce a [provenance attestation](https://slsa.dev/provenance/v1) that links the container image and source code, along with some metadata about how the artifact was produced. 445 | Because the workflow is only using the generator, it doesn't have the full context necessary to populate the entire provenance, so some fields aren't present. 446 | 447 | Here's an example of the provenance generated by this job ([Rekor entry](https://search.sigstore.dev/?logIndex=21341723)): 448 | 449 |
450 | SLSA Provenance attestation (click to expand) 451 | 452 | ```yaml 453 | _type: https://in-toto.io/Statement/v0.1 454 | predicateType: https://slsa.dev/provenance/v0.2 455 | subject: 456 | - name: ghcr.io/liatrio/gh-trusted-builds-app 457 | digest: 458 | sha256: 6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 459 | predicate: 460 | builder: 461 | id: >- 462 | https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main 463 | buildType: https://github.com/slsa-framework/slsa-github-generator/container@v1 464 | invocation: 465 | configSource: 466 | uri: git+https://github.com/liatrio/gh-trusted-builds-app@refs/heads/main 467 | digest: 468 | sha1: e1f1d4396181766e12fca22f2ba856e8154b4304 469 | entryPoint: .github/workflows/app.yaml 470 | parameters: {} 471 | environment: 472 | github_actor: rcoy-v 473 | github_actor_id: '9846738' 474 | github_base_ref: '' 475 | github_event_name: push 476 | github_event_payload: 477 | after: e1f1d4396181766e12fca22f2ba856e8154b4304 478 | base_ref: null 479 | before: c4df379485ab80b62ae0cc57c611348a3015f944 480 | commits: 481 | - author: 482 | email: ryanv@liatrio.com 483 | name: Ryan Vance 484 | username: rcoy-v 485 | committer: 486 | email: noreply@github.com 487 | name: GitHub 488 | username: web-flow 489 | distinct: true 490 | id: e1f1d4396181766e12fca22f2ba856e8154b4304 491 | message: 'docs: remove extra newline (#1)' 492 | timestamp: '2023-05-22T10:27:27-05:00' 493 | tree_id: e5dc0b7046c70012c4b84bc7ede6f969efe79edf 494 | url: >- 495 | https://github.com/liatrio/gh-trusted-builds-app/commit/e1f1d4396181766e12fca22f2ba856e8154b4304 496 | compare: >- 497 | https://github.com/liatrio/gh-trusted-builds-app/compare/c4df379485ab...e1f1d4396181 498 | created: false 499 | deleted: false 500 | forced: false 501 | head_commit: 502 | author: 503 | email: ryanv@liatrio.com 504 | name: Ryan Vance 505 | username: rcoy-v 506 | committer: 507 | email: noreply@github.com 508 | name: GitHub 509 | username: web-flow 510 | distinct: true 511 | id: e1f1d4396181766e12fca22f2ba856e8154b4304 512 | message: 'docs: remove extra newline (#1)' 513 | timestamp: '2023-05-22T10:27:27-05:00' 514 | tree_id: e5dc0b7046c70012c4b84bc7ede6f969efe79edf 515 | url: >- 516 | https://github.com/liatrio/gh-trusted-builds-app/commit/e1f1d4396181766e12fca22f2ba856e8154b4304 517 | organization: 518 | avatar_url: https://avatars.githubusercontent.com/u/5726618?v=4 519 | description: Enterprise Delivery Transformation, DevOps, Cloud Native Automation 520 | events_url: https://api.github.com/orgs/liatrio/events 521 | hooks_url: https://api.github.com/orgs/liatrio/hooks 522 | id: 5726618 523 | issues_url: https://api.github.com/orgs/liatrio/issues 524 | login: liatrio 525 | members_url: https://api.github.com/orgs/liatrio/members{/member} 526 | node_id: MDEyOk9yZ2FuaXphdGlvbjU3MjY2MTg= 527 | public_members_url: https://api.github.com/orgs/liatrio/public_members{/member} 528 | repos_url: https://api.github.com/orgs/liatrio/repos 529 | url: https://api.github.com/orgs/liatrio 530 | pusher: 531 | email: ryanv@liatrio.com 532 | name: rcoy-v 533 | ref: refs/heads/main 534 | repository: 535 | allow_forking: true 536 | archive_url: >- 537 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/{archive_format}{/ref} 538 | archived: false 539 | assignees_url: >- 540 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/assignees{/user} 541 | blobs_url: >- 542 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/git/blobs{/sha} 543 | branches_url: >- 544 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/branches{/branch} 545 | clone_url: https://github.com/liatrio/gh-trusted-builds-app.git 546 | collaborators_url: >- 547 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/collaborators{/collaborator} 548 | comments_url: >- 549 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/comments{/number} 550 | commits_url: >- 551 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/commits{/sha} 552 | compare_url: >- 553 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/compare/{base}...{head} 554 | contents_url: >- 555 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/contents/{+path} 556 | contributors_url: >- 557 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/contributors 558 | created_at: 1684768749 559 | default_branch: main 560 | deployments_url: >- 561 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/deployments 562 | description: null 563 | disabled: false 564 | downloads_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/downloads 565 | events_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/events 566 | fork: false 567 | forks: 0 568 | forks_count: 0 569 | forks_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/forks 570 | full_name: liatrio/gh-trusted-builds-app 571 | git_commits_url: >- 572 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/git/commits{/sha} 573 | git_refs_url: >- 574 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/git/refs{/sha} 575 | git_tags_url: >- 576 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/git/tags{/sha} 577 | git_url: git://github.com/liatrio/gh-trusted-builds-app.git 578 | has_discussions: false 579 | has_downloads: true 580 | has_issues: true 581 | has_pages: false 582 | has_projects: true 583 | has_wiki: true 584 | homepage: null 585 | hooks_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/hooks 586 | html_url: https://github.com/liatrio/gh-trusted-builds-app 587 | id: 643991426 588 | is_template: false 589 | issue_comment_url: >- 590 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/issues/comments{/number} 591 | issue_events_url: >- 592 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/issues/events{/number} 593 | issues_url: >- 594 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/issues{/number} 595 | keys_url: >- 596 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/keys{/key_id} 597 | labels_url: >- 598 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/labels{/name} 599 | language: Go 600 | languages_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/languages 601 | license: null 602 | master_branch: main 603 | merges_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/merges 604 | milestones_url: >- 605 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/milestones{/number} 606 | mirror_url: null 607 | name: gh-trusted-builds-app 608 | node_id: R_kgDOJmKHgg 609 | notifications_url: >- 610 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/notifications{?since,all,participating} 611 | open_issues: 0 612 | open_issues_count: 0 613 | organization: liatrio 614 | owner: 615 | avatar_url: https://avatars.githubusercontent.com/u/5726618?v=4 616 | email: cloudservices@liatrio.com 617 | events_url: https://api.github.com/users/liatrio/events{/privacy} 618 | followers_url: https://api.github.com/users/liatrio/followers 619 | following_url: https://api.github.com/users/liatrio/following{/other_user} 620 | gists_url: https://api.github.com/users/liatrio/gists{/gist_id} 621 | gravatar_id: '' 622 | html_url: https://github.com/liatrio 623 | id: 5726618 624 | login: liatrio 625 | name: liatrio 626 | node_id: MDEyOk9yZ2FuaXphdGlvbjU3MjY2MTg= 627 | organizations_url: https://api.github.com/users/liatrio/orgs 628 | received_events_url: https://api.github.com/users/liatrio/received_events 629 | repos_url: https://api.github.com/users/liatrio/repos 630 | site_admin: false 631 | starred_url: https://api.github.com/users/liatrio/starred{/owner}{/repo} 632 | subscriptions_url: https://api.github.com/users/liatrio/subscriptions 633 | type: Organization 634 | url: https://api.github.com/users/liatrio 635 | private: false 636 | pulls_url: >- 637 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/pulls{/number} 638 | pushed_at: 1684769247 639 | releases_url: >- 640 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/releases{/id} 641 | size: 0 642 | ssh_url: git@github.com:liatrio/gh-trusted-builds-app.git 643 | stargazers: 0 644 | stargazers_count: 0 645 | stargazers_url: >- 646 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/stargazers 647 | statuses_url: >- 648 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/statuses/{sha} 649 | subscribers_url: >- 650 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/subscribers 651 | subscription_url: >- 652 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/subscription 653 | svn_url: https://github.com/liatrio/gh-trusted-builds-app 654 | tags_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/tags 655 | teams_url: https://api.github.com/repos/liatrio/gh-trusted-builds-app/teams 656 | topics: [] 657 | trees_url: >- 658 | https://api.github.com/repos/liatrio/gh-trusted-builds-app/git/trees{/sha} 659 | updated_at: '2023-05-22T15:19:19Z' 660 | url: https://github.com/liatrio/gh-trusted-builds-app 661 | visibility: public 662 | watchers: 0 663 | watchers_count: 0 664 | web_commit_signoff_required: false 665 | sender: 666 | avatar_url: https://avatars.githubusercontent.com/u/9846738?v=4 667 | events_url: https://api.github.com/users/rcoy-v/events{/privacy} 668 | followers_url: https://api.github.com/users/rcoy-v/followers 669 | following_url: https://api.github.com/users/rcoy-v/following{/other_user} 670 | gists_url: https://api.github.com/users/rcoy-v/gists{/gist_id} 671 | gravatar_id: '' 672 | html_url: https://github.com/rcoy-v 673 | id: 9846738 674 | login: rcoy-v 675 | node_id: MDQ6VXNlcjk4NDY3Mzg= 676 | organizations_url: https://api.github.com/users/rcoy-v/orgs 677 | received_events_url: https://api.github.com/users/rcoy-v/received_events 678 | repos_url: https://api.github.com/users/rcoy-v/repos 679 | site_admin: false 680 | starred_url: https://api.github.com/users/rcoy-v/starred{/owner}{/repo} 681 | subscriptions_url: https://api.github.com/users/rcoy-v/subscriptions 682 | type: User 683 | url: https://api.github.com/users/rcoy-v 684 | github_head_ref: '' 685 | github_ref: refs/heads/main 686 | github_ref_type: branch 687 | github_repository_id: '643991426' 688 | github_repository_owner: liatrio 689 | github_repository_owner_id: '5726618' 690 | github_run_attempt: '1' 691 | github_run_id: '5047631192' 692 | github_run_number: '4' 693 | github_sha1: e1f1d4396181766e12fca22f2ba856e8154b4304 694 | metadata: 695 | buildInvocationID: 5047631192-1 696 | completeness: 697 | parameters: true 698 | environment: false 699 | materials: false 700 | reproducible: false 701 | materials: 702 | - uri: git+https://github.com/liatrio/gh-trusted-builds-app@refs/heads/main 703 | digest: 704 | sha1: e1f1d4396181766e12fca22f2ba856e8154b4304 705 | ``` 706 | 707 |
708 | 709 | In addition to the provenance, the `build-and-push` workflow's `source-attestation` job produces a custom pull request attestation using [`liatrio/gh-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations). 710 | This attestation ties the commit to the pull request and includes information about whether the pull request was approved and which individuals reviewed it. 711 | Many enterprises have policies that require a minimum number of reviewers, so this attestation can be used to require that artifacts were built only from approved source code changes. 712 | 713 | Here's an example of a pull request attestation produced by the workflow ([Rekor entry](https://search.sigstore.dev/?logIndex=21341724)): 714 | 715 | ```yaml 716 | _type: https://in-toto.io/Statement/v0.1 717 | predicateType: https://liatr.io/attestations/github-pull-request/v1 718 | subject: 719 | - name: git+https://github.com/liatrio/gh-trusted-builds-app.git 720 | digest: 721 | sha1: e1f1d4396181766e12fca22f2ba856e8154b4304 722 | - name: ghcr.io/liatrio/gh-trusted-builds-app 723 | digest: 724 | sha256: 6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 725 | predicate: 726 | link: https://github.com/liatrio/gh-trusted-builds-app/pull/1 727 | title: 'docs: remove extra newline' 728 | author: rcoy-v 729 | mergedBy: rcoy-v 730 | createdAt: '2023-05-22T15:27:05Z' 731 | mergedAt: '2023-05-22T15:27:27Z' 732 | base: main 733 | head: rcoy-v-patch-1 734 | approved: true 735 | reviewers: 736 | - name: alexashley 737 | approved: true 738 | reviewLink: >- 739 | https://github.com/liatrio/gh-trusted-builds-app/pull/1#pullrequestreview-1436887240 740 | timestamp: '2023-05-22T15:27:18Z' 741 | contributors: 742 | - name: rcoy-v 743 | predicateCreatedAt: '2023-05-22T15:28:48.369418041Z' 744 | ``` 745 | 746 | Finally, the last job in this workflow, called `sbom`, produces a software bill of materials using [Syft](https://github.com/anchore/syft). 747 | The workflow runs Syft against the pushed image and produces an SBOM in the [SPDX](https://spdx.dev/) format. 748 | Then the job runs `cosign attest` in order to sign and upload the SBOM as an attestation. 749 | While there's support in Syft for attesting the SBOM through `sfyt attest`, we're using `cosign` directly for more fine-grained configuration: 750 | 751 | ``` 752 | $ syft -o spdx-json --file sbom.spdx.json ghcr.io/${{ github.repository }}@${{ needs.push.outputs.digest }} 753 | 754 | $ cosign attest --predicate="sbom.spdx.json" \ 755 | --rekor-url ${{ inputs.rekorUrl }} \ 756 | --type spdxjson \ 757 | --fulcio-url ${{ inputs.fulcioUrl }} \ 758 | --yes \ 759 | ghcr.io/${{ github.repository }}@${{ needs.push.outputs.digest }} 760 | ``` 761 | 762 | For an example of an SBOM, here's an attestation from one of the workflow runs ([Rekor log entry](https://search.sigstore.dev/?logIndex=21791183)): 763 | 764 |
765 | SPDX bill of materials (click to expand) 766 | 767 | ```yaml 768 | _type: https://in-toto.io/Statement/v0.1 769 | predicateType: https://spdx.dev/Document 770 | subject: 771 | - name: ghcr.io/liatrio/gh-trusted-builds-app 772 | digest: 773 | sha256: 294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 774 | predicate: 775 | SPDXID: SPDXRef-DOCUMENT 776 | creationInfo: 777 | created: '2023-05-26T22:02:06Z' 778 | creators: 779 | - 'Organization: Anchore, Inc' 780 | - 'Tool: syft-0.82.0' 781 | licenseListVersion: '3.20' 782 | dataLicense: CC0-1.0 783 | documentNamespace: >- 784 | https://anchore.com/syft/image/ghcr.io/liatrio/gh-trusted-builds-app@sha256-294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc-5b3e747f-d8dc-4a0b-a01f-60d9ed082d68 785 | files: 786 | - SPDXID: SPDXRef-File-app-d20c3eddd3b3b879 787 | checksums: 788 | - algorithm: SHA1 789 | checksumValue: '0000000000000000000000000000000000000000' 790 | comment: >- 791 | layerID: 792 | sha256:53ea96ed00f53fed01d48a16b049a99938902ed5ee4517e57f464d7fabacb33f 793 | copyrightText: '' 794 | fileName: /app 795 | fileTypes: 796 | - OTHER 797 | licenseConcluded: NOASSERTION 798 | - SPDXID: SPDXRef-File-app-server-50b4875c6ae53f25 799 | checksums: 800 | - algorithm: SHA256 801 | checksumValue: a947dcd63d19e76e860f32f8bdd33ca47297d10d8e6f1a2eb913224d93fe9fe4 802 | comment: >- 803 | layerID: 804 | sha256:53ea96ed00f53fed01d48a16b049a99938902ed5ee4517e57f464d7fabacb33f 805 | copyrightText: '' 806 | fileName: /app/server 807 | fileTypes: 808 | - APPLICATION 809 | - BINARY 810 | licenseConcluded: NOASSERTION 811 | name: >- 812 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 813 | packages: 814 | - SPDXID: >- 815 | SPDXRef-Package-go-module-github.com-liatrio-gh-trusted-builds-app-1808e23a377c2b90 816 | copyrightText: NOASSERTION 817 | downloadLocation: NOASSERTION 818 | externalRefs: 819 | - referenceCategory: SECURITY 820 | referenceLocator: cpe:2.3:a:liatrio:gh-trusted-builds-app:\(devel\):*:*:*:*:*:*:* 821 | referenceType: cpe23Type 822 | - referenceCategory: SECURITY 823 | referenceLocator: cpe:2.3:a:liatrio:gh_trusted_builds_app:\(devel\):*:*:*:*:*:*:* 824 | referenceType: cpe23Type 825 | - referenceCategory: PACKAGE-MANAGER 826 | referenceLocator: pkg:golang/github.com/liatrio/gh-trusted-builds-app@(devel) 827 | referenceType: purl 828 | licenseConcluded: NOASSERTION 829 | licenseDeclared: NOASSERTION 830 | name: github.com/liatrio/gh-trusted-builds-app 831 | sourceInfo: 'acquired package info from go module information: /app/server' 832 | versionInfo: (devel) 833 | relationships: 834 | - comment: >- 835 | evident-by: indicates the package's existence is evident by the given 836 | file 837 | relatedSpdxElement: SPDXRef-File-app-server-50b4875c6ae53f25 838 | relationshipType: OTHER 839 | spdxElementId: >- 840 | SPDXRef-Package-go-module-github.com-liatrio-gh-trusted-builds-app-1808e23a377c2b90 841 | - relatedSpdxElement: SPDXRef-DOCUMENT 842 | relationshipType: DESCRIBES 843 | spdxElementId: SPDXRef-DOCUMENT 844 | spdxVersion: SPDX-2.3 845 | ``` 846 | 847 |
848 | 849 | 850 | ### Security: Image Scan 851 | 852 | Now that the image is built, it needs to be scanned for vulnerabilities by an approved image scanner. 853 | The security team uses [Trivy](https://github.com/aquasecurity/trivy) for this, because it supports outputting the scan results in a format that `cosign` can use to create an attestation: 854 | 855 | ```yaml 856 | steps: 857 | - name: Trivy Scan 858 | uses: aquasecurity/trivy-action@0.9.2 859 | with: 860 | image-ref: ghcr.io/${{ github.repository }}@${{ inputs.digest }} 861 | format: 'cosign-vuln' 862 | output: trivy.report.json 863 | env: 864 | TRIVY_USERNAME: ${{ github.actor }} 865 | TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} 866 | ``` 867 | 868 | The `scan-image` workflow only contains a single job called `scan`, which takes as an input the digest of the image created in the `build-and-push` workflow. 869 | It invokes the Trivy GitHub Action and then uses `cosign attest` to upload the vulnerability results as an attestation to Rekor. 870 | 871 | This is an example of what the vulnerability attestation looks like ([Rekor entry](https://search.sigstore.dev/?logIndex=21341756)): 872 | 873 |
874 | Vulnerability attestation (click to expand) 875 | 876 | ```yaml 877 | _type: https://in-toto.io/Statement/v0.1 878 | predicateType: https://cosign.sigstore.dev/attestation/vuln/v1 879 | subject: 880 | - name: ghcr.io/liatrio/gh-trusted-builds-app 881 | digest: 882 | sha256: 6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 883 | predicate: 884 | invocation: 885 | parameters: null 886 | uri: '' 887 | event_id: '' 888 | builder.id: '' 889 | scanner: 890 | uri: pkg:github/aquasecurity/trivy@0.38.1 891 | version: 0.38.1 892 | db: 893 | uri: '' 894 | version: '' 895 | result: 896 | ArtifactName: >- 897 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 898 | ArtifactType: container_image 899 | Metadata: 900 | DiffIDs: 901 | - >- 902 | sha256:0c2717ceeb1e6dec4b2a748974b77fd8f13ac5c3d9d434f80f2c1b58c83f31ae 903 | - >- 904 | sha256:8a90007133baa3918410eca530757f8e7c65aff8421ac010a185ff51c2f88e80 905 | ImageConfig: 906 | architecture: amd64 907 | config: 908 | Entrypoint: 909 | - /app/server 910 | Env: 911 | - >- 912 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 913 | Labels: 914 | org.opencontainers.image.created: '2023-05-22T15:27:46.155Z' 915 | org.opencontainers.image.description: '' 916 | org.opencontainers.image.licenses: '' 917 | org.opencontainers.image.revision: e1f1d4396181766e12fca22f2ba856e8154b4304 918 | org.opencontainers.image.source: https://github.com/liatrio/gh-trusted-builds-app 919 | org.opencontainers.image.title: gh-trusted-builds-app 920 | org.opencontainers.image.url: https://github.com/liatrio/gh-trusted-builds-app 921 | org.opencontainers.image.version: main 922 | WorkingDir: /app 923 | created: '2023-05-22T15:28:09.391753905Z' 924 | history: 925 | - comment: buildkit.dockerfile.v0 926 | created: '2023-05-22T15:27:48.389191081Z' 927 | created_by: WORKDIR /app 928 | - comment: buildkit.dockerfile.v0 929 | created: '2023-05-22T15:28:09.391753905Z' 930 | created_by: 'COPY /app/server . # buildkit' 931 | - comment: buildkit.dockerfile.v0 932 | created: '2023-05-22T15:28:09.391753905Z' 933 | created_by: ENTRYPOINT ["/app/server"] 934 | empty_layer: true 935 | os: linux 936 | rootfs: 937 | diff_ids: 938 | - >- 939 | sha256:0c2717ceeb1e6dec4b2a748974b77fd8f13ac5c3d9d434f80f2c1b58c83f31ae 940 | - >- 941 | sha256:8a90007133baa3918410eca530757f8e7c65aff8421ac010a185ff51c2f88e80 942 | type: layers 943 | ImageID: >- 944 | sha256:dfde59c7ffd4a99f202c45669cc6311dadc906d82f7e40f445ebc7a226bd8781 945 | RepoDigests: 946 | - >- 947 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 948 | SchemaVersion: 2 949 | metadata: 950 | scanStartedOn: '2023-05-22T15:29:20.193667225Z' 951 | scanFinishedOn: '2023-05-22T15:29:20.193667225Z' 952 | ``` 953 | 954 |
955 | 956 | The image produced by the demo is very simple, and as of this writing doesn't contain any vulnerabilities. 957 | However, if there were vulnerabilities in the image, they would appear in the attestation and policy could be used to reject artifacts that either had too many vulnerabilities or had certain high-risk CVEs. 958 | 959 | ### Security: Policy Verification 960 | 961 | Now that we've attested to the number of code reviewers, built and signed an image, attested to image provenance and vulnerabilities, it's time to evaluate those attestations. 962 | While it would be possible to use a workflow or [Kubernetes validating admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) to evaluate all the attestations, 963 | it would be cumbersome to have that controller know everything that needs to be validated about the image. Especially considering that enterprises will refine their internal policies over time and need to make adjustments to every deployment gate. 964 | 965 | Instead, we can evaluate all the attestations up to this point and produce a [verification summary attestation](https://slsa.dev/verification_summary/v1), which will attest that the container image either passed or failed a particular policy. 966 | 967 | In order to do this, we use [`liatrio/gh-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations) to pull the attestations for an image, verify that the attestations were signed by the expected workflows, and 968 | then run [policy](https://github.com/liatrio/gh-trusted-builds-policy) against those attestations in order to check if the image meets the standards enforced by the policy. 969 | 970 | Either way, the attestations tool will produce a VSA that can be used later to prevent low quality or malicious images from being deployed. Here's an example of what the VSA produced by the workflow looks like ([Rekor entry](https://search.sigstore.dev/?logIndex=21341780)): 971 | 972 | ```yaml 973 | _type: https://in-toto.io/Statement/v1 974 | subject: 975 | - name: ghcr.io/liatrio/gh-trusted-builds-app 976 | digest: 977 | sha256: 6c3bf887638f7c0d86731e6208befa1b439e465cb435465d982c50609553b514 978 | predicateType: https://slsa.dev/verification_summary/v0.2 979 | predicate: 980 | input_attestations: 981 | - digest: 982 | sha256: 1c735dab58c44079863b1a5e209617f8357409d63203e2309894e0b0d1e0ffaa 983 | uri: https://rekor.sigstore.dev/api/v1/log/entries?logIndex=21341724 984 | - digest: 985 | sha256: cdcf6759b454b26b6f151b2913c2949d4b1bef8c97d78085553f55ededc79f15 986 | uri: https://rekor.sigstore.dev/api/v1/log/entries?logIndex=21341756 987 | policy: 988 | uri: >- 989 | https://github.com/liatrio/gh-trusted-builds-policy/releases/download/v1.1.1/bundle.tar.gz 990 | policy_level: SLSA_LEVEL_3 991 | resource_uri: ghcr.io/liatrio/gh-trusted-builds-app 992 | time_verified: '2023-05-22T15:29:58.061215474Z' 993 | verification_result: PASSED 994 | verifier: 995 | id: >- 996 | https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main 997 | ``` 998 | 999 | Internally, the `policy-verification` workflow has two jobs: 1000 | - `detect-workflow` 1001 | - `verify` 1002 | 1003 | The `detect-workflow` job determines the reusable workflow ref by requesting an id token and grabbing the `job_workflow_ref` claim, which is used later by the `verify` job to populate the `verifier.id` field in the SLSA VSA. 1004 | This job may be removed in the future if GitHub enhances the `github` context by making the `job_workflow_ref` information available directly. 1005 | 1006 | Next, the `verify` job invokes [`liatrio/gh-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations) with the `vsa` subcommand: 1007 | 1008 | ```yaml 1009 | - name: Create Verification Summary Attestation 1010 | env: 1011 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 1012 | run: | 1013 | attestation vsa \ 1014 | --artifact-uri ghcr.io/${{ github.repository }} \ 1015 | --artifact-digest ${{ inputs.digest }} \ 1016 | --policy-url "https://github.com/liatrio/gh-trusted-builds-policy/releases/download/v1.1.1/bundle.tar.gz" \ 1017 | --verifier-id ${{ github.server_url }}/${{ needs.metadata.outputs.jobWorkflowRef }} \ 1018 | --fulcio-url ${{ inputs.fulcioUrl }} \ 1019 | --rekor-url ${{ inputs.rekorUrl }} 1020 | ``` 1021 | 1022 | The `GITHUB_TOKEN` environment variable is used to query GHCR for attestations and also to push the VSA to the registry after verification is finished. 1023 | Like the other jobs that produce attestations, it also needs the ability to request an id token: 1024 | 1025 | ```yaml 1026 | jobs: 1027 | verify: 1028 | permissions: 1029 | id-token: write 1030 | contents: read 1031 | packages: write 1032 | ``` 1033 | 1034 | ### Platform: Deployment 1035 | 1036 | Finally, we'd like to actually deploy the artifact that the workflow built. 1037 | In an enterprise setting, this may not be part of the same workflow that built the image, but the deployment is included here to show an end-to-end picture. 1038 | 1039 | In this workflow, we first download [`k3d`](https://k3d.io/v5.5.1/), which is a lightweight Kubernetes distribution that runs in Docker. 1040 | It'll serve as our deployment target for the demo, as we can use it to spin up a temporary Kubernetes cluster in the GitHub Actions runner. 1041 | 1042 | Once it's installed, we can create a simple cluster called `demo`: 1043 | 1044 | ```shell 1045 | $ k3d cluster create --agents 1 --no-lb --wait demo 1046 | ``` 1047 | 1048 | Now we can install and configure the Sigstore [`policy-controller`](https://docs.sigstore.dev/policy-controller/overview/), which is what we'll use to verify the VSA produced by the `policy-verification` workflow. 1049 | The `policy-controller` has a custom `ClusterImagePolicy` Kubernetes resource that we need to create in order to tell the controller what attestations to verify. 1050 | For the ease of development, this policy is hard-coded in the workflow: 1051 | 1052 | ```yaml 1053 | apiVersion: policy.sigstore.dev/v1alpha1 1054 | kind: ClusterImagePolicy 1055 | metadata: 1056 | name: demo 1057 | spec: 1058 | images: 1059 | - glob: "ghcr.io/liatrio/gh-trusted-builds-app**" 1060 | authorities: 1061 | - name: attestation 1062 | keyless: 1063 | url: https://fulcio.sigstore.dev 1064 | trustRootRef: 1065 | identities: 1066 | - issuer: https://token.actions.githubusercontent.com 1067 | subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main 1068 | ctlog: 1069 | url: https://rekor.sigstore.dev 1070 | trustRootRef: 1071 | attestations: 1072 | - name: has-passing-vsa 1073 | predicateType: "https://slsa.dev/verification_summary/v0.2" 1074 | policy: 1075 | type: rego 1076 | data: | 1077 | package sigstore 1078 | default isCompliant = false 1079 | isCompliant { 1080 | input.predicate.verifier.id == "https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main" 1081 | input.predicate.verification_result == "PASSED" 1082 | } 1083 | ``` 1084 | 1085 | This `ClusterImagePolicy` says we're expecting an attestation of type `https://slsa.dev/verification_summary/v0.2` that was signed with a certificate issued from the GitHub Actions issuer via Fulcio, and that the certificate was issued to the `policy-verification` workflow. 1086 | In the attestation policy, we also check that the VSA result was `PASSED` and that the verifier was the one we expect. 1087 | 1088 | If the policy controller allows the deployment, then the end result won't look any different from a normal Kubernetes deployment. 1089 | However, if image fails to meet policy, the deployment will be blocked and this message will be returned from the Kubernetes API server: 1090 | 1091 | ``` 1092 | error: failed to create deployment: admission webhook "policy.sigstore.dev" denied the request: validation failed: failed policy: demo: spec.template.spec.containers[0].image 1093 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:4c2a8a95f7ecc73abc62abd699dfb33579dd29ecde4f44f009dbbca6305609e4 failed evaluating rego policy for type has-passing-vsa: policy is not compliant for query 'isCompliant = data.sigstore.isCompliant' 1094 | ``` 1095 | 1096 | Unfortunately, it doesn't appear that there's much feedback on what aspect of the policy failed. In this case, it's a very simple policy, which would make it easier to understand what failed. 1097 | However, there's more that could be done to make the error understandable for end-users. 1098 | 1099 | ## Verification 1100 | 1101 | While the image attestations are validated by [`liatrio/gh-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations) and the `policy-controller`, it's also possible to validate the steps that were done in 1102 | this demo by using `cosign` directly. 1103 | 1104 | To demonstrate, we'll use this [workflow run](https://github.com/liatrio/gh-trusted-builds-app/actions/runs/5095256269) which produced the image `ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc`. 1105 | 1106 | To get an overview of the images' attestations and signatures, we can use `cosign tree`: 1107 | 1108 | ```shell 1109 | $ 📦 Supply Chain Security Related artifacts for an image: ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1110 | └── 💾 Attestations for an image tag: ghcr.io/liatrio/gh-trusted-builds-app:sha256-294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc.att 1111 | ├── 🍒 sha256:86f0c07ab8720d5676dd38aa3c46c2179475f760d7fdbdeb8374292a00f86226 1112 | ├── 🍒 sha256:937fea3f79cff9469147cadb127e7721fb66eaee20e228756cad7c20b77296d8 1113 | ├── 🍒 sha256:e4a4239487d5a33cd3836d2654c7b76148b1810d1005c34796be3499021ed297 1114 | ├── 🍒 sha256:28dc5117fe59531af747159a7227f3f0a21175581b13873cce15912e3aaef204 1115 | └── 🍒 sha256:c4b6fb8b2b0e352135d21635d6724c93587370a179e61c39a32089d4d85274e8 1116 | └── 🔐 Signatures for an image tag: ghcr.io/liatrio/gh-trusted-builds-app:sha256-294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc.sig 1117 | └── 🍒 sha256:010e941fc66633c54e29f2576dcb1e1c642a8c9313781b668dec26903a7998af 1118 | ``` 1119 | 1120 | First, we'll verify that the image was signed by the platform team's `build-and-push` workflow: 1121 | 1122 | ```shell 1123 | $ cosign verify \ 1124 | --annotations liatr.io/signed-off-by=platform-team \ 1125 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1126 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1127 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1128 | --rekor-url https://rekor.sigstore.dev \ 1129 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1130 | 1131 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1132 | The following checks were performed on each of these signatures: 1133 | - The specified annotations were verified. 1134 | - The cosign claims were validated 1135 | - Existence of the claims in the transparency log was verified offline 1136 | - The code-signing certificate was verified using trusted certificate authority certificates 1137 | ``` 1138 | 1139 | The `verify` subcommand will also output the signature, so we can manually inspect the other fields for more information: 1140 | 1141 |
1142 | Image signature (click to expand) 1143 | 1144 | ```json 1145 | [ 1146 | { 1147 | "critical": { 1148 | "identity": { 1149 | "docker-reference": "ghcr.io/liatrio/gh-trusted-builds-app" 1150 | }, 1151 | "image": { 1152 | "docker-manifest-digest": "sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc" 1153 | }, 1154 | "type": "cosign container image signature" 1155 | }, 1156 | "optional": { 1157 | "1.3.6.1.4.1.57264.1.1": "https://token.actions.githubusercontent.com", 1158 | "1.3.6.1.4.1.57264.1.2": "workflow_dispatch", 1159 | "1.3.6.1.4.1.57264.1.3": "54a0e5823b30c4fb8d0ff93b532e64d9478e012d", 1160 | "1.3.6.1.4.1.57264.1.4": "app", 1161 | "1.3.6.1.4.1.57264.1.5": "liatrio/gh-trusted-builds-app", 1162 | "1.3.6.1.4.1.57264.1.6": "refs/heads/main", 1163 | "Bundle": { 1164 | "SignedEntryTimestamp": "MEUCIQDLeeJTlGROlwuXen9V8c0vA0gNjh1kCgFI21I7hDlBDQIgQ9mpdPXzkNt3Sg/KupxNTUH6JvcOEa0JvAxdimybue8=", 1165 | "Payload": { 1166 | "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwMTBlOTQxZmM2NjYzM2M1NGUyOWYyNTc2ZGNiMWUxYzY0MmE4YzkzMTM3ODFiNjY4ZGVjMjY5MDNhNzk5OGFmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNnMW4vVkZqV2xlL3V2bWpYMWszL3dTYWhKb3Y5c0hPY0VsRnp2L2gvT3Z3SWhBT0VvYnVLNFd6VzhQQ2cvVUptT2V2ODRHb0YwQ0RqRjVHQmlVN0s0dUFkcyIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaEJSRU5EUW05WFowRjNTVUpCWjBsVlEwVmhOVmhxTm5sSFdqSjZXazloVDAweFNta3JjRkJGTTJJMGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcE5kMDVVU1RKTmFrbDNUVlJGTTFkb1kwNU5hazEzVGxSSk1rMXFTWGhOVkVVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZsY1hORlFuZDBUMXBUU1hkSldUaEZSVmhQVGswMWJVdFFPQ3RUYW5wcE1EZEpkQzhLV2pkTk1ESm1hbE5rZEhCWFkwNTBjelpEZG5CNGNXeFhLekZVWm10TlRWVnFRVzkyY2l0WlFUQjRXbFZIYlRWM2R6WlBRMEpoVVhkbloxZG5UVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZyZDJaSENqTXlMMXBXVDBJNVIxZFNhSFJXYzFFeGJGbFdWemhaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJWbldVUldVakJTUVZGSUwwSklRWGRpYjFwellVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGNGbFlVbmxoVnpoMldqSm5kQXBrU0VveFl6TlNiRnBETVdsa1YyeHpXa2hOZEdReU9YbGhNbHB6WWpOa2VreDVOVzVoV0ZKdlpGZEpkbVF5T1hsaE1scHpZak5rZWt3eVNqRmhWM2hyQ2t4WFJuVmFRekYzWkZoT2IweHViR2hpVjNoQlkyMVdiV041T1c5YVYwWnJZM2s1ZEZsWGJIVk5SR3RIUTJselIwRlJVVUpuTnpoM1FWRkZSVXN5YURBS1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFaRU0xYW1JeU1IZElkMWxMUzNkWlFncENRVWRFZG5wQlFrRm5VVkprTWpsNVlUSmFjMkl6WkdaYVIyeDZZMGRHTUZreVozZE9aMWxMUzNkWlFrSkJSMFIyZWtGQ1FYZFJiMDVVVW1oTlIxVXhDazlFU1hwWmFrMTNXWHBTYlZscWFHdE5SMXB0VDFST2FVNVVUWGxhVkZrd1drUnJNRTU2YUd4TlJFVjVXa1JCVWtKbmIzSkNaMFZGUVZsUEwwMUJSVVVLUWtGT2FHTklRWGRMZDFsTFMzZFpRa0pCUjBSMmVrRkNRbEZSWkdKSGJHaGtTRXB3WW5rNWJtRkRNVEJqYmxaNlpFZFdhMHhYU2pGaFYzaHJZM2t4YUFwalNFRjNTRkZaUzB0M1dVSkNRVWRFZG5wQlFrSm5VVkJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRWMwZERhWE5IUVZGUlFtYzNPSGRCVVdkRkNreFJkM0poU0ZJd1kwaE5Oa3g1T1RCaU1uUnNZbWsxYUZrelVuQmlNalY2VEcxa2NHUkhhREZaYmxaNldsaEthbUl5TlRCYVZ6VXdURzFPZG1KVVFqZ0tRbWR2Y2tKblJVVkJXVTh2VFVGRlNrSkhORTFpUjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WVZkR01HTnRiSFpNTW1SdlRGaFNlUXBrV0U0d1dsZFJkRmx1Vm5CaVIxSjZURmhrZG1OdGRHMWlSemt6WTNrNGRWb3liREJoU0ZacFRETmtkbU50ZEcxaVJ6a3pZM2s1YVdSWGJITmFRekZvQ21KdFVYUmpTRlo2WVVNMU5WbFhNWE5SU0Vwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUUkNaMjl5UW1kRlJVRlpUeTlOUVVWTFFrTnZUVXRFUVhvS1dtcG5OVTU2YXpGWmFtaHJUbTFOTkZsdFNUTk9NazE1VGtSWmVWcFhVVFJOZWxwb1RXcG9iVnBVUW1oTlZFSnFXVmRWZDBoUldVdExkMWxDUWtGSFJBcDJla0ZDUTNkUlVFUkJNVzVoV0ZKdlpGZEpkR0ZIT1hwa1IxWnJUVVZCUjBOcGMwZEJVVkZDWnpjNGQwRlJkMFZOWjNkM1lVaFNNR05JVFRaTWVUbHVDbUZZVW05a1YwbDFXVEk1ZEV3eWVIQlpXRko1WVZjNGRsb3laM1JrU0VveFl6TlNiRnBETVdsa1YyeHpXa2hOZEZsWVFuZE5SR2RIUTJselIwRlJVVUlLWnpjNGQwRlJNRVZMWjNkdlRsUlNhRTFIVlRGUFJFbDZXV3BOZDFsNlVtMVphbWhyVFVkYWJVOVVUbWxPVkUxNVdsUlpNRnBFYXpCT2VtaHNUVVJGZVFwYVJFRm1RbWR2Y2tKblJVVkJXVTh2VFVGRlQwSkNSVTFFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFWcENaMjl5UW1kRlJVRlpUeTlOUVVWUUNrSkJjMDFEVkZrd1RYcHJOVTFVVVhsT2FrRnhRbWR2Y2tKblJVVkJXVTh2VFVGRlVVSkNkMDFIYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWUtZbE01YzJGWFJqQmpiV3gyVFVKalIwTnBjMGRCVVZGQ1p6YzRkMEZTUlVWRFVYZElUbFJqZVU1cVdYaFBSRUp5UW1kdmNrSm5SVVZCV1U4dlRVRkZVd3BDUmpCTlZ6Sm9NR1JJUW5wUGFUaDJXakpzTUdGSVZtbE1iVTUyWWxNNWMyRlhSakJqYld4MlRESmtiMHhZVW5sa1dFNHdXbGRSZEZsdVZuQmlSMUo2Q2t4WFJuZGpRemgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbG9ZMGhCZFdWWFJuUmlSVUo1V2xkYWVrd3lhR3haVjFKNlRESXhhR0ZYTkhjS1QwRlpTMHQzV1VKQ1FVZEVkbnBCUWtWM1VYRkVRMmN4VGtkRmQxcFVWVFJOYWs1cFRYcENhazVIV21sUFIxRjNXbTFaTlUweVNURk5la3BzVG1wU2F3cFBWRkV6VDBkVmQwMVVTbXROUTBWSFEybHpSMEZSVVVKbk56aDNRVkpSUlVWM2QxSmtNamw1WVRKYWMySXpaR1phUjJ4NlkwZEdNRmt5WjNkWmQxbExDa3QzV1VKQ1FVZEVkbnBCUWtaUlVsWkVSazV2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2RzYUdSSVNuQmllVGx1WVVNeE1HTnVWbm9LWkVkV2EweFhTakZoVjNoclkza3hhR05JUVhaWlYwNHdZVmM1ZFdONU9YbGtWelY2VEhwVmQwOVVWWGxPVkZsNVRtcHJkbGxZVWpCYVZ6RjNaRWhOZGdwTlZFTkNhVkZaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamRDU0d0QlpIZENNVUZPTURsTlIzSkhlSGhGZVZsNGEyVklTbXh1VG5kTGFWTnNOalF6YW5sMENpODBaVXRqYjBGMlMyVTJUMEZCUVVKcFJtOVZhMWhOUVVGQlVVUkJSVmwzVWtGSloyTk1VSEpwTjJWcVp5dGhjMnA2Tm5OdGJHOXNUekpXUmtoSFZVNEtNa2hSV0RsTWFtOTFiakJUWW01QlEwbEhjRlpHUkhaWFpXTk9WbEIwVkU1elRWSTVaSFZuWnpGaGJWcEpWM0pRYlV0WlFYaFFNM0ZZZDNaUlRVRnZSd3BEUTNGSFUwMDBPVUpCVFVSQk1tdEJUVWRaUTAxUlEwbFBNakY1TlhSUVpHMUJkRGhJY1ROdFdEbE1SbEZGZEdWeWVHTjNVM0Z2YTFobE1USldSbVZLQ2xWRWMyVTBjSGhhU1VoaFpWYzVUa2hRVDNSdWRsQnJRMDFSUkZOSGRIWkdOVVprUjBsa1RFRjJZMnQxZDNJeFEyeHBXSE5uV25GNlkyMHpZVkY0WlVrS2RIZHhlbFJqV2taS1NXWnFha3hOTmtoTVRrWnNVRkpDTlRsTlBRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifX19fQ==", 1167 | "integratedTime": 1685138477, 1168 | "logIndex": 21791129, 1169 | "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" 1170 | } 1171 | }, 1172 | "Issuer": "https://token.actions.githubusercontent.com", 1173 | "Subject": "https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main", 1174 | "githubWorkflowName": "app", 1175 | "githubWorkflowRef": "refs/heads/main", 1176 | "githubWorkflowRepository": "liatrio/gh-trusted-builds-app", 1177 | "githubWorkflowSha": "54a0e5823b30c4fb8d0ff93b532e64d9478e012d", 1178 | "githubWorkflowTrigger": "workflow_dispatch", 1179 | "liatr.io/github-actions-run-link": "https://github.com/liatrio/gh-trusted-builds-app/actions/runs/5095256269", 1180 | "liatr.io/signed-off-by": "platform-team" 1181 | } 1182 | } 1183 | ] 1184 | ``` 1185 | 1186 |
1187 | 1188 | Now we'll verify each attestation that the pipeline produced using the `cosign verify-attestation` command 1189 | - `https://liatr.io/attestations/github-pull-request/v1` 1190 | - `https://slsa.dev/provenance/v0.2` 1191 | - `https://cosign.sigstore.dev/attestation/vuln/v1` 1192 | - `https://spdx.dev/Document` 1193 | - `https://slsa.dev/verification_summary/v0.2` 1194 | 1195 | First up is the custom pull request attestation. It was produced in the `build-and-push` workflow, so attestation verification looks similar to verifying the image: 1196 | 1197 | ```shell 1198 | $ cosign verify-attestation \ 1199 | --type https://liatr.io/attestations/github-pull-request/v1 \ 1200 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1201 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1202 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1203 | --rekor-url https://rekor.sigstore.dev \ 1204 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1205 | 1206 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1207 | The following checks were performed on each of these signatures: 1208 | - The cosign claims were validated 1209 | - Existence of the claims in the transparency log was verified offline 1210 | - The code-signing certificate was verified using trusted certificate authority certificates 1211 | Certificate subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main 1212 | Certificate issuer URL: https://token.actions.githubusercontent.com 1213 | GitHub Workflow Trigger: workflow_dispatch 1214 | GitHub Workflow SHA: 54a0e5823b30c4fb8d0ff93b532e64d9478e012d 1215 | GitHub Workflow Name: app 1216 | GitHub Workflow Trigger liatrio/gh-trusted-builds-app 1217 | GitHub Workflow Ref: refs/heads/main 1218 | ``` 1219 | 1220 | Now we can do the same for the SLSA provenance attestation. This is an attestation type that's natively understood by `cosign`, so we can use `--type slsaprovenance`: 1221 | 1222 | ```shell 1223 | $ cosign verify-attestation \ 1224 | --type slsaprovenance \ 1225 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1226 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1227 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1228 | --rekor-url https://rekor.sigstore.dev \ 1229 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1230 | 1231 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1232 | The following checks were performed on each of these signatures: 1233 | - The cosign claims were validated 1234 | - Existence of the claims in the transparency log was verified offline 1235 | - The code-signing certificate was verified using trusted certificate authority certificates 1236 | Certificate subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main 1237 | Certificate issuer URL: https://token.actions.githubusercontent.com 1238 | GitHub Workflow Trigger: workflow_dispatch 1239 | GitHub Workflow SHA: 54a0e5823b30c4fb8d0ff93b532e64d9478e012d 1240 | GitHub Workflow Name: app 1241 | GitHub Workflow Trigger liatrio/gh-trusted-builds-app 1242 | GitHub Workflow Ref: refs/heads/main 1243 | ``` 1244 | 1245 | Next, we can check for the vulnerability attestation produced in the `scan-image` workflow. This is another attestation type that `cosign` is familiar with, so we can use `--type vuln`: 1246 | 1247 | ```shell 1248 | $ cosign verify-attestation \ 1249 | --type vuln \ 1250 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1251 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/scan-image.yaml@refs/heads/main \ 1252 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1253 | --rekor-url https://rekor.sigstore.dev \ 1254 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1255 | 1256 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1257 | The following checks were performed on each of these signatures: 1258 | - The cosign claims were validated 1259 | - Existence of the claims in the transparency log was verified offline 1260 | - The code-signing certificate was verified using trusted certificate authority certificates 1261 | Certificate subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/scan-image.yaml@refs/heads/main 1262 | Certificate issuer URL: https://token.actions.githubusercontent.com 1263 | GitHub Workflow Trigger: workflow_dispatch 1264 | GitHub Workflow SHA: 54a0e5823b30c4fb8d0ff93b532e64d9478e012d 1265 | GitHub Workflow Name: app 1266 | GitHub Workflow Trigger liatrio/gh-trusted-builds-app 1267 | GitHub Workflow Ref: refs/heads/main 1268 | ``` 1269 | 1270 | Similarly, we can verify the SBOM attestation. Like the SLSA provenance and vulnerability attestation types, this is another format the `cosign` understands natively: 1271 | 1272 | ```shell 1273 | $ cosign verify-attestation \ 1274 | --type spdxjson \ 1275 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1276 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1277 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1278 | --rekor-url https://rekor.sigstore.dev \ 1279 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1280 | 1281 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1282 | The following checks were performed on each of these signatures: 1283 | - The cosign claims were validated 1284 | - Existence of the claims in the transparency log was verified offline 1285 | - The code-signing certificate was verified using trusted certificate authority certificates 1286 | Certificate subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main 1287 | Certificate issuer URL: https://token.actions.githubusercontent.com 1288 | GitHub Workflow Trigger: workflow_dispatch 1289 | GitHub Workflow SHA: 54a0e5823b30c4fb8d0ff93b532e64d9478e012d 1290 | GitHub Workflow Name: app 1291 | GitHub Workflow Trigger liatrio/gh-trusted-builds-app 1292 | GitHub Workflow Ref: refs/heads/main 1293 | ``` 1294 | 1295 | Lastly, we can check the verification summary attestation produced by [`liatrio/gh-trusted-builds-attestations`](https://github.com/liatrio/gh-trusted-builds-attestations): 1296 | 1297 | ```shell 1298 | $ cosign verify-attestation \ 1299 | --type https://slsa.dev/verification_summary/v0.2 \ 1300 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1301 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main \ 1302 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1303 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1304 | 1305 | Verification for ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc -- 1306 | The following checks were performed on each of these signatures: 1307 | - The cosign claims were validated 1308 | - Existence of the claims in the transparency log was verified offline 1309 | - The code-signing certificate was verified using trusted certificate authority certificates 1310 | Certificate subject: https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/policy-verification.yaml@refs/heads/main 1311 | Certificate issuer URL: https://token.actions.githubusercontent.com 1312 | GitHub Workflow Trigger: workflow_dispatch 1313 | GitHub Workflow SHA: 54a0e5823b30c4fb8d0ff93b532e64d9478e012d 1314 | GitHub Workflow Name: app 1315 | GitHub Workflow Trigger liatrio/gh-trusted-builds-app 1316 | GitHub Workflow Ref: refs/heads/main 1317 | ``` 1318 | 1319 | Even after verifying the image signature and attestations, there may still be checks we wish to do on the individual attestations, which is where [policy](https://github.com/liatrio/gh-trusted-builds-policy) comes in. 1320 | For instance, we could check the pull request attestation indicates that multiple reviewers approved a change or that our container isn't vulnerable to a particular CVE. 1321 | 1322 | Of course, it's also helpful to know what failed verification looks like. What happens if an image is missing an attestation? 1323 | We can simulate that by asking cosign to verify an attestation we know doesn't exist: 1324 | 1325 | ```shell 1326 | $ cosign verify-attestation \ 1327 | --type foo \ 1328 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1329 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1330 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1331 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1332 | 1333 | Error: none of the attestations matched the predicate type: foo, found: https://liatr.io/attestations/github-pull-request/v1,https://slsa.dev/provenance/v0.2,https://spdx.dev/Document 1334 | main.go:74: error during command execution: none of the attestations matched the predicate type: foo, found: https://liatr.io/attestations/github-pull-request/v1,https://slsa.dev/provenance/v0.2,https://spdx.dev/Document 1335 | ``` 1336 | 1337 | We tried to ask `cosign` to verify the existence of a `foo` attestation, signed by the `build-and-push` workflow, and because there is no `foo` attestation, `cosign` will report that it wasn't able to find one. 1338 | 1339 | The output will be similar if we ask `cosign` to verify an attestation that does exist, but was signed by a different identity. 1340 | In this case, we'll try to verify that the vuln attestation was signed by the `build-and-push` workflow, when it was actually signed by the `scan-image` workflow. 1341 | 1342 | ```shell 1343 | $ cosign verify-attestation \ 1344 | --type vuln \ 1345 | --certificate-oidc-issuer https://token.actions.githubusercontent.com \ 1346 | --certificate-identity https://github.com/liatrio/gh-trusted-builds-workflows/.github/workflows/build-and-push.yaml@refs/heads/main \ 1347 | --certificate-github-workflow-repository liatrio/gh-trusted-builds-app \ 1348 | ghcr.io/liatrio/gh-trusted-builds-app@sha256:294bafb143807a4afe6b90e6b8b208b9616798effc48e4018b6b9eef9a6ef6bc 1349 | 1350 | Error: none of the attestations matched the predicate type: vuln, found: https://liatr.io/attestations/github-pull-request/v1,https://slsa.dev/provenance/v0.2,https://spdx.dev/Document 1351 | main.go:74: error during command execution: none of the attestations matched the predicate type: vuln, found: https://liatr.io/attestations/github-pull-request/v1,https://slsa.dev/provenance/v0.2,https://spdx.dev/Document 1352 | ``` 1353 | 1354 | In this case, the output is very similar, even though we know that the `vuln` attestation does exist. However, `cosign` first filters the signatures by the signer identities, so it's only looking at the identity that we specified (i.e., the `build-and-push` workflow). 1355 | 1356 | ## Additional Resources 1357 | 1358 | - [Sigstore Security Model](https://docs.sigstore.dev/security/) - an overview of the security model for different Sigstore components. 1359 | - [in-toto specification](https://github.com/in-toto/attestation/tree/main/spec) 1360 | - [Fulcio certificate issuance overview](https://github.com/sigstore/fulcio/blob/main/docs/how-certificate-issuing-works.md) 1361 | - [Fulcio certificate extensions](https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md) 1362 | - [transparency.dev](https://transparency.dev/) - an overview of the verifiable data structures behind Rekor and Fulcio's certificate transparency log. 1363 | --------------------------------------------------------------------------------