Repository: chainguard-dev/apko
Pull request #1307 (2024-09-16 08:49:32)

rsa: remove backwards compat APIs
Now that known users of these APIs have migrated to functions calls
with an explicit hash, remove these backward compat APIs.

This will catch any unknown users of these APIs.



Patch:
diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml
index 14f627a..04b4e2f 100644
--- a/.github/workflows/build-samples.yml
+++ b/.github/workflows/build-samples.yml
@@ -25,7 +25,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
@@ -62,7 +62,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
@@ -97,7 +97,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
@@ -127,7 +127,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
@@ -166,7 +166,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
@@ -205,7 +205,7 @@ jobs:
       - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
         with:
           egress-policy: audit
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
           go-version-file: 'go.mod'
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index cf310da..83a3f07 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -19,7 +19,7 @@ jobs:
         with:
           egress-policy: audit
 
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.1.5
         with:
@@ -32,7 +32,7 @@ jobs:
           ./apko version
 
       - name: Setup cosign (needed in CI tests)
-        uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0
+        uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
 
       - name: Setup crane (needed in CI tests)
         uses: imjasonh/setup-crane@31b88efe9de28ae0ffa220711af4b60be9435f6e # v0.4
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index 579055d..8703414 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -22,7 +22,7 @@ jobs:
       with:
         egress-policy: audit
 
-    - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
     - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v3.0.0
       with:
@@ -30,7 +30,7 @@ jobs:
         check-latest: true
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@8214744c546c1e5c8f03dde8fab3a7353211988d
+      uses: github/codeql-action/init@662472033e021d55d94146f66f6058822b0b39fd
       with:
         languages: go
 
@@ -38,4 +38,4 @@ jobs:
       run: make apko
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@8214744c546c1e5c8f03dde8fab3a7353211988d
+      uses: github/codeql-action/analyze@662472033e021d55d94146f66f6058822b0b39fd
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index fcff897..62effbb 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -23,13 +23,13 @@ jobs:
     - uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
       with:
         egress-policy: audit
-    - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+    - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
     - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.2.0
       with:
         go-version-file: 'go.mod'
         check-latest: true
 
-    - uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0
+    - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
 
     - uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v2.8.1
       with:
diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml
index a8c198b..4ddcc72 100644
--- a/.github/workflows/verify.yaml
+++ b/.github/workflows/verify.yaml
@@ -15,16 +15,16 @@ jobs:
       contents: read
 
     steps:
-      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v2.2.0
         with:
           go-version-file: 'go.mod'
           check-latest: true
+
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
-        timeout-minutes: 5
+        uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
         with:
-          version: v1.60.1
+          version: v1.61
 
       - run: |
           make generate
diff --git a/.golangci.yml b/.golangci.yml
index a1a6cb7..d5d92c7 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -15,6 +15,10 @@ linters-settings:
         ws: "\\s*"
   goimports:
     local-prefixes: chainguard.dev/apko
+  gosec:
+    excludes:
+      # TODO: review those and remove if possible
+      - G115  # integer overflow conversion uint64 -> int
 
 linters:
   enable:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index a47edec..d453575 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -58,5 +58,5 @@ changelog:
 
 release:
   draft: false
-  prerelease: true
+  prerelease: false
   name_template: "Release {{ .Tag }}"
diff --git a/README.md b/README.md
index cab79a9..c777832 100644
--- a/README.md
+++ b/README.md
@@ -40,8 +40,6 @@ To use the examples, you'll generally want to mount your current directory into
 docker run -v "$PWD":/work cgr.dev/chainguard/apko build examples/alpine-base.yaml apko-alpine:edge apko-alpine.tar
 ```
 
-Alternatively, if you're on a Mac, you can use [Lima](./mac/README.md) to run an Alpine Linux VM.
-
 ## Quickstart
 
 An apko file for building an Alpine base image looks like this:
@@ -142,5 +140,3 @@ The [ko](https://github.com/google/ko) project builds Go projects from source in
 
 The [kontain.me](https://github.com/imjasonh/kontain.me) service creates fresh container images on
 demand using different forms of declarative configuration (including ko and apko).
-
-
diff --git a/go.mod b/go.mod
index 1ae64a6..84865fa 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,9 @@
 module chainguard.dev/apko
 
-go 1.23.1
+go 1.23.2
 
 require (
-	chainguard.dev/sdk v0.1.25
+	chainguard.dev/sdk v0.1.28
 	github.com/chainguard-dev/clog v1.5.1-0.20240811185937-4c523ae4593f
 	github.com/charmbracelet/log v0.4.0
 	github.com/dominodatalab/os-release v0.0.0-20190522011736-bcdb4a3e3c2f
@@ -13,36 +13,36 @@ require (
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/invopop/jsonschema v0.12.0
-	github.com/klauspost/compress v1.17.9
+	github.com/klauspost/compress v1.17.11
 	github.com/klauspost/pgzip v1.2.6
 	github.com/package-url/packageurl-go v0.1.3
 	github.com/psanford/memfs v0.0.0-20230130182539-4dbf7e3e865e
-	github.com/sigstore/cosign/v2 v2.4.0
+	github.com/sigstore/cosign/v2 v2.4.1
 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
 	github.com/spf13/cobra v1.8.1
 	github.com/stretchr/testify v1.9.0
 	github.com/tmc/dot v0.0.0-20210901225022-f9bc17da75c0
 	github.com/u-root/u-root v0.14.0
 	go.lsp.dev/uri v0.3.0
-	go.opentelemetry.io/otel v1.30.0
-	go.opentelemetry.io/otel/trace v1.30.0
-	go.step.sm/crypto v0.52.0
-	golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
+	go.opentelemetry.io/otel v1.31.0
+	go.opentelemetry.io/otel/trace v1.31.0
+	go.step.sm/crypto v0.54.0
+	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
 	golang.org/x/sync v0.8.0
-	golang.org/x/sys v0.25.0
-	golang.org/x/time v0.6.0
-	google.golang.org/api v0.197.0
+	golang.org/x/sys v0.26.0
+	golang.org/x/time v0.7.0
+	google.golang.org/api v0.203.0
 	gopkg.in/ini.v1 v1.67.0
 	gopkg.in/yaml.v3 v3.0.1
-	k8s.io/apimachinery v0.31.1
+	k8s.io/apimachinery v0.31.2
 	sigs.k8s.io/release-utils v0.8.5
 )
 
 require (
 	chainguard.dev/go-grpc-kit v0.17.6 // indirect
-	cloud.google.com/go/auth v0.9.3 // indirect
+	cloud.google.com/go/auth v0.9.9 // indirect
 	cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
-	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	cloud.google.com/go/compute/metadata v0.5.2 // indirect
 	dario.cat/mergo v1.0.1 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -123,9 +123,9 @@ require (
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
-	github.com/prometheus/client_golang v1.20.3 // indirect
+	github.com/prometheus/client_golang v1.20.4 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
-	github.com/prometheus/common v0.57.0 // indirect
+	github.com/prometheus/common v0.59.1 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/sassoftware/relic v7.2.1+incompatible // indirect
@@ -145,22 +145,22 @@ require (
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.opencensus.io v0.24.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
-	go.opentelemetry.io/otel/metric v1.30.0 // indirect
+	go.opentelemetry.io/otel/metric v1.31.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.27.0 // indirect
-	golang.org/x/mod v0.20.0 // indirect
-	golang.org/x/net v0.29.0 // indirect
+	golang.org/x/crypto v0.28.0 // indirect
+	golang.org/x/mod v0.21.0 // indirect
+	golang.org/x/net v0.30.0 // indirect
 	golang.org/x/oauth2 v0.23.0 // indirect
-	golang.org/x/term v0.24.0 // indirect
-	golang.org/x/text v0.18.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
-	google.golang.org/grpc v1.66.1 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	golang.org/x/term v0.25.0 // indirect
+	golang.org/x/text v0.19.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
+	google.golang.org/grpc v1.67.1 // indirect
+	google.golang.org/protobuf v1.35.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gotest.tools/v3 v3.5.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 3aac562..08b8808 100644
--- a/go.sum
+++ b/go.sum
@@ -1,14 +1,14 @@
 chainguard.dev/go-grpc-kit v0.17.6 h1:lwIs9LmSnm8jwrH1QmigCwMP6MYkIBENq/0xGduYZss=
 chainguard.dev/go-grpc-kit v0.17.6/go.mod h1:ZNaXn2KEO++2u2WveHs65krYiHmAEGjYLeEtfaQaOWU=
-chainguard.dev/sdk v0.1.25 h1:C+FeoJCt4AeMMlWw6+vu5CIcNeYJgooPse6UMLcZzik=
-chainguard.dev/sdk v0.1.25/go.mod h1:k88fQlFggN176oyRjg7c1ICpEAbDJOlTcZe+BBHk+70=
+chainguard.dev/sdk v0.1.28 h1:xLQv0JxiGhqVKOL059DmTReTjrKFhUsP5U1W6cgr+jQ=
+chainguard.dev/sdk v0.1.28/go.mod h1:9EvGI9GY5UPDbZ5AhGbMO8865ixNu36afQYCZ5M95NM=
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U=
-cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk=
+cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ=
+cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
 cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
 cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
-cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
-cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
+cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
+cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -223,8 +223,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
 github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -289,16 +289,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
-github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4=
-github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
+github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY=
-github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI=
+github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
+github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
 github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
@@ -317,8 +317,8 @@ github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbm
 github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
-github.com/sigstore/cosign/v2 v2.4.0 h1:2NdidNgClg+oXr/fDIr37E/BE6j00gqgUhSiBK2kjSQ=
-github.com/sigstore/cosign/v2 v2.4.0/go.mod h1:j+fH1DCUkcn92qp6ezDj4JbGMri6eG1nLJC+hs64rvc=
+github.com/sigstore/cosign/v2 v2.4.1 h1:b8UXEfJFks3hmTwyxrRNrn6racpmccUycBHxDMkEPvU=
+github.com/sigstore/cosign/v2 v2.4.1/go.mod h1:GvzjBeUKigI+XYnsoVQDmMAsMMc6engxztRSuxE+x9I=
 github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
 github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
 github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
@@ -377,26 +377,26 @@ go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd
 go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
-go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
-go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
+go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
+go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
-go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
-go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
+go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
+go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
 go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo=
 go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok=
-go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
-go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
+go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
+go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
 go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
 go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
-go.step.sm/crypto v0.52.0 h1:3blUzFm0S4tPrijcvcP47tvd7VmEEGJnvzblE+sg5LI=
-go.step.sm/crypto v0.52.0/go.mod h1:GcT4hMILsNiiN3dIXH/Df5fLVs/KIwGZva85faw7lYw=
+go.step.sm/crypto v0.54.0 h1:V8p+12Ld0NRA/RBMYoKXA0dWmVKZSdCwP56IwzweT9g=
+go.step.sm/crypto v0.54.0/go.mod h1:vQJyTngfZDW+UyZdFzOMCY/txWDAmcwViEUC7Gn4YfU=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -415,11 +415,11 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
 golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
-golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
+golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
+golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -428,8 +428,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -447,8 +447,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
 golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -481,8 +481,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -490,8 +490,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -501,10 +501,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
-golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -517,24 +517,24 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
+golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.197.0 h1:x6CwqQLsFiA5JKAiGyGBjc2bNtHtLddhJCE2IKuhhcQ=
-google.golang.org/api v0.197.0/go.mod h1:AuOuo20GoQ331nq7DquGHlU6d+2wN2fZ8O0ta60nRNw=
+google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU=
+google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0=
-google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
+google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
 google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
@@ -542,8 +542,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
-google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -553,8 +553,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -576,7 +576,7 @@ gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
-k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
+k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
 sigs.k8s.io/release-utils v0.8.5 h1:FUtFqEAN621gSXv0L7kHyWruBeS7TUU9aWf76olX7uQ=
 sigs.k8s.io/release-utils v0.8.5/go.mod h1:qsm5bdxdgoHkD8HsXpgme2/c3mdsNaiV53Sz2HmKeJA=
diff --git a/internal/cli/build.go b/internal/cli/build.go
index 0763314..fcccfd2 100644
--- a/internal/cli/build.go
+++ b/internal/cli/build.go
@@ -33,6 +33,7 @@ import (
 
 	"github.com/chainguard-dev/clog"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	"chainguard.dev/apko/pkg/build"
 	"chainguard.dev/apko/pkg/build/oci"
 	"chainguard.dev/apko/pkg/build/types"
@@ -108,7 +109,7 @@ Along the image, apko will generate SBOMs (software bill of materials) describin
 				build.WithTags(args[1]),
 				build.WithVCS(withVCS),
 				build.WithAnnotations(annotations),
-				build.WithCacheDir(cacheDir, offline),
+				build.WithCache(cacheDir, offline, apk.NewCache(true)),
 				build.WithLockFile(lockfile),
 				build.WithTempDir(tmp),
 				build.WithIncludePaths(includePaths),
@@ -301,7 +302,7 @@ func buildImageComponents(ctx context.Context, workDir string, archs []types.Arc
 	}
 
 	// generate the index
-	finalDigest, idx, err := oci.GenerateIndex(ctx, *ic, imgs)
+	finalDigest, idx, err := oci.GenerateIndex(ctx, *ic, imgs, multiArchBDE)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to generate OCI index: %w", err)
 	}
diff --git a/internal/cli/dot.go b/internal/cli/dot.go
index 51924bd..d1cd856 100644
--- a/internal/cli/dot.go
+++ b/internal/cli/dot.go
@@ -51,8 +51,8 @@ func dotcmd() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "dot",
-		Short: "Output a digraph showing the resolved dependencies of an apko config.",
-		Long: `Output a digraph showing the resolved dependencies of an apko config.
+		Short: "Output a digraph showing the resolved dependencies of an apko config",
+		Long: `Output a digraph showing the resolved dependencies of an apko config
 
 # Render an svg of example.yaml
 apko dot example.yaml | dot -Tvsg > graph.svg
@@ -72,7 +72,7 @@ apko dot --web -S example.yaml
 				build.WithExtraKeys(extraKeys),
 				build.WithExtraBuildRepos(extraBuildRepos),
 				build.WithExtraRuntimeRepos(extraRuntimeRepos),
-				build.WithCacheDir(cacheDir, offline),
+				build.WithCache(cacheDir, offline, apk.NewCache(true)),
 			)
 		},
 	}
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 3748b77..b734b5e 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -31,6 +31,7 @@ import (
 
 	"github.com/chainguard-dev/clog"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	"chainguard.dev/apko/pkg/build"
 	"chainguard.dev/apko/pkg/build/oci"
 	"chainguard.dev/apko/pkg/build/types"
@@ -116,7 +117,7 @@ in a keychain.`,
 					build.WithTags(args[1:]...),
 					build.WithVCS(withVCS),
 					build.WithAnnotations(annotations),
-					build.WithCacheDir(cacheDir, offline),
+					build.WithCache(cacheDir, offline, apk.NewCache(true)),
 					build.WithLockFile(lockfile),
 					build.WithTempDir(tmp),
 					build.WithIgnoreSignatures(ignoreSignatures),
diff --git a/internal/cli/show-config.go b/internal/cli/show-config.go
index 9d34bc7..69a739e 100644
--- a/internal/cli/show-config.go
+++ b/internal/cli/show-config.go
@@ -23,6 +23,7 @@ import (
 	"github.com/spf13/cobra"
 	"gopkg.in/yaml.v3"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	apkfs "chainguard.dev/apko/pkg/apk/fs"
 	"chainguard.dev/apko/pkg/build"
 )
@@ -49,7 +50,7 @@ The derived configuration is rendered in YAML.
 				build.WithExtraKeys(extraKeys),
 				build.WithExtraBuildRepos(extraBuildRepos),
 				build.WithExtraRuntimeRepos(extraRuntimeRepos),
-				build.WithCacheDir(cacheDir, offline),
+				build.WithCache(cacheDir, offline, apk.NewCache(true)),
 			)
 		},
 	}
diff --git a/internal/cli/show-packages.go b/internal/cli/show-packages.go
index c3d04b2..a61418c 100644
--- a/internal/cli/show-packages.go
+++ b/internal/cli/show-packages.go
@@ -24,6 +24,7 @@ import (
 
 	"github.com/chainguard-dev/clog"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	"chainguard.dev/apko/pkg/build"
 	"chainguard.dev/apko/pkg/build/types"
 )
@@ -108,7 +109,7 @@ packagelock and packagelock-source are particularly useful for inserting back in
 				build.WithExtraKeys(extraKeys),
 				build.WithExtraBuildRepos(extraBuildRepos),
 				build.WithExtraRuntimeRepos(extraRuntimeRepos),
-				build.WithCacheDir(cacheDir, offline),
+				build.WithCache(cacheDir, offline, apk.NewCache(true)),
 			)
 		},
 	}
diff --git a/mac/README.md b/mac/README.md
deleted file mode 100644
index 7d247bf..0000000
--- a/mac/README.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# apko on mac
-
-Currently `apko` relies on `apk`, which is currently
-not available for mac.
-
-This page documents workarounds to run
-`apko` on a mac.
-
-## OCI Container (Docker)
-
-Use the [official container image](https://github.com/chainguard-images/apko):
-
-```
-$ docker run -v $PWD:/work cgr.dev/chainguard/apko build examples/alpine-base.yaml apko-alpine:edge apko-alpine.tar
-```
-
-## Lima
-
-We maintain an example configuration file for
-[Lima](https://github.com/lima-vm/lima)
-(see [`lima/apko-playground.yaml`](./lima/apko-playground.yaml)).
-
-This provides a VM with the following:
-
-- 1 CPU, 2GiB memory, 10GiB disk
-- Latest release of `apko` (from Alpine edge repo)
-- Useful tools such as `vim`
-- Latest releases of various Docker credential helpers (ECR, GCR)
-- Dummy version of `docker-credential-osxkeychain`
-- Example config files from the repo at `/examples`
-
-Your `$HOME` directory will be mounted into the VM (read-only), in
-order to have access to things such as `~/.docker/config.json`,
-cloud registry credentials, etc.
-
-Root shell is needed for `apko build`. We also override `$HOME` with
-your mac's `$HOME` (mounted into the VM) so that Docker credential
-helpers work properly with `apko publish`.
-
-The commands below assume to be run in this
-directory of the repository, and require `limactl`.
-
-### Start environment
-
-```
-limactl start --tty=false lima/apko-playground.yaml
-```
-
-### Obtain a shell
-
-```
-limactl shell apko-playground sudo su -c "HOME=\"${HOME}\" ash"
-```
-
-### Build an example image
-
-```
-apko build /examples/nginx.yaml tag /tmp/output.tar
-```
-
-### Publish an example image
-
-```
-apko publish /examples/nginx.yaml <registry_ref>
-```
-
-### Delete environment
-
-```
-limactl delete -f apko-playground
-```
diff --git a/mac/lima/apko-playground.yaml b/mac/lima/apko-playground.yaml
deleted file mode 100644
index e61e422..0000000
--- a/mac/lima/apko-playground.yaml
+++ /dev/null
@@ -1,109 +0,0 @@
-# apko lima configuration file
-#
-#   To start environment:
-#     limactl start --tty=false apko-playground.yaml
-#
-#   To obtain root shell (needed for apko build):
-#     limactl shell apko-playground sudo su -c "HOME=\"${HOME}\" ash"
-#
-#   Build an example image:
-#     apko build /examples/nginx.yaml tag /tmp/output.tar
-#
-#   Publish an example image:
-#      apko publish /examples/nginx.yaml <registry_ref>
-#   
-#   To delete environment:
-#     limactl delete -f apko-playground
-#
-images:
-- location: https://github.com/lima-vm/alpine-lima/releases/download/v0.2.27/alpine-lima-std-3.17.0-x86_64.iso
-  arch: "x86_64"
-  digest: "sha512:07b88838734de63edf302a531aaf57f5a48ec31b31b8a95740faa4fac11852b375c5c83180ff34311ee284b8123536b86b70f0606591b1986e9af2268f3ea675"
-- location: https://github.com/lima-vm/alpine-lima/releases/download/v0.2.27/alpine-lima-std-3.17.0-aarch64.iso
-  arch: "aarch64"
-  digest: "sha512:b8028c96385ea5be499e37c142cb9e9d9c861f2647ff7f8cceaf867961ef8878e34a8ba0e8cfe2e01ac082191048e7acc30534782b13c5ec4bea53dad5919c33"
-cpus: 1
-memory: 2GiB
-disk: 10GiB
-firmware:
-  legacyBIOS: true
-containerd:
-  system: false
-  user: false
-mounts:
-# Allow access to things like ~/.docker/config.json and cloud credentials etc.
-- location: "~"
-provision:
-- mode: system
-  script: |
-    #!/bin/ash
-    set -eux -o pipefail
-
-    # Install apko from Alpine edge repo
-    apk add apko -X https://dl-cdn.alpinelinux.org/alpine/edge/testing/
-
-    # Add useful tools like vim
-    apk update
-    apk add vim
-
-    # Add Docker credential helpers needed for pushing to various cloud registries
-    arch="$(uname -m)"
-    if [[ "${arch}" != "x86_64" ]] && [[ "${arch}" != "aarch64" ]]; then
-      echo "Unsupported arch: ${arch}. Exiting."
-      exit 1
-    fi
-
-    # 1. docker-credential-gcr (Google)
-    # https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases
-    if [[ "${arch}" == "x86_64" ]]; then
-      wget -O tmp.tar.gz https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v2.1.0/docker-credential-gcr_linux_amd64-2.1.0.tar.gz
-      sha256sum tmp.tar.gz | grep '^91cca7b5ca33133bcd217982be31d670efe7f1a33eb5be72e014f74feecac00f '
-    else
-      wget -O tmp.tar.gz https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v2.1.0/docker-credential-gcr_linux_arm64-2.1.0.tar.gz
-      sha256sum tmp.tar.gz | grep '^0fcde1af46a4ddc3135a673b61707a4e241b5fc994d493c6c0c03e9ebcf5eee4 '
-    fi
-    tar -xvf tmp.tar.gz docker-credential-gcr
-    chmod +x docker-credential-gcr
-    mv docker-credential-gcr /usr/bin
-    rm -f tmp.tar.gz
-
-    # 2. docker-credential-ecr-login (Amazon)
-    # https://github.com/awslabs/amazon-ecr-credential-helper/releases
-    if [[ "${arch}" == "x86_64" ]]; then
-      wget -O docker-credential-ecr-login https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.6.0/linux-amd64/docker-credential-ecr-login
-      sha256sum docker-credential-ecr-login | grep '^af805202cb5d627dde2e6d4be1f519b195fd5a3a35ddc88d5010b4a4e5a98dd8 '
-    else
-      wget -O docker-credential-ecr-login https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.6.0/linux-arm64/docker-credential-ecr-login
-      sha256sum docker-credential-ecr-login | grep '^760ecd36acf720cfe6a6ddb6fb20a32845e8886ea2e5333441c4bcca0a1d9620 '
-    fi
-    chmod +x docker-credential-ecr-login
-    mv docker-credential-ecr-login /usr/bin
-
-    # 3. docker-credential-acr-env (Microsoft)
-    # https://github.com/chrismellard/docker-credential-acr-env/releases
-    # No version yet available for aarch64, skipping.
-    # See: https://github.com/chrismellard/docker-credential-acr-env/issues/6
-
-    # 4. docker-credential-osxkeychain (mac system)
-    # Add a dummy version of docker-credential-osxkeychain typically found
-    # in mac ~/.docker/config.json
-    echo '#!/bin/ash' > /usr/bin/docker-credential-osxkeychain
-    echo 'echo "{\"ServerURL\":\"${1}\",\"Username\":\"\",\"Secret\":\"\"}"' \
-      >> /usr/bin/docker-credential-osxkeychain
-    chmod +x /usr/bin/docker-credential-osxkeychain
-
-    # Get the examples/ dir from GitHub release, place at /examples
-    wget https://github.com/chainguard-dev/apko/archive/refs/heads/main.zip
-    unzip main.zip "apko-main/examples/*" -d /examples -j
-    rm -f main.zip
-message: |-
-  ---
-  Run the following to get a root shell (needed to run apko build):
-    limactl shell apko-playground sudo su -c "HOME=\"${HOME}\" ash"
-  
-  Try building an image:
-    apko build /examples/nginx.yaml tag /tmp/output.tar
-
-  Try publishing an image:
-    apko publish /examples/nginx.yaml <registry_ref>
-  ---
diff --git a/pkg/apk/apk/apkindex.go b/pkg/apk/apk/apkindex.go
index dd3ac2d..2ae4019 100644
--- a/pkg/apk/apk/apkindex.go
+++ b/pkg/apk/apk/apkindex.go
@@ -119,8 +119,12 @@ func ParsePackageIndex(apkIndexUnpacked io.Reader) ([]*Package, error) {
 			continue
 		}
 
-		if len(line) > 1 && line[1:2] != ":" {
-			return nil, fmt.Errorf("cannot parse line %d: expected \":\" in not found", linenr)
+		if len(line) < 2 {
+			return nil, fmt.Errorf("cannot parse line %d: expected len >= 2, saw %q", linenr, line)
+		}
+
+		if line[1:2] != ":" {
+			return nil, fmt.Errorf("cannot parse line %d: expected \":\" not found", linenr)
 		}
 
 		token := line[:1]
diff --git a/pkg/apk/apk/cache.go b/pkg/apk/apk/cache.go
index 1a999f4..83ee7f2 100644
--- a/pkg/apk/apk/cache.go
+++ b/pkg/apk/apk/cache.go
@@ -26,133 +26,74 @@ import (
 	"sync"
 
 	"go.opentelemetry.io/otel"
+	"golang.org/x/sync/singleflight"
 )
 
-// This is terrible but simpler than plumbing around a cache for now.
-// We will assume that for a given process, we want to reuse etag values.
-// Doing this cuts down on the number of requests we send for index and keys.
-var globalEtagCache = &etagCache{}
-
-type etagResp struct {
-	resp      *http.Response
-	err       error
-	cacheFile string
+type Cache struct {
+	etagCache  *sync.Map
+	headFlight *singleflight.Group
+	getFlight  *singleflight.Group
 }
 
-type etagCache struct {
-	// url -> *sync.Once
-	etags sync.Map
-
-	// url -> etagResp
-	resps sync.Map
-}
-
-// get dedupes incoming etag-based requests by url (using a sync.Map[string]sync.Once) and stores the results
-// in a sync.Map[string]etagResp. If we request the same URL multiple times, we will only ever reach out to
-// the internet for the first once and reuse the results for all subsequent calls (unless the response does
-// not have an etag).
-func (e *etagCache) get(ctx context.Context, t *cacheTransport, request *http.Request, cacheFile string) (*http.Response, error) {
-	ctx, span := otel.Tracer("go-apk").Start(ctx, fmt.Sprintf("etagCache.get(%q)", request.URL.String()))
-	defer span.End()
-
-	url := request.URL.String()
-
-	// Do all the expensive things inside the once.
-	once, _ := e.etags.LoadOrStore(url, &sync.Once{})
-	once.(*sync.Once).Do(func() {
-		ctx, span := otel.Tracer("go-apk").Start(ctx, "once.Do")
-		defer span.End()
-
-		req := request.Clone(request.Context())
-		req.Method = http.MethodHead
-		resp, rerr := t.wrapped.Do(req)
-		if resp != nil {
-			// We don't expect any body from a HEAD so just always close it to appease the linter.
-			resp.Body.Close()
-		}
-		if rerr != nil || resp.StatusCode != 200 {
-			e.resps.Store(url, etagResp{
-				resp: resp,
-				err:  rerr,
-			})
-			return
-		}
-
-		initialEtag, ok := etagFromResponse(resp)
-		if !ok {
-			return
-		}
-
-		// We simulate content-based addressing with the etag values using an .etag
-		// file extension.
-		etagFile := cacheFileFromEtag(cacheFile, initialEtag)
-		if _, err := os.Stat(etagFile); err == nil {
-			e.resps.Store(url, etagResp{
-				cacheFile: etagFile,
-			})
-			return
-		}
+// NewCache returns a new Cache, which allows us to persist the results of HEAD requests
+// for a given URL across multiple builds. This is generally desirable when building many images
+// all at once, since we can eliminate round trips to HEAD the same e.g. APKINDEX repeatedly.
+// However, this is not desirable for long-lived processes where we expected the APKINDEX to change
+// over time and want to observe those changes and re-fetch the new APKINDEX. Since apko is used
+// in both contexts, this knob is exposed, but very few people actually should be using apko as a library,
+// so I'm mostly just talking to myself here. This used to be a process-wide global cache, which was
+// great for the short-lived terraform module, but a terrible default behavior for a library.
+//
+// Even if you don't want to cache HEAD requests, this is still useful for coalescing concurrent
+// requests for the same resource when passing etag=false.
+func NewCache(etag bool) *Cache {
+	c := &Cache{
+		headFlight: &singleflight.Group{},
+		getFlight:  &singleflight.Group{},
+	}
 
-		// Only download the index once.
-		etagFile, err := t.retrieveAndSaveFile(ctx, request, func(r *http.Response) (string, error) {
-			_, span := otel.Tracer("go-apk").Start(ctx, "callback")
-			defer span.End()
-			// On the etag path, use the etag from the actual response to
-			// compute the final file name.
-			finalEtag, ok := etagFromResponse(r)
-			if !ok {
-				return "", fmt.Errorf("GET response did not contain an etag, but HEAD returned %q", initialEtag)
-			}
+	if etag {
+		c.etagCache = &sync.Map{}
+	}
 
-			return cacheFileFromEtag(cacheFile, finalEtag), nil
-		})
-		e.resps.Store(url, etagResp{
-			err:       err,
-			cacheFile: etagFile,
-		})
-	})
+	return c
+}
 
-	v, ok := e.resps.Load(url)
-	if !ok {
-		// If the server doesn't return etags, and we require them,
-		// then do not cache.
-		return t.wrapped.Do(request)
+func (c *Cache) load(cacheFile string) (*http.Response, bool) {
+	if c == nil || c.etagCache == nil {
+		return nil, false
 	}
-	resp := v.(etagResp)
 
-	// If we didn't manage to cache it, return the response and/or error.
-	if resp.cacheFile == "" {
-		return resp.resp, resp.err
+	v, ok := c.etagCache.Load(cacheFile)
+	if !ok {
+		return nil, false
 	}
 
-	f, err := os.Open(resp.cacheFile)
-	if err != nil {
-		return nil, fmt.Errorf("open(%q): %w", resp.cacheFile, err)
-	}
+	return v.(*http.Response), true
+}
 
-	fi, err := f.Stat()
-	if err != nil {
-		return nil, fmt.Errorf("stat(%q): %w", resp.cacheFile, err)
+func (c *Cache) store(cacheFile string, resp *http.Response) {
+	if c == nil || c.etagCache == nil {
+		return
 	}
 
-	return &http.Response{
-		StatusCode:    http.StatusOK,
-		Body:          f,
-		ContentLength: fi.Size(),
-	}, nil
+	c.etagCache.Store(cacheFile, resp)
 }
 
 // cache
 type cache struct {
 	dir     string
 	offline bool
+
+	shared *Cache
 }
 
 // client return an http.Client that knows how to read from and write to the cache
 // key is in the implementation of https://pkg.go.dev/net/http#RoundTripper
-func (c cache) client(wrapped *http.Client, etagRequired bool) *http.Client {
+func (c *cache) client(wrapped *http.Client, etagRequired bool) *http.Client {
 	return &http.Client{
 		Transport: &cacheTransport{
+			cache:        c.shared,
 			wrapped:      wrapped,
 			root:         c.dir,
 			offline:      c.offline,
@@ -162,6 +103,7 @@ func (c cache) client(wrapped *http.Client, etagRequired bool) *http.Client {
 }
 
 type cacheTransport struct {
+	cache        *Cache
 	wrapped      *http.Client
 	root         string
 	offline      bool
@@ -172,20 +114,12 @@ func (t *cacheTransport) RoundTrip(request *http.Request) (*http.Response, error
 	ctx, span := otel.Tracer("go-apk").Start(request.Context(), "cacheTransport.RoundTrip")
 	defer span.End()
 
-	// do we have the file in the cache?
-	if request.URL == nil {
-		return nil, fmt.Errorf("no URL in request")
-	}
 	cacheFile, err := cachePathFromURL(t.root, *request.URL)
 	if err != nil {
 		return nil, fmt.Errorf("invalid cache path based on URL: %w", err)
 	}
 
 	if !t.etagRequired {
-		// We don't cache the response for these because they get cached later in cachePackage.
-
-		ctx, span := otel.Tracer("go-apk").Start(ctx, fmt.Sprintf("Open(%q)", cacheFile))
-		defer span.End()
 		// Try to open the file in the cache.
 		// If we hit an error, just send the request.
 		f, err := os.Open(cacheFile)
@@ -193,9 +127,11 @@ func (t *cacheTransport) RoundTrip(request *http.Request) (*http.Response, error
 			if t.offline {
 				return nil, fmt.Errorf("failed to read %q in offline cache: %w", cacheFile, err)
 			}
+
 			_, span := otel.Tracer("go-apk").Start(ctx, fmt.Sprintf("Request(%q)", request.URL.String()))
 			defer span.End()
 
+			// We don't cache the response for these because they get cached later in cachePackage.
 			return t.wrapped.Do(request)
 		}
 
@@ -206,45 +142,157 @@ func (t *cacheTransport) RoundTrip(request *http.Request) (*http.Response, error
 	}
 
 	if t.offline {
-		cacheDir := cacheDirFromFile(cacheFile)
-		des, err := os.ReadDir(cacheDir)
+		return t.fetchOffline(cacheFile)
+	}
+
+	return t.fetchAndCache(ctx, request, cacheFile)
+}
+
+func (t *cacheTransport) head(request *http.Request, cacheFile string) (*http.Response, error) {
+	resp, ok := t.cache.load(cacheFile)
+	if ok {
+		return resp, nil
+	}
+
+	v, err, _ := t.cache.headFlight.Do(cacheFile, func() (interface{}, error) {
+		req := request.Clone(request.Context())
+		req.Method = http.MethodHead
+		resp, err := t.wrapped.Do(req)
 		if err != nil {
-			return nil, fmt.Errorf("listing %q for offline cache: %w", cacheDir, err)
+			return nil, err
 		}
 
-		if len(des) == 0 {
-			return nil, fmt.Errorf("no offline cached entries for %s", cacheDir)
+		// HEAD shouldn't have a body. Make sure we close it so we can reuse the connection.
+		defer resp.Body.Close()
+
+		t.cache.store(cacheFile, resp)
+
+		return resp, nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return v.(*http.Response), nil
+}
+
+func (t *cacheTransport) get(ctx context.Context, request *http.Request, cacheFile, initialEtag string) (string, error) {
+	v, err, _ := t.cache.getFlight.Do(cacheFile, func() (interface{}, error) {
+		// We simulate content-based addressing with the etag values using an .etag file extension.
+		etagFile, err := cacheFileFromEtag(cacheFile, initialEtag)
+		if err != nil {
+			return "", err
 		}
+		if _, err := os.Stat(etagFile); err == nil {
+			return etagFile, nil
+		}
+
+		// Only download the index once.
+		return t.retrieveAndSaveFile(ctx, request, func(r *http.Response) (string, error) {
+			_, span := otel.Tracer("go-apk").Start(ctx, "callback")
+			defer span.End()
+
+			// On the etag path, use the etag from the actual response to compute the final file name.
+			finalEtag, ok := etagFromResponse(r)
+			if !ok {
+				return "", fmt.Errorf("GET response did not contain an etag, but HEAD returned %q", initialEtag)
+			}
+
+			return cacheFileFromEtag(cacheFile, finalEtag)
+		})
+	})
+	if err != nil {
+		return "", err
+	}
 
-		newest, err := des[0].Info()
+	return v.(string), nil
+}
+
+func (t *cacheTransport) fetchAndCache(ctx context.Context, request *http.Request, cacheFile string) (*http.Response, error) {
+	initialEtag := request.Header.Get("I-Cant-Believe-Its-Not-If-None-Match")
+	if initialEtag == "" {
+		resp, err := t.head(request, cacheFile)
 		if err != nil {
 			return nil, err
 		}
 
-		for _, de := range des[1:] {
-			fi, err := de.Info()
-			if err != nil {
-				return nil, err
-			}
+		if request.Method == http.MethodHead {
+			return resp, err
+		}
 
-			if fi.ModTime().After(newest.ModTime()) {
-				newest = fi
-			}
+		etag, ok := etagFromResponse(resp)
+		if !ok {
+			return t.wrapped.Do(request)
 		}
 
-		f, err := os.Open(filepath.Join(cacheDir, newest.Name()))
+		initialEtag = etag
+	}
+
+	// This is a bit of a hack. We cache parsed APKINDEX files in globalIndexCache, which needs the etag as a cache key.
+	// Since we already send a HEAD request to get that etag, we want to avoid sending a redundant HEAD request above if we can.
+	// However, we don't actually want to pass along this header because it's using the "parsed" version of the Etag from
+	// etagFromResponse and doesn't actually attempt to follow HTTP semantics, so we remove it here to avoid any confusion.
+	request.Header.Del("I-Cant-Believe-Its-Not-If-None-Match")
+
+	etagFile, err := t.get(ctx, request, cacheFile, initialEtag)
+	if err != nil {
+		return nil, err
+	}
+
+	f, err := os.Open(etagFile)
+	if err != nil {
+		return nil, fmt.Errorf("open(%q): %w", etagFile, err)
+	}
+
+	fi, err := f.Stat()
+	if err != nil {
+		return nil, fmt.Errorf("stat(%q): %w", etagFile, err)
+	}
+
+	return &http.Response{
+		StatusCode:    http.StatusOK,
+		Body:          f,
+		ContentLength: fi.Size(),
+	}, nil
+}
+
+func (t *cacheTransport) fetchOffline(cacheFile string) (*http.Response, error) {
+	cacheDir := cacheDirFromFile(cacheFile)
+	des, err := os.ReadDir(cacheDir)
+	if err != nil {
+		return nil, fmt.Errorf("listing %q for offline cache: %w", cacheDir, err)
+	}
+
+	if len(des) == 0 {
+		return nil, fmt.Errorf("no offline cached entries for %s", cacheDir)
+	}
+
+	newest, err := des[0].Info()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, de := range des[1:] {
+		fi, err := de.Info()
 		if err != nil {
 			return nil, err
 		}
 
-		return &http.Response{
-			StatusCode:    http.StatusOK,
-			Body:          f,
-			ContentLength: newest.Size(),
-		}, nil
+		if fi.ModTime().After(newest.ModTime()) {
+			newest = fi
+		}
 	}
 
-	return globalEtagCache.get(ctx, t, request, cacheFile)
+	f, err := os.Open(filepath.Join(cacheDir, newest.Name()))
+	if err != nil {
+		return nil, err
+	}
+
+	return &http.Response{
+		StatusCode:    http.StatusOK,
+		Body:          f,
+		ContentLength: newest.Size(),
+	}, nil
 }
 
 func cacheDirFromFile(cacheFile string) string {
@@ -255,7 +303,7 @@ func cacheDirFromFile(cacheFile string) string {
 	return filepath.Dir(cacheFile)
 }
 
-func cacheFileFromEtag(cacheFile, etag string) string {
+func cacheFileFromEtag(cacheFile, etag string) (string, error) {
 	cacheDir := filepath.Dir(cacheFile)
 	ext := ".etag"
 
@@ -265,7 +313,15 @@ func cacheFileFromEtag(cacheFile, etag string) string {
 		ext = ".tar.gz"
 	}
 
-	return filepath.Join(cacheDir, etag+ext)
+	absPath, err := filepath.Abs(filepath.Join(cacheDir, etag+ext))
+	if err != nil {
+		return "", err
+	}
+	if !strings.HasPrefix(absPath, cacheDir) {
+		return "", fmt.Errorf("unsafe etag value: %q", etag)
+	}
+
+	return absPath, nil
 }
 
 func etagFromResponse(resp *http.Response) (string, bool) {
diff --git a/pkg/apk/apk/implementation.go b/pkg/apk/apk/implementation.go
index 7d97f27..ba34e8a 100644
--- a/pkg/apk/apk/implementation.go
+++ b/pkg/apk/apk/implementation.go
@@ -79,7 +79,7 @@ type APK struct {
 
 	// This is a map of arch to apk.APK for every arch in a mult-arch situation.
 	// It's stuffed here to avoid plumbing it across every method, but it's optional.
-	Others map[string]*APK
+	ByArch map[string]*APK
 }
 
 func New(options ...Option) (*APK, error) {
@@ -497,21 +497,16 @@ func (a *APK) ResolveWorld(ctx context.Context) (toInstall []*RepositoryPackage,
 	resolver := NewPkgResolver(ctx, indexes)
 
 	// For other architectures we're building (if any), we want to disqualify any packages not present in all archs.
-	others := map[string][]NamedIndex{}
-	for otherArch, otherAPK := range a.Others {
-		if otherArch == a.arch {
-			// No need to do this on ourselves.
-			continue
-		}
-
+	allArchs := map[string][]NamedIndex{}
+	for otherArch, otherAPK := range a.ByArch {
 		indexes, err := otherAPK.GetRepositoryIndexes(ctx, a.ignoreSignatures)
 		if err != nil {
 			return toInstall, conflicts, fmt.Errorf("getting indexes for %q sibling: %w", otherArch, err)
 		}
-		others[otherArch] = indexes
+		allArchs[otherArch] = indexes
 	}
 
-	toInstall, conflicts, err = resolver.GetPackagesWithDependencies(ctx, directPkgs, others)
+	toInstall, conflicts, err = resolver.GetPackagesWithDependencies(ctx, directPkgs, allArchs)
 	if err != nil {
 		return
 	}
@@ -829,7 +824,16 @@ func DiscoverKeys(ctx context.Context, client *http.Client, auth auth.Authentica
 	ctx, span := otel.Tracer("go-apk").Start(ctx, "DiscoverKeys")
 	defer span.End()
 
-	discoveryRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s", strings.TrimSuffix(repository, "/"), "apk-configuration"), nil)
+	if !strings.HasPrefix(repository, "https://") && !strings.HasPrefix(repository, "http://") {
+		// Ignore non-remote repositories.
+		return nil, nil
+	}
+	asURL, err := url.Parse(strings.TrimSuffix(repository, "/") + "/apk-configuration")
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse repository URL: %w", err)
+	}
+
+	discoveryRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, asURL.String(), nil)
 	if err != nil {
 		return nil, err
 	}
@@ -923,6 +927,11 @@ func (a *APK) fetchChainguardKeys(ctx context.Context, repository string) error
 
 	log := clog.FromContext(ctx)
 
+	if !strings.HasPrefix(repository, "https://") && !strings.HasPrefix(repository, "http://") {
+		log.Debugf("ignoring non-http(s) repository %s", repository)
+		return nil
+	}
+
 	keys, err := a.DiscoverKeys(ctx, repository)
 	if err != nil {
 		log.Warnf("ignoring missing keys for %s: %v", repository, err)
diff --git a/pkg/apk/apk/index.go b/pkg/apk/apk/index.go
index f8fec84..e0f99e7 100644
--- a/pkg/apk/apk/index.go
+++ b/pkg/apk/apk/index.go
@@ -31,6 +31,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/chainguard-dev/clog"
 	"github.com/klauspost/compress/gzip"
 	"go.lsp.dev/uri"
 	"go.opentelemetry.io/otel"
@@ -47,40 +48,129 @@ var signatureFileRegex = regexp.MustCompile(`^\.SIGN\.(DSA|RSA|RSA256|RSA512)\.(
 // We just hold the parsed index in memory rather than re-parsing it every time,
 // which requires gunzipping, which is (somewhat) expensive.
 var globalIndexCache = &indexCache{
-	modtimes: map[string]time.Time{},
+	modtimes:  map[string]time.Time{},
+	urlToEtag: map[string]string{},
 }
 
 type indexResult struct {
-	idx *APKIndex
+	idx NamedIndex
 	err error
 }
 
 type indexCache struct {
 	// For remote indexes.
-	onces sync.Map
+	onces     sync.Map
+	urlToEtag map[string]string
+	etagMu    sync.Mutex // guards urlToEtag
 
 	// For local indexes.
 	sync.Mutex
 	modtimes map[string]time.Time
 
-	// repoBase -> indexResult
+	// etag|filename -> indexResult
 	indexes sync.Map
 }
 
-func (i *indexCache) get(ctx context.Context, u string, keys map[string][]byte, arch string, opts *indexOpts) (*APKIndex, error) {
+func (i *indexCache) forget(key string) {
+	i.onces.Delete(key)
+	i.indexes.Delete(key)
+}
+
+func (i *indexCache) store(key string, idx NamedIndex, err error) {
+	i.indexes.Store(key, indexResult{
+		idx: idx,
+		err: err,
+	})
+}
+
+func (i *indexCache) load(key string) (NamedIndex, error) {
+	v, ok := i.indexes.Load(key)
+	if !ok {
+		return nil, fmt.Errorf("indexCache did not see key %q after writing it", key)
+	}
+	result := v.(indexResult)
+
+	return result.idx, result.err
+}
+
+func (i *indexCache) get(ctx context.Context, repoName, repoURL string, keys map[string][]byte, arch string, opts *indexOpts) (NamedIndex, error) {
+	u := IndexURL(repoURL, arch)
+
 	ctx, span := otel.Tracer("go-apk").Start(ctx, fmt.Sprintf("indexCache.get(%q)", u))
 	defer span.End()
 
+	repoBase := fmt.Sprintf("%s/%s", repoURL, arch)
+	repoRef := Repository{URI: repoBase}
+
 	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
-		// We don't want remote indexes to change while we're running.
-		once, _ := i.onces.LoadOrStore(u, &sync.Once{})
+		asURL, err := url.Parse(u)
+		if err != nil {
+			return nil, fmt.Errorf("parsing repo: %w", err)
+		}
+
+		// We usually don't want remote indexes to change while we're running.
+		// But sometimes, we do, in which case we want to key off of the etag.
+		// We can use a separate etag cache in the httpClient to avoid the HEAD
+		// if it's set, but if it's not set we need to do a HEAD each time.
+		client := opts.httpClient
+		head, err := http.NewRequestWithContext(ctx, http.MethodHead, u, nil)
+		if err != nil {
+			return nil, err
+		}
+		if opts.auth == nil {
+			opts.auth = auth.DefaultAuthenticators
+		}
+		if err := opts.auth.AddAuth(ctx, head); err != nil {
+			return nil, fmt.Errorf("unable to add auth to request: %w", err)
+		}
+
+		resp, err := client.Do(head)
+		if err != nil {
+			return nil, err
+		}
+
+		fetchAndParse := func(etag string) (NamedIndex, error) {
+			b, err := fetchRepositoryIndex(ctx, u, etag, opts)
+			if err != nil {
+				return nil, fmt.Errorf("fetching %s: %w", asURL.Redacted(), err)
+			}
+			idx, err := parseRepositoryIndex(ctx, u, keys, arch, b, opts)
+			if err != nil {
+				return nil, fmt.Errorf("parsing %s: %w", asURL.Redacted(), err)
+			}
+			return NewNamedRepositoryWithIndex(repoName, repoRef.WithIndex(idx)), nil
+		}
+
+		etag, ok := etagFromResponse(resp)
+		if !ok {
+			// If there's no etag, we can't cache it, so just return the result.
+			return fetchAndParse(etag)
+		}
+
+		key := fmt.Sprintf("%s@%s", u, etag)
+
+		once, _ := i.onces.LoadOrStore(key, &sync.Once{})
 		once.(*sync.Once).Do(func() {
-			idx, err := getRepositoryIndex(ctx, u, keys, arch, opts)
-			i.indexes.Store(u, indexResult{
-				idx: idx,
-				err: err,
-			})
+			// If we've seen this URL before, delete any references to old indexes so we can GC them.
+			// Lock reads/writes to the map, without blocking the fetchAndParse goroutine.
+			i.etagMu.Lock()
+			prev, ok := i.urlToEtag[u]
+			if ok {
+				prevKey := fmt.Sprintf("%s@%s", u, prev)
+				i.forget(prevKey)
+			}
+			i.etagMu.Unlock()
+
+			idx, err := fetchAndParse(etag)
+			i.store(key, idx, err)
+
+			// Record the current etag for this URL so we can GC it later.
+			i.etagMu.Lock()
+			i.urlToEtag[u] = etag
+			i.etagMu.Unlock()
 		})
+
+		return i.load(key)
 	} else {
 		i.Lock()
 		defer i.Unlock()
@@ -88,30 +178,28 @@ func (i *indexCache) get(ctx context.Context, u string, keys map[string][]byte,
 		// We do expect local indexes to change, so we check modtimes.
 		stat, err := os.Stat(u)
 		if err != nil {
-			return nil, nil
+			return nil, fmt.Errorf("stat: %w", err)
 		}
 
 		mod := stat.ModTime()
 		before, ok := i.modtimes[u]
 		if !ok || mod.After(before) {
+			b, err := os.ReadFile(u)
+			if err != nil {
+				return nil, fmt.Errorf("reading file: %w", err)
+			}
 			// If this is the first time or it has changed since the last time...
-			idx, err := getRepositoryIndex(ctx, u, keys, arch, opts)
-			i.indexes.Store(u, indexResult{
-				idx: idx,
-				err: err,
-			})
+			idx, err := parseRepositoryIndex(ctx, u, keys, arch, b, opts)
+			if err != nil {
+				i.store(u, nil, err)
+			} else {
+				i.store(u, NewNamedRepositoryWithIndex(repoName, repoRef.WithIndex(idx)), nil)
+			}
 			i.modtimes[u] = mod
 		}
-	}
 
-	v, ok := i.indexes.Load(u)
-	if !ok {
-		asURL, _ := url.Parse(u)
-		panic(fmt.Errorf("did not see index %q after writing it", asURL.Redacted()))
+		return i.load(u)
 	}
-	result := v.(indexResult)
-
-	return result.idx, result.err
 }
 
 // IndexURL full URL to the index file for the given repo and arch
@@ -154,23 +242,19 @@ func GetRepositoryIndexes(ctx context.Context, repos []string, keys map[string][
 				repoURL = parts[1]
 			}
 
-			u := IndexURL(repoURL, arch)
-			repoBase := fmt.Sprintf("%s/%s", repoURL, arch)
-
-			index, err := globalIndexCache.get(ctx, u, keys, arch, opts)
+			index, err := globalIndexCache.get(ctx, repoName, repoURL, keys, arch, opts)
 			if err != nil {
-				asURL, _ := url.Parse(u)
-				return fmt.Errorf("reading index %s: %w", asURL.Redacted(), err)
-			}
+				redacted := redact(IndexURL(repoURL, arch))
+				if errors.Is(err, fs.ErrNotExist) {
+					// This can happen for local repos, just log and continue.
+					clog.WarnContextf(ctx, "getting local index %s: %v", redacted, err)
+					return nil
+				}
 
-			// Can happen for fs.ErrNotExist in file scheme, we just ignore it.
-			if index == nil {
-				return nil
+				return fmt.Errorf("reading index %s: %w", redacted, err)
 			}
 
-			repoRef := Repository{URI: repoBase}
-
-			indexes[i] = NewNamedRepositoryWithIndex(repoName, repoRef.WithIndex(index))
+			indexes[i] = index
 			return nil
 		})
 	}
@@ -198,75 +282,51 @@ func shouldCheckSignatureForIndex(index string, arch string, opts *indexOpts) bo
 	return true
 }
 
-func getRepositoryIndex(ctx context.Context, u string, keys map[string][]byte, arch string, opts *indexOpts) (*APKIndex, error) { //nolint:gocyclo
-	ctx, span := otel.Tracer("go-apk").Start(ctx, "getRepositoryIndex")
-	defer span.End()
+func fetchRepositoryIndex(ctx context.Context, u string, etag string, opts *indexOpts) ([]byte, error) { //nolint:gocyclo
+	client := opts.httpClient
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
+	if err != nil {
+		return nil, err
+	}
 
-	// Normalize the repo as a URI, so that local paths
-	// are translated into file:// URLs, allowing them to be parsed
-	// into a url.URL{}.
-	var (
-		b     []byte
-		asURL *url.URL
-		err   error
-	)
-	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
-		asURL, err = url.Parse(u)
-	} else {
-		// Attempt to parse non-https elements into URI's so they are translated into
-		// file:// URLs allowing them to parse into a url.URL{}
-		asURL, err = url.Parse(string(uri.New(u)))
+	if etag != "" {
+		// This is a hack, I'm sorry, but it's the only way to avoid modifying our weird transportCache too much.
+		// Earlier, we check the etag value to avoid re-parsing the APKINDEX if it hasn't changed, but the transport
+		// also really wants to do a HEAD request in order to do etag-based caching itself. To avoid the double HEAD,
+		// I'm stuffing the etag into the If-None-Match header, which isn't exactly correct semantics but it rhymes.
+		// The alternative is to rewrite everything, which I don't have time to do right now, so it is what it is.
+		req.Header.Set("I-Cant-Believe-Its-Not-If-None-Match", etag)
+	}
+
+	if opts.auth == nil {
+		opts.auth = auth.DefaultAuthenticators
+	}
+	if err := opts.auth.AddAuth(ctx, req); err != nil {
+		return nil, fmt.Errorf("unable to add auth to request: %w", err)
 	}
+
+	// This will return a body that retries requests using Range requests if Read() hits an error.
+	rrt := newRangeRetryTransport(ctx, client)
+	res, err := rrt.RoundTrip(req)
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse repo as URI: %w", err)
+		return nil, err
+	}
+	if res.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("unexpected status code %d", res.StatusCode)
 	}
+	defer res.Body.Close()
 
-	switch asURL.Scheme {
-	case "file":
-		b, err = os.ReadFile(u)
-		if err != nil {
-			if !errors.Is(err, fs.ErrNotExist) {
-				return nil, fmt.Errorf("failed to read repository %s: %w", asURL.Redacted(), err)
-			}
-			return nil, nil
-		}
-	case "https", "http":
-		client := opts.httpClient
-		req, err := http.NewRequestWithContext(ctx, http.MethodGet, asURL.String(), nil)
-		if err != nil {
-			return nil, err
-		}
+	b, err := io.ReadAll(res.Body)
+	if err != nil {
+		return nil, fmt.Errorf("reading body: %w", err)
+	}
 
-		if opts.auth == nil {
-			opts.auth = auth.DefaultAuthenticators
-		}
-		if err := opts.auth.AddAuth(ctx, req); err != nil {
-			return nil, fmt.Errorf("unable to add auth to request: %w", err)
-		}
+	return b, nil
+}
 
-		// This will return a body that retries requests using Range requests if Read() hits an error.
-		rrt := newRangeRetryTransport(ctx, client)
-		res, err := rrt.RoundTrip(req)
-		if err != nil {
-			return nil, fmt.Errorf("unable to get repository index at %s: %w", asURL.Redacted(), err)
-		}
-		switch res.StatusCode {
-		case http.StatusOK:
-			// this is fine
-		case http.StatusNotFound:
-			return nil, fmt.Errorf("repository index not found for architecture %s at %s", arch, asURL.Redacted())
-		default:
-			return nil, fmt.Errorf("unexpected status code %d when getting repository index for architecture %s at %s", res.StatusCode, arch, asURL.Redacted())
-		}
-		defer res.Body.Close()
-		buf := bytes.NewBuffer(nil)
-		if _, err := io.Copy(buf, res.Body); err != nil {
-			return nil, fmt.Errorf("unable to read repository index at %s: %w", asURL.Redacted(), err)
-		}
-		b = buf.Bytes()
-	default:
-		return nil, fmt.Errorf("repository scheme %s not supported", asURL.Scheme)
-	}
+func parseRepositoryIndex(ctx context.Context, u string, keys map[string][]byte, arch string, b []byte, opts *indexOpts) (*APKIndex, error) { //nolint:gocyclo
+	_, span := otel.Tracer("go-apk").Start(ctx, "parseRepositoryIndex")
+	defer span.End()
 
 	// validate the signature
 	if shouldCheckSignatureForIndex(u, arch, opts) {
@@ -361,7 +421,7 @@ func getRepositoryIndex(ctx context.Context, u string, keys map[string][]byte, a
 	// with a valid signature, convert it to an ApkIndex
 	index, err := IndexFromArchive(io.NopCloser(bytes.NewReader(b)))
 	if err != nil {
-		return nil, fmt.Errorf("unable to read convert repository index bytes to index struct at %s: %w", asURL.Redacted(), err)
+		return nil, fmt.Errorf("unable to read convert repository index bytes to index struct: %w", err)
 	}
 
 	return index, err
@@ -398,3 +458,19 @@ func WithIndexAuthenticator(a auth.Authenticator) IndexOption {
 		o.auth = a
 	}
 }
+
+func redact(in string) string {
+	asURL, err := url.Parse(in)
+	if err != nil {
+		// Attempt to parse non-https elements into URI's so they are translated into
+		// file:// URLs allowing them to parse into a url.URL{}
+		asURL, err := url.Parse(string(uri.New(in)))
+		if err != nil {
+			return in
+		}
+
+		return asURL.Redacted()
+	}
+
+	return asURL.Redacted()
+}
diff --git a/pkg/apk/apk/install.go b/pkg/apk/apk/install.go
index 9d26480..c537f17 100644
--- a/pkg/apk/apk/install.go
+++ b/pkg/apk/apk/install.go
@@ -242,6 +242,10 @@ func (a *APK) installAPKFiles(ctx context.Context, in io.Reader, pkg *Package) (
 
 			if installed {
 				a.installedFiles[header.Name] = pkg
+
+				if err := a.fs.Chtimes(header.Name, header.AccessTime, header.ModTime); err != nil {
+					return nil, fmt.Errorf("chtimes for %s: %w", header.Name, err)
+				}
 			}
 
 		case tar.TypeSymlink:
diff --git a/pkg/apk/apk/installed.go b/pkg/apk/apk/installed.go
index 141a763..a139bbb 100644
--- a/pkg/apk/apk/installed.go
+++ b/pkg/apk/apk/installed.go
@@ -216,7 +216,7 @@ func (a *APK) updateTriggers(pkg *Package, controlTarGz io.Reader) error {
 	}
 
 	for _, value := range values {
-		if _, err := triggers.Write([]byte(fmt.Sprintf("%s %s\n", base64.StdEncoding.EncodeToString(pkg.Checksum), value))); err != nil {
+		if _, err := triggers.Write([]byte(fmt.Sprintf("Q1%s %s\n", base64.StdEncoding.EncodeToString(pkg.Checksum), value))); err != nil {
 			return fmt.Errorf("unable to write triggers file %s: %w", triggersFilePath, err)
 		}
 	}
diff --git a/pkg/apk/apk/options.go b/pkg/apk/apk/options.go
index 54949ab..223103d 100644
--- a/pkg/apk/apk/options.go
+++ b/pkg/apk/apk/options.go
@@ -83,7 +83,7 @@ func WithFS(fs apkfs.FullFS) Option {
 //
 // If offline is true, only read from the cache and do not make any network requests to
 // populate it.
-func WithCache(cacheDir string, offline bool) Option {
+func WithCache(cacheDir string, offline bool, shared *Cache) Option {
 	return func(o *opts) error {
 		var err error
 		if cacheDir == "" {
@@ -96,6 +96,7 @@ func WithCache(cacheDir string, offline bool) Option {
 		o.cache = &cache{
 			dir:     cacheDir,
 			offline: offline,
+			shared:  shared,
 		}
 		return nil
 	}
diff --git a/pkg/apk/apk/repo.go b/pkg/apk/apk/repo.go
index 823e340..8e34fa6 100644
--- a/pkg/apk/apk/repo.go
+++ b/pkg/apk/apk/repo.go
@@ -21,13 +21,13 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"maps"
 	"path/filepath"
+	"slices"
 	"strings"
 	"sync"
 
 	"go.opentelemetry.io/otel"
-	"golang.org/x/exp/maps"
-	"golang.org/x/exp/slices"
 
 	"github.com/chainguard-dev/clog"
 )
@@ -205,9 +205,22 @@ type PkgResolver struct {
 	installIfMap map[string][]*repositoryPackage // contains any package that should be installed if the named package is installed
 }
 
+// Clone returns a copy of PkgResolver.
+func (p *PkgResolver) Clone() *PkgResolver {
+	return &PkgResolver{
+		indexes:      p.indexes,
+		nameMap:      maps.Clone(p.nameMap),
+		installIfMap: maps.Clone(p.installIfMap),
+	}
+}
+
 // NewPkgResolver creates a new pkgResolver from a list of indexes.
 // The indexes are anything that implements NamedIndex.
 func NewPkgResolver(ctx context.Context, indexes []NamedIndex) *PkgResolver {
+	return globalResolverCache.Get(ctx, indexes)
+}
+
+func newPkgResolver(ctx context.Context, indexes []NamedIndex) *PkgResolver {
 	_, span := otel.Tracer("go-apk").Start(ctx, "NewPkgResolver")
 	defer span.End()
 
@@ -436,55 +449,14 @@ func (p *PkgResolver) constrain(constraints []string, dq map[*RepositoryPackage]
 	return nil
 }
 
-func (p *PkgResolver) disqualifyMissingArchs(otherArchs map[string][]NamedIndex, dq map[*RepositoryPackage]string) {
-	// arch -> name -> set[version]
-	allowablePackages := map[string]map[string]map[string]struct{}{}
-
-	// Build up a map per arch to quickly check existence of a package name+version.
-	for arch, indexes := range otherArchs {
-		allowed := map[string]map[string]struct{}{}
-		for _, index := range indexes {
-			for _, pkg := range index.Packages() {
-				versions, ok := allowed[pkg.Name]
-				if !ok {
-					versions = map[string]struct{}{}
-				}
-				versions[pkg.Version] = struct{}{}
-				allowed[pkg.Name] = versions
-			}
-		}
-		allowablePackages[arch] = allowed
-	}
-
-	// Check each package in this arch against every other arch, disqualifying any that are missing.
-	for _, pkgVersions := range p.nameMap {
-		for _, pkg := range pkgVersions {
-			for arch, allowed := range allowablePackages {
-				versions, ok := allowed[pkg.Name]
-				if !ok {
-					p.disqualify(dq, pkg.RepositoryPackage, fmt.Sprintf("package %q not available for arch %q", pkg.Filename(), arch))
-				}
-				if _, ok := versions[pkg.Version]; !ok {
-					p.disqualify(dq, pkg.RepositoryPackage, fmt.Sprintf("package %q not available for arch %q", pkg.Filename(), arch))
-				}
-			}
-		}
-	}
-}
-
 // GetPackagesWithDependencies get all of the dependencies for the given packages based on the
 // indexes. Does not filter for installed already or not.
-func (p *PkgResolver) GetPackagesWithDependencies(ctx context.Context, packages []string, otherArchs map[string][]NamedIndex) (toInstall []*RepositoryPackage, conflicts []string, err error) {
+func (p *PkgResolver) GetPackagesWithDependencies(ctx context.Context, packages []string, allArchs map[string][]NamedIndex) (toInstall []*RepositoryPackage, conflicts []string, err error) {
 	_, span := otel.Tracer("go-apk").Start(ctx, "GetPackageWithDependencies")
 	defer span.End()
 
 	// Tracks all the packages we have disqualified and the reason we disqualified them.
-	dq := map[*RepositoryPackage]string{}
-
-	// If we are solving in the context of other architectures, we want to disqualify any packages that aren't available in all architectures.
-	if len(otherArchs) != 0 {
-		p.disqualifyMissingArchs(otherArchs, dq)
-	}
+	dq := globalDisqualifyCache.Get(ctx, allArchs)
 
 	// We're going to mutate this as our set of input packages to install, so make a copy.
 	constraints := slices.Clone(packages)
@@ -780,7 +752,7 @@ func (p *PkgResolver) getPackageDependencies(pkg *RepositoryPackage, allowPin st
 			options[dep] = pkgs
 		}
 
-		constraints = maps.Keys(options)
+		constraints = slices.Collect(maps.Keys(options))
 		if len(constraints) == 0 {
 			// Nothing left to solve.
 			continue
@@ -1068,3 +1040,55 @@ func maybedqerror(constraint string, pkgs []*repositoryPackage, dq map[*Reposito
 
 	return fmt.Errorf("could not find constraint %q in indexes", constraint)
 }
+
+func disqualifyDifference(ctx context.Context, byArch map[string][]NamedIndex) map[*RepositoryPackage]string {
+	dq := map[*RepositoryPackage]string{}
+
+	if len(byArch) == 1 {
+		// There is no difference between archs if we have one arch.
+		return dq
+	}
+
+	// arch -> name -> set[version]
+	allowablePackages := map[string]map[string]map[string]struct{}{}
+
+	for arch, indexes := range byArch {
+		// Build up a map per arch to quickly check existence of a package name+version.
+		allowed := map[string]map[string]struct{}{}
+		for _, index := range indexes {
+			for _, pkg := range index.Packages() {
+				versions, ok := allowed[pkg.Name]
+				if !ok {
+					versions = map[string]struct{}{}
+				}
+				versions[pkg.Version] = struct{}{}
+				allowed[pkg.Name] = versions
+			}
+		}
+		allowablePackages[arch] = allowed
+	}
+
+	for arch := range allowablePackages {
+		p := newPkgResolver(ctx, byArch[arch])
+		for otherArch, allowed := range allowablePackages {
+			if otherArch == arch {
+				continue
+			}
+
+			for _, pkgVersions := range p.nameMap {
+				for _, pkg := range pkgVersions {
+					versions, ok := allowed[pkg.Name]
+					if !ok {
+						dq[pkg.RepositoryPackage] = fmt.Sprintf("package %q not available for arch %q", pkg.Filename(), otherArch)
+						continue
+					}
+					if _, ok := versions[pkg.Version]; !ok {
+						dq[pkg.RepositoryPackage] = fmt.Sprintf("package %q not available for arch %q", pkg.Filename(), otherArch)
+					}
+				}
+			}
+		}
+	}
+
+	return dq
+}
diff --git a/pkg/apk/apk/repository.go b/pkg/apk/apk/repository.go
index 0b7f7f5..4810cf0 100644
--- a/pkg/apk/apk/repository.go
+++ b/pkg/apk/apk/repository.go
@@ -19,10 +19,22 @@ func NewRepositoryFromComponents(baseURI, release, repo, arch string) Repository
 
 // WithIndex returns a RepositoryWithIndex object with the
 func (r *Repository) WithIndex(index *APKIndex) *RepositoryWithIndex {
-	return &RepositoryWithIndex{
+	rwi := &RepositoryWithIndex{
 		Repository: r,
 		index:      index,
 	}
+	pkgs := make([]*RepositoryPackage, 0, len(index.Packages))
+	for _, pkg := range index.Packages {
+		rp := &RepositoryPackage{
+			Package:    pkg,
+			repository: rwi,
+		}
+		pkgs = append(pkgs, rp)
+	}
+
+	rwi.pkgs = pkgs
+
+	return rwi
 }
 
 // IndexURI returns the uri of the APKINDEX for this repository
@@ -40,19 +52,12 @@ func (r *Repository) IsRemote() bool {
 type RepositoryWithIndex struct {
 	*Repository
 	index *APKIndex
+	pkgs  []*RepositoryPackage
 }
 
 // Packages returns a list of RepositoryPackage in this repository
-func (r *RepositoryWithIndex) Packages() (pkgs []*RepositoryPackage) {
-	for _, pkg := range r.index.Packages {
-		rp := &RepositoryPackage{
-			Package:    pkg,
-			repository: r,
-		}
-		pkgs = append(pkgs, rp)
-	}
-
-	return
+func (r *RepositoryWithIndex) Packages() []*RepositoryPackage {
+	return r.pkgs
 }
 
 // Count returns the amout of packages that are available in this repository
diff --git a/pkg/apk/apk/shameful_global_caches.go b/pkg/apk/apk/shameful_global_caches.go
new file mode 100644
index 0000000..2e0843f
--- /dev/null
+++ b/pkg/apk/apk/shameful_global_caches.go
@@ -0,0 +1,149 @@
+// Copyright 2023 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apk
+
+import (
+	"context"
+	"maps"
+	"slices"
+	"strings"
+	"sync"
+)
+
+// It is expensive to parse every version in the APKINDEX and grow a bunch of maps.
+// This caches a PkgResolver based on the input []NamedIndex.
+var globalResolverCache = &resolverCache{}
+
+type resolverCache struct {
+	sync.Mutex
+	children map[NamedIndex]*resolverCache
+	pr       *PkgResolver
+}
+
+func (r *resolverCache) find(indexes []NamedIndex) *PkgResolver {
+	if len(indexes) == 0 {
+		return r.pr
+	}
+
+	if r.children == nil {
+		return nil
+	}
+
+	child, ok := r.children[indexes[0]]
+	if !ok {
+		return nil
+	}
+
+	return child.find(indexes[1:])
+}
+
+func (r *resolverCache) fill(indexes []NamedIndex, pr *PkgResolver) {
+	if len(indexes) == 0 {
+		r.pr = pr
+		return
+	}
+
+	if r.children == nil {
+		r.children = make(map[NamedIndex]*resolverCache)
+	}
+
+	child, ok := r.children[indexes[0]]
+	if !ok {
+		child = &resolverCache{}
+		r.children[indexes[0]] = child
+	}
+
+	child.fill(indexes[1:], pr)
+}
+
+func (r *resolverCache) Get(ctx context.Context, indexes []NamedIndex) *PkgResolver {
+	r.Lock()
+	defer r.Unlock()
+
+	if pr := r.find(indexes); pr != nil {
+		return pr.Clone()
+	}
+
+	pr := newPkgResolver(ctx, indexes)
+	r.fill(indexes, pr)
+
+	return pr.Clone()
+}
+
+// It is expensive to compute the complement
+// This a PkgResolver based on the input []NamedIndex.
+var globalDisqualifyCache = &disqualifyCache{}
+
+type disqualifyCache struct {
+	sync.Mutex
+	children map[NamedIndex]*disqualifyCache
+	dq       map[*RepositoryPackage]string
+}
+
+func (r *disqualifyCache) find(indexes []NamedIndex) map[*RepositoryPackage]string {
+	if len(indexes) == 0 {
+		return r.dq
+	}
+
+	if r.children == nil {
+		return nil
+	}
+
+	child, ok := r.children[indexes[0]]
+	if !ok {
+		return nil
+	}
+
+	return child.find(indexes[1:])
+}
+
+func (r *disqualifyCache) fill(indexes []NamedIndex, dq map[*RepositoryPackage]string) {
+	if len(indexes) == 0 {
+		r.dq = dq
+		return
+	}
+
+	if r.children == nil {
+		r.children = make(map[NamedIndex]*disqualifyCache)
+	}
+
+	child, ok := r.children[indexes[0]]
+	if !ok {
+		child = &disqualifyCache{}
+		r.children[indexes[0]] = child
+	}
+
+	child.fill(indexes[1:], dq)
+}
+
+// It is expensive to compute the difference between every architecture.
+// This caches that difference based on the input []NamedIndex for every architecture.
+func (r *disqualifyCache) Get(ctx context.Context, byArch map[string][]NamedIndex) map[*RepositoryPackage]string {
+	r.Lock()
+	defer r.Unlock()
+
+	indexes := slices.Concat(slices.Collect(maps.Values(byArch))...)
+	slices.SortFunc(indexes, func(a, b NamedIndex) int {
+		return strings.Compare(a.Name(), b.Name())
+	})
+	if dq := r.find(indexes); dq != nil {
+		return maps.Clone(dq)
+	}
+
+	dq := disqualifyDifference(ctx, byArch)
+	r.fill(indexes, dq)
+
+	return maps.Clone(dq)
+}
diff --git a/pkg/apk/auth/auth.go b/pkg/apk/auth/auth.go
index 62a100a..b2af1e2 100644
--- a/pkg/apk/auth/auth.go
+++ b/pkg/apk/auth/auth.go
@@ -17,9 +17,9 @@ var DefaultAuthenticators Authenticator = multiAuthenticator{
 	// First, we'll try to use the HTTP_AUTH environment variable if it's set.
 	EnvAuth{},
 	// If both of these envs are set, we'll try to use the k8s token first.
-	NewK8sAuth(os.Getenv("K8S_TOKEN_PATH"), os.Getenv("CHAINGUARD_IDENTITY"), "https://issuer.enforce.dev", "https://apk.cgr.dev"),
+	NewK8sAuth(os.Getenv("K8S_TOKEN_PATH"), os.Getenv("CHAINGUARD_IDENTITY"), "https://issuer.enforce.dev", "apk.cgr.dev"),
 	// If only the identity env is set, and k8s auth didn't work, we'll try to use exchanged GCP auth.
-	NewChainguardIdentityAuth(os.Getenv("CHAINGUARD_IDENTITY"), "https://issuer.enforce.dev", "https://apk.cgr.dev"),
+	NewChainguardIdentityAuth(os.Getenv("CHAINGUARD_IDENTITY"), "https://issuer.enforce.dev", "apk.cgr.dev"),
 	// If nothing has worked yet, we'll try to use chainctl.
 	CGRAuth{},
 }
diff --git a/pkg/apk/auth/chainguard.go b/pkg/apk/auth/chainguard.go
index 06785a8..991d69a 100644
--- a/pkg/apk/auth/chainguard.go
+++ b/pkg/apk/auth/chainguard.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"io/fs"
+	"log/slog"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -11,7 +12,6 @@ import (
 	"time"
 
 	"chainguard.dev/sdk/sts"
-	"github.com/charmbracelet/log"
 	"golang.org/x/time/rate"
 	"google.golang.org/api/idtoken"
 )
@@ -24,9 +24,10 @@ import (
 // Audience is usually https://apk.cgr.dev.
 func NewChainguardIdentityAuth(identity, issuer, audience string) Authenticator {
 	return &cgAuth{
-		id:  identity,
-		iss: issuer,
-		aud: audience,
+		id:        identity,
+		iss:       issuer,
+		aud:       audience,
+		sometimes: rate.Sometimes{Interval: 10 * time.Minute},
 	}
 }
 
@@ -53,7 +54,7 @@ func (a *cgAuth) AddAuth(ctx context.Context, req *http.Request) error {
 			a.cgerr = fmt.Errorf("creating token source: %w", err)
 			return
 		}
-		log.Infof("Exchanging GCP token for Chainguard identity %s", a.id)
+		slog.Default().With("iss", a.iss, "aud", a.aud).Info("Exchanging GCP token for Chainguard identity " + a.id)
 		tok, err := ts.Token()
 		if err != nil {
 			a.cgerr = fmt.Errorf("getting token: %w", err)
@@ -113,7 +114,7 @@ func (k *k8sAuth) AddAuth(ctx context.Context, req *http.Request) error {
 			k.cgerr = fmt.Errorf("reading token: %w", err)
 			return
 		}
-		log.Infof("Exchanging K8s token for Chainguard identity %s", k.id)
+		slog.Default().With("iss", k.iss, "aud", k.aud).Info("Exchanging K8s token for Chainguard identity " + k.id)
 		ctok, err := sts.ExchangePair(ctx, k.iss, k.aud, string(b), sts.WithIdentity(k.id))
 		if err != nil {
 			k.cgerr = fmt.Errorf("exchanging token: %w", err)
diff --git a/pkg/apk/client/client.go b/pkg/apk/client/client.go
index dfb73ff..2a90347 100644
--- a/pkg/apk/client/client.go
+++ b/pkg/apk/client/client.go
@@ -12,8 +12,8 @@ import (
 
 const (
 	WolfiAPKRepo                = "https://packages.wolfi.dev/os"
-	ChainguardEnterpriseAPKRepo = "https://packages.cgr.dev/os"
-	ChainguardExtrasAPKRepo     = "https://packages.cgr.dev/extras"
+	ChainguardEnterpriseAPKRepo = "https://apk.cgr.dev/chainguard-private"
+	ChainguardExtrasAPKRepo     = "https://apk.cgr.dev/extra-packages"
 )
 
 const (
diff --git a/pkg/apk/fs/fs.go b/pkg/apk/fs/fs.go
index 2e3810c..74d3e18 100644
--- a/pkg/apk/fs/fs.go
+++ b/pkg/apk/fs/fs.go
@@ -16,6 +16,7 @@ package fs
 import (
 	"io"
 	"io/fs"
+	"time"
 )
 
 // FullFS is a filesystem that supports all filesystem operations.
@@ -39,6 +40,7 @@ type FullFS interface {
 	Remove(name string) error
 	Chmod(path string, perm fs.FileMode) error
 	Chown(path string, uid int, gid int) error
+	Chtimes(path string, atime time.Time, mtime time.Time) error
 	SetXattr(path string, attr string, data []byte) error
 	GetXattr(path string, attr string) ([]byte, error)
 	RemoveXattr(path string, attr string) error
diff --git a/pkg/apk/fs/memfs.go b/pkg/apk/fs/memfs.go
index a6e50b4..7ae5c19 100644
--- a/pkg/apk/fs/memfs.go
+++ b/pkg/apk/fs/memfs.go
@@ -129,13 +129,11 @@ func (m *memFS) Mkdir(path string, perms fs.FileMode) error {
 	}
 	// now create the directory
 	anode.children[filepath.Base(path)] = &node{
-		name:       filepath.Base(path),
-		mode:       fs.ModeDir | perms,
-		dir:        true,
-		modTime:    time.Now(),
-		createTime: time.Now(),
-		children:   map[string]*node{},
-		xattrs:     map[string][]byte{},
+		name:     filepath.Base(path),
+		mode:     fs.ModeDir | perms,
+		dir:      true,
+		children: map[string]*node{},
+		xattrs:   map[string][]byte{},
 	}
 	return nil
 }
@@ -179,13 +177,11 @@ func (m *memFS) MkdirAll(path string, perm fs.FileMode) error {
 		newnode, ok := anode.children[part]
 		if !ok {
 			newnode = &node{
-				name:       part,
-				mode:       fs.ModeDir | perm,
-				dir:        true,
-				modTime:    time.Now(),
-				createTime: time.Now(),
-				children:   map[string]*node{},
-				xattrs:     map[string][]byte{},
+				name:     part,
+				mode:     fs.ModeDir | perm,
+				dir:      true,
+				children: map[string]*node{},
+				xattrs:   map[string][]byte{},
 			}
 			anode.children[part] = newnode
 		}
@@ -245,12 +241,10 @@ func (m *memFS) openFile(name string, flag int, perm fs.FileMode, linkCount int)
 	if flag&os.O_CREATE != 0 && !ok {
 		// create the file
 		anode = &node{
-			name:       base,
-			mode:       perm,
-			dir:        false,
-			modTime:    time.Now(),
-			createTime: time.Now(),
-			xattrs:     map[string][]byte{},
+			name:   base,
+			mode:   perm,
+			dir:    false,
+			xattrs: map[string][]byte{},
 		}
 		parentAnode.children[base] = anode
 	}
@@ -332,13 +326,11 @@ func (m *memFS) Mknod(path string, mode uint32, dev int) error {
 		return os.ErrExist
 	}
 	anode.children[base] = &node{
-		name:       base,
-		mode:       fs.FileMode(mode) | os.ModeCharDevice | os.ModeDevice,
-		modTime:    time.Now(),
-		createTime: time.Now(),
-		major:      unix.Major(uint64(dev)),
-		minor:      unix.Minor(uint64(dev)),
-		xattrs:     map[string][]byte{},
+		name:   base,
+		mode:   fs.FileMode(mode) | os.ModeCharDevice | os.ModeDevice,
+		major:  unix.Major(uint64(dev)),
+		minor:  unix.Minor(uint64(dev)),
+		xattrs: map[string][]byte{},
 	}
 
 	return nil
@@ -372,6 +364,7 @@ func (m *memFS) Chmod(path string, perm fs.FileMode) error {
 	anode.mode = perm | (anode.mode & os.ModeType)
 	return nil
 }
+
 func (m *memFS) Chown(path string, uid, gid int) error {
 	anode, err := m.getNode(path)
 	if err != nil {
@@ -382,6 +375,15 @@ func (m *memFS) Chown(path string, uid, gid int) error {
 	return nil
 }
 
+func (m *memFS) Chtimes(path string, atime time.Time, mtime time.Time) error {
+	anode, err := m.getNode(path)
+	if err != nil {
+		return err
+	}
+	anode.modTime = mtime
+	return nil
+}
+
 func (m *memFS) Create(name string) (File, error) {
 	return m.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o666)
 }
@@ -401,7 +403,6 @@ func (m *memFS) Symlink(oldname, newname string) error {
 	anode.children[base] = &node{
 		name:       base,
 		mode:       0o777 | os.ModeSymlink,
-		modTime:    time.Now(),
 		linkTarget: oldname,
 		xattrs:     map[string][]byte{},
 	}
@@ -625,7 +626,6 @@ type node struct {
 	name         string
 	data         []byte
 	modTime      time.Time
-	createTime   time.Time
 	linkTarget   string
 	linkCount    int // extra links, so 0 means a single pointer. O-based, like most compuuter counting systems.
 	major, minor uint32
diff --git a/pkg/apk/fs/rwosfs.go b/pkg/apk/fs/rwosfs.go
index 76ca091..9800e6a 100644
--- a/pkg/apk/fs/rwosfs.go
+++ b/pkg/apk/fs/rwosfs.go
@@ -504,6 +504,7 @@ func (f *dirFS) Chmod(path string, perm fs.FileMode) error {
 	}
 	return f.overrides.Chmod(path, perm)
 }
+
 func (f *dirFS) Chown(path string, uid, gid int) error {
 	if f.caseSensitiveOnDisk(path) {
 		// ignore error, as we track it in memory anyways, and disk filesystem might not support it
@@ -512,6 +513,13 @@ func (f *dirFS) Chown(path string, uid, gid int) error {
 	return f.overrides.Chown(path, uid, gid)
 }
 
+func (f *dirFS) Chtimes(path string, atime time.Time, mtime time.Time) error {
+	if err := os.Chtimes(filepath.Join(f.base, path), atime, mtime); err != nil {
+		return fmt.Errorf("unable to change times: %w", err)
+	}
+	return f.overrides.Chtimes(path, atime, mtime)
+}
+
 func (f *dirFS) Mknod(name string, mode uint32, dev int) error {
 	if f.caseSensitiveOnDisk(name) {
 		err := unix.Mknod(filepath.Join(f.base, name), mode, dev)
diff --git a/pkg/apk/signature/rsa.go b/pkg/apk/signature/rsa.go
index d468f4e..a1378fd 100644
--- a/pkg/apk/signature/rsa.go
+++ b/pkg/apk/signature/rsa.go
@@ -99,11 +99,3 @@ func RSAVerifyDigest(digest []byte, digestType crypto.Hash, signature []byte, pu
 
 	return nil
 }
-
-// Backwards compat
-func RSASignSHA1Digest(digest []byte, keyFile, passphrase string) ([]byte, error) {
-	return RSASignDigest(digest, crypto.SHA1, keyFile, passphrase)
-}
-func RSAVerifySHA1Digest(digest, signature []byte, publicKey []byte) error {
-	return RSAVerifyDigest(digest, crypto.SHA1, signature, publicKey)
-}
diff --git a/pkg/apk/tarball/tarball.go b/pkg/apk/tarball/tarball.go
index f2bdb27..2c71dad 100644
--- a/pkg/apk/tarball/tarball.go
+++ b/pkg/apk/tarball/tarball.go
@@ -20,17 +20,19 @@ import (
 )
 
 type Context struct {
-	SourceDateEpoch time.Time
-	OverrideUIDGID  bool
-	UID             int
-	GID             int
-	OverrideUname   string
-	OverrideGname   string
-	SkipClose       bool
-	UseChecksums    bool
-	remapUIDs       map[int]int
-	remapGIDs       map[int]int
-	overridePerms   map[string]tar.Header
+	SourceDateEpoch    time.Time
+	useSourceDateEpoch bool
+
+	OverrideUIDGID bool
+	UID            int
+	GID            int
+	OverrideUname  string
+	OverrideGname  string
+	SkipClose      bool
+	UseChecksums   bool
+	remapUIDs      map[int]int
+	remapGIDs      map[int]int
+	overridePerms  map[string]tar.Header
 }
 
 type Option func(*Context) error
@@ -52,6 +54,10 @@ func NewContext(opts ...Option) (*Context, error) {
 func WithSourceDateEpoch(t time.Time) Option {
 	return func(ctx *Context) error {
 		ctx.SourceDateEpoch = t
+
+		// to distinguish between unset and zero value
+		ctx.useSourceDateEpoch = true
+
 		return nil
 	}
 }
diff --git a/pkg/apk/tarball/write.go b/pkg/apk/tarball/write.go
index 82fcb50..6a89632 100644
--- a/pkg/apk/tarball/write.go
+++ b/pkg/apk/tarball/write.go
@@ -148,10 +148,15 @@ func (c *Context) writeTar(ctx context.Context, tw *tar.Writer, fsys fs.FS, user
 		// work around some weirdness, without this we wind up with just the basename
 		header.Name = path
 
-		// zero out timestamps for reproducibility
-		header.AccessTime = c.SourceDateEpoch
-		header.ModTime = c.SourceDateEpoch
-		header.ChangeTime = c.SourceDateEpoch
+		// if SourceDateEpoch is set explicitly, overwrite the timestamps, otherwise propagate modtime
+		// (this option is unused in apko but melange uses this, so we support it for now)
+		if c.useSourceDateEpoch {
+			header.AccessTime = c.SourceDateEpoch
+			header.ModTime = c.SourceDateEpoch
+			header.ChangeTime = c.SourceDateEpoch
+		} else {
+			header.ModTime = info.ModTime()
+		}
 
 		if uid, ok := c.remapUIDs[header.Uid]; ok {
 			header.Uid = uid
diff --git a/pkg/build/build.go b/pkg/build/build.go
index a723ad5..76cb47d 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -257,9 +257,9 @@ func New(ctx context.Context, fs apkfs.FullFS, opts ...Option) (*Context, error)
 	// note that this is not easy to do in a switch statement, because of the second
 	// condition, if err := ...; err == nil {}
 	if bc.o.CacheDir != "" {
-		apkOpts = append(apkOpts, apk.WithCache(bc.o.CacheDir, bc.o.Offline))
+		apkOpts = append(apkOpts, apk.WithCache(bc.o.CacheDir, bc.o.Offline, bc.o.SharedCache))
 	} else if _, err := os.UserCacheDir(); err == nil {
-		apkOpts = append(apkOpts, apk.WithCache(bc.o.CacheDir, bc.o.Offline))
+		apkOpts = append(apkOpts, apk.WithCache(bc.o.CacheDir, bc.o.Offline, bc.o.SharedCache))
 	} else {
 		log.Warnf("cache disabled because cache dir was not set, and cannot determine system default: %v", err)
 	}
@@ -305,6 +305,18 @@ func New(ctx context.Context, fs apkfs.FullFS, opts ...Option) (*Context, error)
 	return &bc, nil
 }
 
+type notAFile struct {
+	rc *os.File
+}
+
+func (f *notAFile) Read(p []byte) (int, error) {
+	return f.rc.Read(p)
+}
+
+func (f *notAFile) Close() error {
+	return f.rc.Close()
+}
+
 // layer implements v1.Layer from go-containerregistry to avoid re-computing
 // digests and diffids.
 type layer struct {
@@ -322,7 +334,14 @@ func (l *layer) Digest() (v1.Hash, error) {
 }
 
 func (l *layer) Compressed() (io.ReadCloser, error) {
-	return os.Open(l.filename)
+	f, err := os.Open(l.filename)
+	if err != nil {
+		return nil, err
+	}
+
+	// There is a bug in how go uses sendfile on macos, so we need to make this not a file.
+	// See https://github.com/golang/go/issues/70000
+	return &notAFile{f}, nil
 }
 
 func (l *layer) Uncompressed() (io.ReadCloser, error) {
diff --git a/pkg/build/build_implementation.go b/pkg/build/build_implementation.go
index cbd50fc..2392d98 100644
--- a/pkg/build/build_implementation.go
+++ b/pkg/build/build_implementation.go
@@ -77,10 +77,7 @@ func (bc *Context) BuildTarball(ctx context.Context) (string, hash.Hash, hash.Ha
 	bc.o.TarballPath = outfile.Name()
 	defer outfile.Close()
 
-	// we use a general override of 0,0 for all files, but the specific overrides, that come from the installed package DB, come later
-	tw, err := tarball.NewContext(
-		tarball.WithSourceDateEpoch(bc.o.SourceDateEpoch),
-	)
+	tw, err := tarball.NewContext()
 	if err != nil {
 		return "", nil, nil, 0, fmt.Errorf("failed to construct tarball build context: %w", err)
 	}
diff --git a/pkg/build/multi.go b/pkg/build/multi.go
index 4029984..d33ea10 100644
--- a/pkg/build/multi.go
+++ b/pkg/build/multi.go
@@ -58,7 +58,7 @@ func NewMultiArch(ctx context.Context, archs []types.Architecture, opts ...Optio
 	}
 
 	for _, bc := range m.Contexts {
-		bc.apk.Others = apks
+		bc.apk.ByArch = apks
 	}
 
 	return m, nil
diff --git a/pkg/build/oci/image.go b/pkg/build/oci/image.go
index 92da7e5..43db805 100644
--- a/pkg/build/oci/image.go
+++ b/pkg/build/oci/image.go
@@ -39,9 +39,15 @@ import (
 	"chainguard.dev/apko/pkg/options"
 )
 
-func BuildImageFromLayer(ctx context.Context, baseImage v1.Image, layer v1.Layer, ic types.ImageConfiguration, created time.Time, arch types.Architecture) (oci.SignedImage, error) {
+func BuildImageFromLayer(ctx context.Context, baseImage v1.Image, layer v1.Layer, oic types.ImageConfiguration, created time.Time, arch types.Architecture) (oci.SignedImage, error) {
 	log := clog.FromContext(ctx)
 
+	// Create a copy to avoid modifying the original ImageConfiguration.
+	ic := &types.ImageConfiguration{}
+	if err := oic.MergeInto(ic); err != nil {
+		return nil, err
+	}
+
 	mediaType, err := layer.MediaType()
 	if err != nil {
 		return nil, fmt.Errorf("accessing layer MediaType: %w", err)
@@ -93,6 +99,7 @@ func BuildImageFromLayer(ctx context.Context, baseImage v1.Image, layer v1.Layer
 			annotations["org.opencontainers.image.revision"] = hash
 		}
 	}
+	annotations["org.opencontainers.image.created"] = created.Format(time.RFC3339)
 
 	if mediaType != ggcrtypes.DockerLayer && len(annotations) > 0 {
 		v1Image = mutate.Annotations(v1Image, annotations).(v1.Image)
diff --git a/pkg/build/oci/index.go b/pkg/build/oci/index.go
index 5399891..bfb0eb1 100644
--- a/pkg/build/oci/index.go
+++ b/pkg/build/oci/index.go
@@ -23,6 +23,7 @@ import (
 	"os"
 	"sort"
 	"strings"
+	"time"
 
 	"github.com/google/go-containerregistry/pkg/name"
 	v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -41,41 +42,41 @@ import (
 // GenerateIndex generates an OCI image index from the given imgs. The index type
 // will be "application/vnd.oci.image.index.v1+json".
 // The index is stored in memory.
-func GenerateIndex(ctx context.Context, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage) (name.Digest, oci.SignedImageIndex, error) {
+func GenerateIndex(ctx context.Context, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage, created time.Time) (name.Digest, oci.SignedImageIndex, error) {
 	_, span := otel.Tracer("apko").Start(ctx, "GenerateIndex")
 	defer span.End()
 
-	return generateIndexWithMediaType(ggcrtypes.OCIImageIndex, ic, imgs)
+	return generateIndexWithMediaType(ggcrtypes.OCIImageIndex, ic, imgs, created)
 }
 
 // GenerateDockerIndex generates a docker multi-arch manifest from the given imgs. The index type
 // will be "application/vnd.docker.distribution.manifest.list.v2+json".
 // The index is stored in memory.
-func GenerateDockerIndex(ctx context.Context, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage) (name.Digest, oci.SignedImageIndex, error) {
-	return generateIndexWithMediaType(ggcrtypes.DockerManifestList, ic, imgs)
+func GenerateDockerIndex(ctx context.Context, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage, created time.Time) (name.Digest, oci.SignedImageIndex, error) {
+	return generateIndexWithMediaType(ggcrtypes.DockerManifestList, ic, imgs, created)
 }
 
 // generateIndexWithMediaType generates an index or docker manifest list from the given imgs. The index type
 // is provided by the `mediaType` parameter.
-func generateIndexWithMediaType(mediaType ggcrtypes.MediaType, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage) (name.Digest, oci.SignedImageIndex, error) {
+func generateIndexWithMediaType(mediaType ggcrtypes.MediaType, ic types.ImageConfiguration, imgs map[types.Architecture]oci.SignedImage, created time.Time) (name.Digest, oci.SignedImageIndex, error) {
 	// If annotations are set and we're using the OCI mediaType, set annotations on the index.
-	annotations := map[string]string{}
+	annCopy := make(map[string]string, len(ic.Annotations))
 	if mediaType == ggcrtypes.OCIImageIndex {
-		annotations = ic.Annotations
-		if annotations == nil {
-			annotations = map[string]string{}
+		for k, v := range ic.Annotations {
+			annCopy[k] = v
 		}
 		if ic.VCSUrl != "" {
 			if url, hash, ok := strings.Cut(ic.VCSUrl, "@"); ok {
-				annotations["org.opencontainers.image.source"] = url
-				annotations["org.opencontainers.image.revision"] = hash
+				annCopy["org.opencontainers.image.source"] = url
+				annCopy["org.opencontainers.image.revision"] = hash
 			}
 		}
+		annCopy["org.opencontainers.image.created"] = created.Format(time.RFC3339)
 	}
 
 	idx := signed.ImageIndex(
 		mutate.IndexMediaType(
-			mutate.Annotations(empty.Index, annotations).(v1.ImageIndex),
+			mutate.Annotations(empty.Index, annCopy).(v1.ImageIndex),
 			mediaType),
 	)
 	archs := make([]types.Architecture, 0, len(imgs))
diff --git a/pkg/build/options.go b/pkg/build/options.go
index d91c97d..2045768 100644
--- a/pkg/build/options.go
+++ b/pkg/build/options.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"time"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	"chainguard.dev/apko/pkg/apk/auth"
 	"chainguard.dev/apko/pkg/build/types"
 
@@ -189,11 +190,12 @@ func WithAnnotations(annotations map[string]string) Option {
 	}
 }
 
-// WithCacheDir set the cache directory to use
-func WithCacheDir(cacheDir string, offline bool) Option {
+// WithCache set the cache directory to use
+func WithCache(cacheDir string, offline bool, shared *apk.Cache) Option {
 	return func(bc *Context) error {
 		bc.o.CacheDir = cacheDir
 		bc.o.Offline = offline
+		bc.o.SharedCache = shared
 		return nil
 	}
 }
diff --git a/pkg/build/types/image_configuration.go b/pkg/build/types/image_configuration.go
index 776c8cb..a275330 100644
--- a/pkg/build/types/image_configuration.go
+++ b/pkg/build/types/image_configuration.go
@@ -18,8 +18,10 @@ import (
 	"context"
 	"fmt"
 	"hash"
+	"maps"
 	"os"
 	"reflect"
+	"slices"
 	"strings"
 
 	"github.com/google/go-cmp/cmp"
@@ -124,7 +126,7 @@ func (ic *ImageConfiguration) MergeInto(target *ImageConfiguration) error {
 		return err
 	}
 	if target.Environment == nil && ic.Environment != nil {
-		target.Environment = ic.Environment
+		target.Environment = maps.Clone(ic.Environment)
 	} else {
 		for k, v := range ic.Environment {
 			if _, ok := target.Environment[k]; !ok {
@@ -132,9 +134,9 @@ func (ic *ImageConfiguration) MergeInto(target *ImageConfiguration) error {
 			}
 		}
 	}
-	target.Paths = append(ic.Paths, target.Paths...)
+	target.Paths = slices.Concat(ic.Paths, target.Paths)
 	if target.Annotations == nil && ic.Annotations != nil {
-		target.Annotations = ic.Annotations
+		target.Annotations = maps.Clone(ic.Annotations)
 	} else {
 		for k, v := range ic.Annotations {
 			if _, ok := target.Annotations[k]; !ok {
@@ -151,16 +153,16 @@ func (a *ImageAccounts) MergeInto(target *ImageAccounts) error {
 	if target.RunAs == "" {
 		target.RunAs = a.RunAs
 	}
-	target.Users = append(a.Users, target.Users...)
-	target.Groups = append(a.Groups, target.Groups...)
+	target.Users = slices.Concat(a.Users, target.Users)
+	target.Groups = slices.Concat(a.Groups, target.Groups)
 	return nil
 }
 
 func (i *ImageContents) MergeInto(target *ImageContents) error {
-	target.Keyring = append(i.Keyring, target.Keyring...)
-	target.BuildRepositories = append(i.BuildRepositories, target.BuildRepositories...)
-	target.RuntimeRepositories = append(i.RuntimeRepositories, target.RuntimeRepositories...)
-	target.Packages = append(i.Packages, target.Packages...)
+	target.Keyring = slices.Concat(i.Keyring, target.Keyring)
+	target.BuildRepositories = slices.Concat(i.BuildRepositories, target.BuildRepositories)
+	target.RuntimeRepositories = slices.Concat(i.RuntimeRepositories, target.RuntimeRepositories)
+	target.Packages = slices.Concat(i.Packages, target.Packages)
 	return nil
 }
 
diff --git a/pkg/options/options.go b/pkg/options/options.go
index 8ca7692..9bce658 100644
--- a/pkg/options/options.go
+++ b/pkg/options/options.go
@@ -21,6 +21,7 @@ import (
 	"runtime"
 	"time"
 
+	"chainguard.dev/apko/pkg/apk/apk"
 	"chainguard.dev/apko/pkg/apk/auth"
 	"chainguard.dev/apko/pkg/build/types"
 )
@@ -49,6 +50,7 @@ type Options struct {
 	Local                   bool               `json:"local,omitempty"`
 	CacheDir                string             `json:"cacheDir,omitempty"`
 	Offline                 bool               `json:"offline,omitempty"`
+	SharedCache             *apk.Cache         `json:"-"`
 	Lockfile                string             `json:"lockfile,omitempty"`
 	Auth                    auth.Authenticator `json:"-"`
 	IncludePaths            []string           `json:"includePaths,omitempty"`
@@ -61,6 +63,7 @@ var Default = Options{
 	Arch:            types.ParseArchitecture(runtime.GOARCH),
 	SourceDateEpoch: time.Unix(0, 0).UTC(),
 	Auth:            auth.DefaultAuthenticators,
+	SharedCache:     apk.NewCache(false),
 }
 
 // Tempdir returns the temporary directory where apko will create
diff --git a/pkg/sbom/generator/spdx/spdx.go b/pkg/sbom/generator/spdx/spdx.go
index 3cbf81a..2eedaf4 100644
--- a/pkg/sbom/generator/spdx/spdx.go
+++ b/pkg/sbom/generator/spdx/spdx.go
@@ -24,6 +24,7 @@ import (
 	"time"
 	"unicode/utf8"
 
+	"github.com/charmbracelet/log"
 	purl "github.com/package-url/packageurl-go"
 	"sigs.k8s.io/release-utils/version"
 
@@ -137,6 +138,18 @@ func (sx *SPDX) Generate(opts *options.Options, path string) error {
 		}
 	}
 
+	dedupedPackages := make([]Package, 0, len(doc.Packages))
+	seenIDs := make(map[string]struct{})
+	for i := range doc.Packages {
+		if _, ok := seenIDs[doc.Packages[i].ID]; !ok {
+			seenIDs[doc.Packages[i].ID] = struct{}{}
+			dedupedPackages = append(dedupedPackages, doc.Packages[i])
+		} else {
+			log.Info("duplicate package ID found in SBOM, deduplicating package...", "ID", doc.Packages[i].ID)
+		}
+	}
+	doc.Packages = dedupedPackages
+
 	if err := renderDoc(doc, path); err != nil {
 		return fmt.Errorf("rendering document: %w", err)
 	}
@@ -178,7 +191,9 @@ func replacePackage(doc *Document, originalID, newID string) {
 	}
 }
 
-// locateApkSBOM returns the SBOM
+// locateApkSBOM returns the path to the SBOM in the given filesystem, using the
+// given Package's name and version. It returns an empty string if the SBOM is
+// not found.
 func locateApkSBOM(fsys apkfs.FullFS, p *Package) (string, error) {
 	re := regexp.MustCompile(`-r\d+$`)
 	for _, s := range []string{
@@ -209,51 +224,53 @@ func (sx *SPDX) ProcessInternalApkSBOM(opts *options.Options, doc *Document, p *
 		return fmt.Errorf("inspecting FS for internal apk SBOM: %w", err)
 	}
 	if path == "" {
+		// The SBOM does not exist.
+		// (So just ignore that the package was specified to the SPDX Generate method?)
 		return nil
 	}
 
-	internalDoc, err := sx.ParseInternalSBOM(opts, path)
+	apkSBOMDoc, err := sx.ParseInternalSBOM(opts, path)
 	if err != nil {
 		// TODO: Log error parsing apk SBOM
 		return nil
 	}
 
 	// Cycle the top level elements...
-	elementIDs := map[string]struct{}{}
-	for _, elementID := range internalDoc.DocumentDescribes {
-		elementIDs[elementID] = struct{}{}
+	idsDescribedByAPKSBOM := map[string]struct{}{}
+	for _, elementID := range apkSBOMDoc.DocumentDescribes {
+		idsDescribedByAPKSBOM[elementID] = struct{}{}
 	}
 
 	// ... searching for a 1st level package
 	targetElementIDs := map[string]struct{}{}
-	for _, pkg := range internalDoc.Packages {
+	for _, pkg := range apkSBOMDoc.Packages {
 		// that matches the name
 		if p.Name != pkg.Name {
 			continue
 		}
 
-		if _, ok := elementIDs[pkg.ID]; !ok {
+		if _, ok := idsDescribedByAPKSBOM[pkg.ID]; !ok {
 			continue
 		}
 
 		targetElementIDs[pkg.ID] = struct{}{}
-		if len(targetElementIDs) == len(elementIDs) {
+		if len(targetElementIDs) == len(idsDescribedByAPKSBOM) {
 			// Exit early if we found them all.
 			break
 		}
 	}
 
 	// Copy the targetElementIDs
-	todo := make(map[string]struct{}, len(internalDoc.Relationships))
+	todo := make(map[string]struct{}, len(apkSBOMDoc.Relationships))
 	for id := range targetElementIDs {
 		todo[id] = struct{}{}
 	}
 
-	if err := copySBOMElements(internalDoc, doc, todo); err != nil {
+	if err := copySBOMElements(apkSBOMDoc, doc, todo); err != nil {
 		return fmt.Errorf("copying element: %w", err)
 	}
 
-	if err := mergeLicensingInfos(internalDoc, doc); err != nil {
+	if err := mergeLicensingInfos(apkSBOMDoc, doc); err != nil {
 		return fmt.Errorf("merging LicensingInfos: %w", err)
 	}
 
@@ -432,6 +449,10 @@ func (sx *SPDX) imagePackage(opts *options.Options) (p *Package) {
 
 // apkPackage returns a SPDX package describing an apk
 func (sx *SPDX) apkPackage(opts *options.Options, pkg *apk.InstalledPackage) Package {
+	url := pkg.URL
+	if url == "" {
+		url = NOASSERTION
+	}
 	return Package{
 		ID: stringToIdentifier(fmt.Sprintf(
 			"SPDXRef-Package-%s-%s", pkg.Name, pkg.Version,
@@ -442,7 +463,7 @@ func (sx *SPDX) apkPackage(opts *options.Options, pkg *apk.InstalledPackage) Pac
 		FilesAnalyzed:    false,
 		LicenseConcluded: pkg.License,
 		Description:      pkg.Description,
-		DownloadLocation: pkg.URL,
+		DownloadLocation: url,
 		Originator:       fmt.Sprintf("Person: %s", pkg.Maintainer),
 		SourceInfo:       "Package info from apk database",
 		Checksums: []Checksum{
@@ -545,7 +566,7 @@ type Package struct {
 	LicenseConcluded string                   `json:"licenseConcluded,omitempty"`
 	LicenseDeclared  string                   `json:"licenseDeclared,omitempty"`
 	Description      string                   `json:"description,omitempty"`
-	DownloadLocation string                   `json:"downloadLocation,omitempty"`
+	DownloadLocation string                   `json:"downloadLocation"`
 	Originator       string                   `json:"originator,omitempty"`
 	Supplier         string                   `json:"supplier,omitempty"`
 	SourceInfo       string                   `json:"sourceInfo,omitempty"`
diff --git a/pkg/sbom/options/options.go b/pkg/sbom/options/options.go
index ebaacaf..a74a26f 100644
--- a/pkg/sbom/options/options.go
+++ b/pkg/sbom/options/options.go
@@ -51,7 +51,7 @@ type Options struct {
 	// Formats dictates which SBOM formats we will output
 	Formats []string
 
-	// Packages is alist of packages which will be listed in the SBOM
+	// Packages is a list of packages which will be listed in the SBOM
 	Packages []*apk.InstalledPackage
 }
 
diff --git a/pkg/tarfs/fs.go b/pkg/tarfs/fs.go
index d8f77d0..5b07bf4 100644
--- a/pkg/tarfs/fs.go
+++ b/pkg/tarfs/fs.go
@@ -259,14 +259,12 @@ func (m *memFS) Mkdir(path string, perms fs.FileMode) error {
 	}
 	// now create the directory
 	anode.children[filepath.Base(path)] = &node{
-		name:       filepath.Base(path),
-		mode:       fs.ModeDir | perms,
-		dir:        true,
-		modTime:    time.Now(),
-		createTime: time.Now(),
-		children:   map[string]*node{},
-		xattrs:     map[string][]byte{},
-		hardlinks:  map[string]*tar.Header{},
+		name:      filepath.Base(path),
+		mode:      fs.ModeDir | perms,
+		dir:       true,
+		children:  map[string]*node{},
+		xattrs:    map[string][]byte{},
+		hardlinks: map[string]*tar.Header{},
 	}
 	return nil
 }
@@ -310,14 +308,12 @@ func (m *memFS) MkdirAll(path string, perm fs.FileMode) error {
 		newnode, ok := anode.children[part]
 		if !ok {
 			newnode = &node{
-				name:       part,
-				mode:       fs.ModeDir | perm,
-				dir:        true,
-				modTime:    time.Now(),
-				createTime: time.Now(),
-				children:   map[string]*node{},
-				xattrs:     map[string][]byte{},
-				hardlinks:  map[string]*tar.Header{},
+				name:      part,
+				mode:      fs.ModeDir | perm,
+				dir:       true,
+				children:  map[string]*node{},
+				xattrs:    map[string][]byte{},
+				hardlinks: map[string]*tar.Header{},
 			}
 			anode.children[part] = newnode
 		}
@@ -383,13 +379,11 @@ func (m *memFS) openFile(name string, flag int, perm fs.FileMode, linkCount int)
 		if !ok {
 			// create the file
 			anode = &node{
-				name:       base,
-				mode:       perm,
-				dir:        false,
-				modTime:    time.Now(),
-				createTime: time.Now(),
-				xattrs:     map[string][]byte{},
-				hardlinks:  map[string]*tar.Header{},
+				name:      base,
+				mode:      perm,
+				dir:       false,
+				xattrs:    map[string][]byte{},
+				hardlinks: map[string]*tar.Header{},
 			}
 			parentAnode.children[base] = anode
 		}
@@ -461,8 +455,7 @@ func (m *memFS) writeHeader(name string, te tarEntry) (bool, error) {
 			name:       base,
 			mode:       te.header.FileInfo().Mode(),
 			dir:        false,
-			modTime:    time.Now(),
-			createTime: time.Now(),
+			modTime:    te.header.ModTime,
 			linkTarget: te.header.Linkname,
 			xattrs:     map[string][]byte{},
 			hardlinks:  map[string]*tar.Header{},
@@ -526,8 +519,7 @@ func (m *memFS) writeHeader(name string, te tarEntry) (bool, error) {
 		name:       base,
 		mode:       te.header.FileInfo().Mode(),
 		dir:        false,
-		modTime:    time.Now(),
-		createTime: time.Now(),
+		modTime:    te.header.ModTime,
 		linkTarget: te.header.Linkname,
 		xattrs:     map[string][]byte{},
 		hardlinks:  map[string]*tar.Header{},
@@ -600,14 +592,12 @@ func (m *memFS) Mknod(path string, mode uint32, dev int) error {
 		return fs.ErrExist
 	}
 	anode.children[base] = &node{
-		name:       base,
-		mode:       fs.FileMode(mode) | os.ModeCharDevice | os.ModeDevice,
-		modTime:    time.Now(),
-		createTime: time.Now(),
-		major:      unix.Major(uint64(dev)),
-		minor:      unix.Minor(uint64(dev)),
-		xattrs:     map[string][]byte{},
-		hardlinks:  map[string]*tar.Header{},
+		name:      base,
+		mode:      fs.FileMode(mode) | os.ModeCharDevice | os.ModeDevice,
+		major:     unix.Major(uint64(dev)),
+		minor:     unix.Minor(uint64(dev)),
+		xattrs:    map[string][]byte{},
+		hardlinks: map[string]*tar.Header{},
 	}
 
 	return nil
@@ -641,6 +631,7 @@ func (m *memFS) Chmod(path string, perm fs.FileMode) error {
 	anode.mode = perm | (anode.mode & os.ModeType)
 	return nil
 }
+
 func (m *memFS) Chown(path string, uid, gid int) error {
 	anode, err := m.getNode(path)
 	if err != nil {
@@ -651,6 +642,15 @@ func (m *memFS) Chown(path string, uid, gid int) error {
 	return nil
 }
 
+func (m *memFS) Chtimes(path string, atime time.Time, mtime time.Time) error {
+	anode, err := m.getNode(path)
+	if err != nil {
+		return err
+	}
+	anode.modTime = mtime
+	return nil
+}
+
 func (m *memFS) Create(name string) (apkfs.File, error) {
 	return m.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0o666)
 }
@@ -670,7 +670,6 @@ func (m *memFS) Symlink(oldname, newname string) error {
 	anode.children[base] = &node{
 		name:       base,
 		mode:       0o777 | os.ModeSymlink,
-		modTime:    time.Now(),
 		linkTarget: oldname,
 		xattrs:     map[string][]byte{},
 		hardlinks:  map[string]*tar.Header{},
@@ -932,7 +931,6 @@ type node struct {
 	name         string
 	data         []byte
 	modTime      time.Time
-	createTime   time.Time
 	linkTarget   string
 	linkCount    int // extra links, so 0 means a single pointer. O-based, like most compuuter counting systems.
 	major, minor uint32


Test patch:
diff --git a/.github/workflows/go-tests.yaml b/.github/workflows/go-tests.yaml
index 41c9c6a..4dc0864 100644
--- a/.github/workflows/go-tests.yaml
+++ b/.github/workflows/go-tests.yaml
@@ -19,7 +19,7 @@ jobs:
           egress-policy: audit
 
       - name: Checkout code
-        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
       - name: Install Go
         uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v3.0.0
diff --git a/internal/cli/build_test.go b/internal/cli/build_test.go
index b48566f..8bbfa5d 100644
--- a/internal/cli/build_test.go
+++ b/internal/cli/build_test.go
@@ -105,6 +105,8 @@ func TestBuild(t *testing.T) {
 }
 
 func TestBuildWithBase(t *testing.T) {
+	t.Skip("we can't easily update the golden image, so this test is disabled for now")
+
 	// TODO(sfc-gh-mhazy) Check sboms after base image support is reflected in them.
 
 	ctx := context.Background()
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 0cb61c0..a117663 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -91,7 +91,7 @@ func TestPublish(t *testing.T) {
 
 	// This test will fail if we ever make a change in apko that changes the image.
 	// Sometimes, this is intentional, and we need to change this and bump the version.
-	want := "sha256:04fd170ec7fc5904638c1ebb09196f2329d06eae68c8b4dd9ac7fb1cf5fbb7cc"
+	want := "sha256:f09e60c2dabb44047225149ccc4a6cdffac23aabf50352931ddd618705e82f2d"
 	require.Equal(t, want, digest.String())
 
 	sdst := fmt.Sprintf("%s:%s.sbom", dst, strings.ReplaceAll(want, ":", "-"))
@@ -109,7 +109,7 @@ func TestPublish(t *testing.T) {
 
 	// This test will fail if we ever make a change in apko that changes the SBOM.
 	// Sometimes, this is intentional, and we need to change this and bump the version.
-	swant := "sha256:21e2b7e0f29f989a9da47b397048a6543bcd394931ecf3818e757aebdf8b0123"
+	swant := "sha256:194bbad4bd7a37e206126b24d63fd0146434b1abbad9afa567fab364c0d5e608"
 	require.Equal(t, swant, got)
 
 	im, err := idx.IndexManifest()
@@ -118,8 +118,8 @@ func TestPublish(t *testing.T) {
 	// We also want to check the children SBOMs because the index SBOM does not have
 	// references to the children SBOMs, just the children!
 	wantBoms := []string{
-		"sha256:c1e01ba7017c59abf15295bc031aecff632aa01ec855c08c5db037a030b17e86",
-		"sha256:432bb01194d2761ee239380bd8f1b281d45d7edb3389e797c3c8b9f477b2284c",
+		"sha256:3bb2af9d9909bc82e537ac17c5283c8e60589b77ada27ec0ad94964709c7d076",
+		"sha256:d3106b5778c36eed5a87d41b7d62234f598917846c3c8d3478a69d5c1f7d4199",
 	}
 
 	for i, m := range im.Manifests {
diff --git a/internal/cli/testdata/golden/blobs/sha256/e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926 b/internal/cli/testdata/golden/blobs/sha256/a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675
similarity index 53%
rename from internal/cli/testdata/golden/blobs/sha256/e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926
rename to internal/cli/testdata/golden/blobs/sha256/a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675
index 5f78ee1..98684a1 100644
--- a/internal/cli/testdata/golden/blobs/sha256/e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926
+++ b/internal/cli/testdata/golden/blobs/sha256/a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675
@@ -1,1 +1,1 @@
-{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":524,"digest":"sha256:684cd13c05ce23fe0a4ce616b40aa4d027ee38ebc7e393a361ecd0f301ea55a8"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3100,"digest":"sha256:16e741f38ee9768330001d0b12b34006d5bdf90bc2dd72716bca4446e66b6ac6"}]}
\ No newline at end of file
+{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:bbaad1e21e057125420f82a99f24337b25e483d9332e6d6e2485a71562bd2900"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3100,"digest":"sha256:16e741f38ee9768330001d0b12b34006d5bdf90bc2dd72716bca4446e66b6ac6"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}
\ No newline at end of file
diff --git a/internal/cli/testdata/golden/blobs/sha256/a8c325d93bf7fede3a4fac598ea0d05b7a56d702efbed5cbcf1f40c3e7ad7076 b/internal/cli/testdata/golden/blobs/sha256/b12d8e8b08c2b824fa25d5d797d1aa95a424968f8136392f6a5476cc89577fbb
similarity index 86%
rename from internal/cli/testdata/golden/blobs/sha256/a8c325d93bf7fede3a4fac598ea0d05b7a56d702efbed5cbcf1f40c3e7ad7076
rename to internal/cli/testdata/golden/blobs/sha256/b12d8e8b08c2b824fa25d5d797d1aa95a424968f8136392f6a5476cc89577fbb
index 82e5513..6622935 100644
--- a/internal/cli/testdata/golden/blobs/sha256/a8c325d93bf7fede3a4fac598ea0d05b7a56d702efbed5cbcf1f40c3e7ad7076
+++ b/internal/cli/testdata/golden/blobs/sha256/b12d8e8b08c2b824fa25d5d797d1aa95a424968f8136392f6a5476cc89577fbb
@@ -1,1 +1,1 @@
-{"architecture":"arm64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:efe1fac5d8d6bcadf61026012fa98c321eb53299f22f02b1fe5ed4c22f2995cf"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"]}}
\ No newline at end of file
+{"architecture":"arm64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:efe1fac5d8d6bcadf61026012fa98c321eb53299f22f02b1fe5ed4c22f2995cf"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}}
\ No newline at end of file
diff --git a/internal/cli/testdata/golden/blobs/sha256/684cd13c05ce23fe0a4ce616b40aa4d027ee38ebc7e393a361ecd0f301ea55a8 b/internal/cli/testdata/golden/blobs/sha256/bbaad1e21e057125420f82a99f24337b25e483d9332e6d6e2485a71562bd2900
similarity index 86%
rename from internal/cli/testdata/golden/blobs/sha256/684cd13c05ce23fe0a4ce616b40aa4d027ee38ebc7e393a361ecd0f301ea55a8
rename to internal/cli/testdata/golden/blobs/sha256/bbaad1e21e057125420f82a99f24337b25e483d9332e6d6e2485a71562bd2900
index 1dac546..6fbf690 100644
--- a/internal/cli/testdata/golden/blobs/sha256/684cd13c05ce23fe0a4ce616b40aa4d027ee38ebc7e393a361ecd0f301ea55a8
+++ b/internal/cli/testdata/golden/blobs/sha256/bbaad1e21e057125420f82a99f24337b25e483d9332e6d6e2485a71562bd2900
@@ -1,1 +1,1 @@
-{"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:ae315dbf6cdeac9bef1b14aed88fd75bbfbf5ed9671994241528c2da3fa6d785"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"]}}
\ No newline at end of file
+{"architecture":"amd64","author":"github.com/chainguard-dev/apko","created":"1970-01-01T00:00:00Z","history":[{"author":"apko","created":"1970-01-01T00:00:00Z","created_by":"apko","comment":"This is an apko single-layer image"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:ae315dbf6cdeac9bef1b14aed88fd75bbfbf5ed9671994241528c2da3fa6d785"]},"config":{"Entrypoint":["/bin/sh","-l"],"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],"Labels":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}}
\ No newline at end of file
diff --git a/internal/cli/testdata/golden/blobs/sha256/7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4 b/internal/cli/testdata/golden/blobs/sha256/f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972
similarity index 53%
rename from internal/cli/testdata/golden/blobs/sha256/7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4
rename to internal/cli/testdata/golden/blobs/sha256/f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972
index 597bf30..52805d8 100644
--- a/internal/cli/testdata/golden/blobs/sha256/7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4
+++ b/internal/cli/testdata/golden/blobs/sha256/f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972
@@ -1,1 +1,1 @@
-{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":524,"digest":"sha256:a8c325d93bf7fede3a4fac598ea0d05b7a56d702efbed5cbcf1f40c3e7ad7076"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3110,"digest":"sha256:6e6ffbcf153baa74794a66c87760f8d393ad82b51770b2c3b6ae5211ee5d7b6d"}]}
\ No newline at end of file
+{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":593,"digest":"sha256:b12d8e8b08c2b824fa25d5d797d1aa95a424968f8136392f6a5476cc89577fbb"},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","size":3110,"digest":"sha256:6e6ffbcf153baa74794a66c87760f8d393ad82b51770b2c3b6ae5211ee5d7b6d"}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}
\ No newline at end of file
diff --git a/internal/cli/testdata/golden/index.json b/internal/cli/testdata/golden/index.json
index 29b1801..44f1880 100755
--- a/internal/cli/testdata/golden/index.json
+++ b/internal/cli/testdata/golden/index.json
@@ -1,1 +1,1 @@
-{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":402,"digest":"sha256:e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926","platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":402,"digest":"sha256:7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4","platform":{"architecture":"arm64","os":"linux"}}]}
\ No newline at end of file
+{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":476,"digest":"sha256:a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675","platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":476,"digest":"sha256:f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972","platform":{"architecture":"arm64","os":"linux"}}],"annotations":{"org.opencontainers.image.created":"1970-01-01T00:00:00Z"}}
\ No newline at end of file
diff --git a/internal/cli/testdata/golden/sboms/sbom-aarch64.spdx.json b/internal/cli/testdata/golden/sboms/sbom-aarch64.spdx.json
index f687a4c..8349b75 100644
--- a/internal/cli/testdata/golden/sboms/sbom-aarch64.spdx.json
+++ b/internal/cli/testdata/golden/sboms/sbom-aarch64.spdx.json
@@ -13,13 +13,13 @@
   "dataLicense": "CC0-1.0",
   "documentNamespace": "https://spdx.org/spdxdocs/apko/",
   "documentDescribes": [
-    "SPDXRef-Package-sha256-7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4"
+    "SPDXRef-Package-sha256-f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972"
   ],
   "packages": [
     {
-      "SPDXID": "SPDXRef-Package-sha256-7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
-      "name": "sha256:7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
-      "versionInfo": "sha256:7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
+      "SPDXID": "SPDXRef-Package-sha256-f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
+      "name": "sha256:f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
+      "versionInfo": "sha256:f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
       "filesAnalyzed": false,
       "description": "apko container image",
       "downloadLocation": "NOASSERTION",
@@ -28,13 +28,13 @@
       "checksums": [
         {
           "algorithm": "SHA256",
-          "checksumValue": "7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4"
+          "checksumValue": "f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972"
         }
       ],
       "externalRefs": [
         {
           "referenceCategory": "PACKAGE-MANAGER",
-          "referenceLocator": "pkg:oci/golden@sha256%3A7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4?arch=arm64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
+          "referenceLocator": "pkg:oci/golden@sha256%3Af5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972?arch=arm64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
           "referenceType": "purl"
         }
       ]
@@ -96,7 +96,7 @@
   ],
   "relationships": [
     {
-      "spdxElementId": "SPDXRef-Package-sha256-7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
+      "spdxElementId": "SPDXRef-Package-sha256-f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
       "relationshipType": "CONTAINS",
       "relatedSpdxElement": "SPDXRef-Package-sha256-6e6ffbcf153baa74794a66c87760f8d393ad82b51770b2c3b6ae5211ee5d7b6d"
     }
diff --git a/internal/cli/testdata/golden/sboms/sbom-index.spdx.json b/internal/cli/testdata/golden/sboms/sbom-index.spdx.json
index a39f6ef..6d78f05 100644
--- a/internal/cli/testdata/golden/sboms/sbom-index.spdx.json
+++ b/internal/cli/testdata/golden/sboms/sbom-index.spdx.json
@@ -1,6 +1,6 @@
 {
   "SPDXID": "SPDXRef-DOCUMENT",
-  "name": "sbom-sha256:685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
+  "name": "sbom-sha256:d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
   "spdxVersion": "SPDX-2.3",
   "creationInfo": {
     "created": "1970-01-01T00:00:00Z",
@@ -13,13 +13,13 @@
   "dataLicense": "CC0-1.0",
   "documentNamespace": "https://spdx.org/spdxdocs/apko/",
   "documentDescribes": [
-    "SPDXRef-Package-sha256-685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168"
+    "SPDXRef-Package-sha256-d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227"
   ],
   "packages": [
     {
-      "SPDXID": "SPDXRef-Package-sha256-685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
-      "name": "sha256:685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
-      "versionInfo": "sha256:685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
+      "SPDXID": "SPDXRef-Package-sha256-d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
+      "name": "sha256:d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
+      "versionInfo": "sha256:d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
       "filesAnalyzed": false,
       "description": "Multi-arch image index",
       "downloadLocation": "NOASSERTION",
@@ -29,21 +29,21 @@
       "checksums": [
         {
           "algorithm": "SHA256",
-          "checksumValue": "685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168"
+          "checksumValue": "d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227"
         }
       ],
       "externalRefs": [
         {
           "referenceCategory": "PACKAGE-MANAGER",
-          "referenceLocator": "pkg:oci/golden@sha256%3A685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168?mediaType=application%2Fvnd.oci.image.index.v1%2Bjson",
+          "referenceLocator": "pkg:oci/golden@sha256%3Ad558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227?mediaType=application%2Fvnd.oci.image.index.v1%2Bjson",
           "referenceType": "purl"
         }
       ]
     },
     {
-      "SPDXID": "SPDXRef-Package-sha256-e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
-      "name": "sha256:e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
-      "versionInfo": "sha256:e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
+      "SPDXID": "SPDXRef-Package-sha256-a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
+      "name": "sha256:a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
+      "versionInfo": "sha256:a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
       "filesAnalyzed": false,
       "downloadLocation": "NOASSERTION",
       "supplier": "Organization: Chainguard, Inc.",
@@ -51,21 +51,21 @@
       "checksums": [
         {
           "algorithm": "SHA256",
-          "checksumValue": "e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926"
+          "checksumValue": "a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675"
         }
       ],
       "externalRefs": [
         {
           "referenceCategory": "PACKAGE-MANAGER",
-          "referenceLocator": "pkg:oci/golden@sha256%3Ae42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926?arch=amd64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
+          "referenceLocator": "pkg:oci/golden@sha256%3Aa87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675?arch=amd64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
           "referenceType": "purl"
         }
       ]
     },
     {
-      "SPDXID": "SPDXRef-Package-sha256-7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
-      "name": "sha256:7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
-      "versionInfo": "sha256:7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4",
+      "SPDXID": "SPDXRef-Package-sha256-f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
+      "name": "sha256:f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
+      "versionInfo": "sha256:f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972",
       "filesAnalyzed": false,
       "downloadLocation": "NOASSERTION",
       "supplier": "Organization: Chainguard, Inc.",
@@ -73,13 +73,13 @@
       "checksums": [
         {
           "algorithm": "SHA256",
-          "checksumValue": "7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4"
+          "checksumValue": "f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972"
         }
       ],
       "externalRefs": [
         {
           "referenceCategory": "PACKAGE-MANAGER",
-          "referenceLocator": "pkg:oci/golden@sha256%3A7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4?arch=arm64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
+          "referenceLocator": "pkg:oci/golden@sha256%3Af5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972?arch=arm64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
           "referenceType": "purl"
         }
       ]
@@ -87,14 +87,14 @@
   ],
   "relationships": [
     {
-      "spdxElementId": "SPDXRef-Package-sha256-685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
+      "spdxElementId": "SPDXRef-Package-sha256-d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
       "relationshipType": "VARIANT_OF",
-      "relatedSpdxElement": "SPDXRef-Package-sha256-e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926"
+      "relatedSpdxElement": "SPDXRef-Package-sha256-a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675"
     },
     {
-      "spdxElementId": "SPDXRef-Package-sha256-685a8f5d66554057ea6e697be713d81bdaec276f4ef845243e60fc61df698168",
+      "spdxElementId": "SPDXRef-Package-sha256-d558b9114f2eaf11b1a3d9720cdcff3b12f087e741377d04f93bd5dc53f36227",
       "relationshipType": "VARIANT_OF",
-      "relatedSpdxElement": "SPDXRef-Package-sha256-7e3a7384fe0243cc4b498cd54be2455fdcbb7aebd5eb94566926a5088868eef4"
+      "relatedSpdxElement": "SPDXRef-Package-sha256-f5e87ab7f4eba7d7a649cd3fcb969b4fcb9c72784c9cbe7ed3f0f60e84933972"
     }
   ]
 }
diff --git a/internal/cli/testdata/golden/sboms/sbom-x86_64.spdx.json b/internal/cli/testdata/golden/sboms/sbom-x86_64.spdx.json
index 639f52f..14292a0 100644
--- a/internal/cli/testdata/golden/sboms/sbom-x86_64.spdx.json
+++ b/internal/cli/testdata/golden/sboms/sbom-x86_64.spdx.json
@@ -13,13 +13,13 @@
   "dataLicense": "CC0-1.0",
   "documentNamespace": "https://spdx.org/spdxdocs/apko/",
   "documentDescribes": [
-    "SPDXRef-Package-sha256-e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926"
+    "SPDXRef-Package-sha256-a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675"
   ],
   "packages": [
     {
-      "SPDXID": "SPDXRef-Package-sha256-e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
-      "name": "sha256:e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
-      "versionInfo": "sha256:e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
+      "SPDXID": "SPDXRef-Package-sha256-a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
+      "name": "sha256:a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
+      "versionInfo": "sha256:a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
       "filesAnalyzed": false,
       "description": "apko container image",
       "downloadLocation": "NOASSERTION",
@@ -28,13 +28,13 @@
       "checksums": [
         {
           "algorithm": "SHA256",
-          "checksumValue": "e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926"
+          "checksumValue": "a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675"
         }
       ],
       "externalRefs": [
         {
           "referenceCategory": "PACKAGE-MANAGER",
-          "referenceLocator": "pkg:oci/golden@sha256%3Ae42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926?arch=amd64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
+          "referenceLocator": "pkg:oci/golden@sha256%3Aa87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675?arch=amd64\u0026mediaType=application%2Fvnd.oci.image.manifest.v1%2Bjson\u0026os=linux",
           "referenceType": "purl"
         }
       ]
@@ -96,7 +96,7 @@
   ],
   "relationships": [
     {
-      "spdxElementId": "SPDXRef-Package-sha256-e42af50bd66e8060653eb032153bc77d039807418ee4d80e0fbca5e3e5028926",
+      "spdxElementId": "SPDXRef-Package-sha256-a87baa16c787394589606e2519134a8e23d9323f475cc3e1a6f63731e8579675",
       "relationshipType": "CONTAINS",
       "relatedSpdxElement": "SPDXRef-Package-sha256-16e741f38ee9768330001d0b12b34006d5bdf90bc2dd72716bca4446e66b6ac6"
     }
diff --git a/pkg/apk/apk/implementation_test.go b/pkg/apk/apk/implementation_test.go
index d13d204..9e3ce78 100644
--- a/pkg/apk/apk/implementation_test.go
+++ b/pkg/apk/apk/implementation_test.go
@@ -111,11 +111,7 @@ func TestInitDB_ChainguardDiscovery(t *testing.T) {
 	apk, err := New(WithFS(src), WithIgnoreMknodErrors(ignoreMknodErrors))
 	require.NoError(t, err)
 
-	// This is a staging group that was set up to host public images published
-	// by https://github.com/chainguard-dev/terraform-provider-apko presubmit.
-	// TODO(mattmoor): Once we push this out to production, we should switch to
-	// using apk.cgr.dev/chainguard or apk.cgr.dev/extra-packages.
-	err = apk.InitDB(context.Background(), "https://apk.chainreg.biz/tf-apko.pub")
+	err = apk.InitDB(context.Background(), "https://apk.cgr.dev/chainguard")
 	require.NoError(t, err)
 	// check all of the contents
 	for _, d := range initDirectories {
@@ -425,7 +421,7 @@ func TestFetchPackage(t *testing.T) {
 
 		opts := []Option{WithFS(src), WithIgnoreMknodErrors(ignoreMknodErrors)}
 		if cache != "" {
-			opts = append(opts, WithCache(cache, false))
+			opts = append(opts, WithCache(cache, false, NewCache(false)))
 		}
 		a, err := New(opts...)
 		require.NoError(t, err, "unable to create APK")
diff --git a/pkg/apk/apk/installed_test.go b/pkg/apk/apk/installed_test.go
index 480b6cd..6c3b110 100644
--- a/pkg/apk/apk/installed_test.go
+++ b/pkg/apk/apk/installed_test.go
@@ -256,7 +256,7 @@ func TestUpdateTriggers(t *testing.T) {
 	readTriggers, err := a.readTriggers()
 	require.NoError(t, err, "unable to read triggers: %v", err)
 	defer readTriggers.Close()
-	cksum := base64.StdEncoding.EncodeToString(pkg.Checksum)
+	cksum := "Q1" + base64.StdEncoding.EncodeToString(pkg.Checksum)
 	// read every line in triggers, looking for one with our comment
 	scanner := bufio.NewScanner(readTriggers)
 	for scanner.Scan() {
@@ -271,7 +271,7 @@ func TestUpdateTriggers(t *testing.T) {
 		return
 	}
 	//nolint:forbidigo // this is a valid use case
-	t.Errorf("could not find entry for commit: %s", cksum)
+	t.Errorf("could not find entry for checksum: %s", cksum)
 }
 
 func TestSortTarHeaders(t *testing.T) {
diff --git a/pkg/apk/apk/repo_test.go b/pkg/apk/apk/repo_test.go
index 5e78387..b8a57ab 100644
--- a/pkg/apk/apk/repo_test.go
+++ b/pkg/apk/apk/repo_test.go
@@ -26,7 +26,6 @@ import (
 	"reflect"
 	"sort"
 	"strings"
-	"sync"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -92,8 +91,6 @@ guyM+Ks3c29KlRf3iX35Gt0CAwEAAQ==
 )
 
 func TestGetRepositoryIndexes(t *testing.T) {
-	var mu sync.Mutex
-	counter := 0
 	prepLayout := func(t *testing.T, cache string, repos []string) *APK {
 		src := apkfs.NewMemFS()
 		err := src.MkdirAll("etc/apk", 0o755)
@@ -111,18 +108,13 @@ func TestGetRepositoryIndexes(t *testing.T) {
 			err = src.WriteFile(reposFilePath, []byte(strings.Join(repos, "\n")), 0o644)
 			require.NoErrorf(t, err, "unable to write repositories")
 		} else {
-			// Append an int to the testAlpineRepos path to bust the global cache if repos are unspecified.
-			// TODO: Stop being lazy and remove the global caches.
-			mu.Lock()
-			counter++
-			err = src.WriteFile(reposFilePath, []byte(fmt.Sprintf("%s/%d", testAlpineRepos, counter)), 0o644)
-			mu.Unlock()
+			err = src.WriteFile(reposFilePath, []byte(testAlpineRepos), 0o644)
 			require.NoErrorf(t, err, "unable to write repositories")
 		}
 
 		opts := []Option{WithFS(src), WithIgnoreMknodErrors(ignoreMknodErrors)}
 		if cache != "" {
-			opts = append(opts, WithCache(cache, false))
+			opts = append(opts, WithCache(cache, false, NewCache(false)))
 		}
 		a, err := New(opts...)
 		require.NoError(t, err, "unable to create APK")
@@ -149,9 +141,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.Greater(t, len(indexes), 0, "no indexes found")
 	})
 	t.Run("cache miss no network", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache, globalIndexCache = &etagCache{}, &indexCache{}
-
 		// we use a transport that always returns a 404 so we know we're not hitting the network
 		// it should fail for a cache hit
 		tmpDir := t.TempDir()
@@ -163,9 +152,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.Error(t, err, "should fail when no cache and no network")
 	})
 	t.Run("we can fetch, but do not cache indices without etag", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache, globalIndexCache = &etagCache{}, &indexCache{}
-
 		// we use a transport that can read from the network
 		// it should fail for a cache hit
 		tmpDir := t.TempDir()
@@ -186,9 +172,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		}))
 	})
 	t.Run("cache miss network should fill cache", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache, globalIndexCache = &etagCache{}, &indexCache{}
-
 		// we use a transport that can read from the network
 		// it should fail for a cache hit
 		tmpDir := t.TempDir()
@@ -216,9 +199,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.Equal(t, index1, index2, "index files do not match")
 	})
 	t.Run("repo url with http basic auth", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache = &etagCache{}
-
 		tmpDir := t.TempDir()
 		a := prepLayout(t, tmpDir, []string{"https://user:pass@dl-cdn.alpinelinux.org/alpine/v3.16/main"})
 
@@ -235,9 +215,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.Greater(t, len(indexes), 0, "no indexes found")
 	})
 	t.Run("cache hit etag match", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache = &etagCache{}
-
 		// it should succeed for a cache hit
 		tmpDir := t.TempDir()
 		testEtag := "test-etag"
@@ -270,9 +247,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.Equal(t, index1, index2, "index files do not match")
 	})
 	t.Run("cache hit etag miss", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache, globalIndexCache = &etagCache{}, &indexCache{}
-
 		// it should succeed for a cache hit
 		tmpDir := t.TempDir()
 		testEtag := "test-etag"
@@ -297,9 +271,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 			Transport: &testLocalTransport{root: testAlternatePkgDir, basenameOnly: true, headers: map[string][]string{http.CanonicalHeaderKey("etag"): {testEtag + "change"}}},
 		})
 
-		// Reset etag cache.
-		globalEtagCache, globalIndexCache = &etagCache{}, &indexCache{}
-
 		indexes, err = a.GetRepositoryIndexes(context.TODO(), false)
 		require.NoErrorf(t, err, "unable to get indexes")
 		require.Greater(t, len(indexes), 0, "no indexes found")
@@ -310,9 +281,6 @@ func TestGetRepositoryIndexes(t *testing.T) {
 		require.NotEqual(t, index1, index2, "index files do not match")
 	})
 	t.Run("test cache concurrency", func(t *testing.T) {
-		// Reset etag cache so we have isolated tests.
-		globalEtagCache = &etagCache{}
-
 		// Use the same temp directory for the cache.
 		tmpDir := t.TempDir()
 
@@ -1039,10 +1007,6 @@ func TestDisqualifyingOtherArchitectures(t *testing.T) {
 	names := []string{"package1", "package2", "onlyinarm64"}
 	_, index := testGetPackagesAndIndex()
 
-	others := map[string][]NamedIndex{
-		"x86_64": testNamedRepositoryFromIndexes(index),
-	}
-
 	arm64 := slices.Clone(index)
 	repo := Repository{}
 	repoWithIndex := repo.WithIndex(&APKIndex{
@@ -1050,7 +1014,14 @@ func TestDisqualifyingOtherArchitectures(t *testing.T) {
 	})
 	arm64 = append(arm64, repoWithIndex)
 
-	resolver := NewPkgResolver(context.Background(), testNamedRepositoryFromIndexes(arm64))
-	_, _, err := resolver.GetPackagesWithDependencies(context.Background(), names, others)
+	armIndex := testNamedRepositoryFromIndexes(arm64)
+
+	byArch := map[string][]NamedIndex{
+		"x86_64":  testNamedRepositoryFromIndexes(index),
+		"aarch64": armIndex,
+	}
+
+	resolver := NewPkgResolver(context.Background(), armIndex)
+	_, _, err := resolver.GetPackagesWithDependencies(context.Background(), names, byArch)
 	require.ErrorContains(t, err, "package \"onlyinarm64-1.0.0.apk\" not available for arch \"x86_64\"")
 }
diff --git a/pkg/build/build_test.go b/pkg/build/build_test.go
index 3cbbf3c..c663cc1 100644
--- a/pkg/build/build_test.go
+++ b/pkg/build/build_test.go
@@ -112,7 +112,7 @@ func TestAuth_good(t *testing.T) {
 	called := false
 	testUser, testPass := "user", "pass"
 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("ETag", "123")
+		w.Header().Set("ETag", "234")
 		if r.Method == http.MethodHead {
 			return
 		}
@@ -151,7 +151,7 @@ func TestAuth_bad(t *testing.T) {
 	called := false
 	testUser, testPass := "user", "pass"
 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("ETag", "123")
+		w.Header().Set("ETag", "345")
 		if r.Method == http.MethodHead {
 			return
 		}
diff --git a/pkg/build/oci/image_test.go b/pkg/build/oci/image_test.go
index a3d6ff0..eef3ce4 100644
--- a/pkg/build/oci/image_test.go
+++ b/pkg/build/oci/image_test.go
@@ -61,7 +61,9 @@ func TestBuildImageFromLayer(t *testing.T) {
 					"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 					"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt",
 				},
-				Labels: map[string]string{},
+				Labels: map[string]string{
+					"org.opencontainers.image.created": now.Format(time.RFC3339),
+				},
 			},
 		},
 	}, {
@@ -88,7 +90,9 @@ func TestBuildImageFromLayer(t *testing.T) {
 					"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 					"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt",
 				},
-				Labels: map[string]string{},
+				Labels: map[string]string{
+					"org.opencontainers.image.created": now.Format(time.RFC3339),
+				},
 			},
 		},
 	}, {
@@ -116,7 +120,9 @@ func TestBuildImageFromLayer(t *testing.T) {
 					"PATH=/something/else:/another/one:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
 					"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt",
 				},
-				Labels: map[string]string{},
+				Labels: map[string]string{
+					"org.opencontainers.image.created": now.Format(time.RFC3339),
+				},
 			},
 		},
 	}, {
@@ -144,7 +150,9 @@ func TestBuildImageFromLayer(t *testing.T) {
 					"PATH=",
 					"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt",
 				},
-				Labels: map[string]string{},
+				Labels: map[string]string{
+					"org.opencontainers.image.created": now.Format(time.RFC3339),
+				},
 			},
 		},
 	}} {
@@ -161,7 +169,3 @@ func TestBuildImageFromLayer(t *testing.T) {
 		})
 	}
 }
-
-func TestBuildImageTarballFromLayer(t *testing.T) {
-
-}
diff --git a/pkg/sbom/generator/spdx/spdx_test.go b/pkg/sbom/generator/spdx/spdx_test.go
index fd82f50..99c4fe0 100644
--- a/pkg/sbom/generator/spdx/spdx_test.go
+++ b/pkg/sbom/generator/spdx/spdx_test.go
@@ -15,8 +15,10 @@
 package spdx
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
+	"path"
 	"path/filepath"
 	"regexp"
 	"testing"
@@ -30,12 +32,9 @@ import (
 	"chainguard.dev/apko/pkg/sbom/options"
 )
 
+// TODO: clean this up and make consistent with the other test cases
 var testOpts = &options.Options{
-	OS: struct {
-		Name    string
-		ID      string
-		Version string
-	}{
+	OS: options.OSInfo{
 		Name:    "unknown",
 		ID:      "unknown",
 		Version: "3.0",
@@ -60,56 +59,7 @@ var testOpts = &options.Options{
 	},
 }
 
-var testCustomLicenseOpts = &options.Options{
-	OS: struct {
-		Name    string
-		ID      string
-		Version string
-	}{
-		Name:    "unknown",
-		ID:      "unknown",
-		Version: "3.0",
-	},
-	FileName: "sbom",
-	Packages: []*apk.InstalledPackage{
-		{
-			Package: apk.Package{
-				Name:        "font-ubuntu",
-				Version:     "0.869-r1",
-				Arch:        "x86_64",
-				Description: "Ubuntu font family",
-				License:     "LicenseRef-ubuntu-font",
-				Origin:      "font-ubuntu",
-			},
-		},
-	},
-}
-
-var testNoSupplierOpts = &options.Options{
-	OS: struct {
-		Name    string
-		ID      string
-		Version string
-	}{
-		Name:    "Apko Images, Plc",
-		ID:      "apko-images",
-		Version: "3.0",
-	},
-	FileName: "sbom",
-	Packages: []*apk.InstalledPackage{
-		{
-			Package: apk.Package{
-				Name:        "libattr1",
-				Version:     "2.5.1-r2",
-				Arch:        "x86_64",
-				Description: "library for managing filesystem extended attributes",
-				License:     "GPL-2.0-or-later",
-				Origin:      "attr",
-			},
-		},
-	},
-}
-
+// TODO: clean this up and make consistent with the other test cases
 func TestGenerate(t *testing.T) {
 	dir := t.TempDir()
 	fsys := apkfs.NewMemFS()
@@ -120,48 +70,130 @@ func TestGenerate(t *testing.T) {
 	require.FileExists(t, path)
 }
 
-func TestGenerateCustomLicense(t *testing.T) {
-	spdx, err := os.ReadFile("testdata/font-ubuntu.spdx.json")
-	require.NoError(t, err)
+func TestSPDX_Generate(t *testing.T) {
+	tests := []struct {
+		name string
+		opts *options.Options
+	}{
+		{
+			name: "custom-license",
+			opts: &options.Options{
+				OS: options.OSInfo{
+					Name:    "unknown",
+					ID:      "unknown",
+					Version: "3.0",
+				},
+				FileName: "sbom",
+				Packages: []*apk.InstalledPackage{
+					{
+						Package: apk.Package{
+							Name:    "font-ubuntu",
+							Version: "0.869-r1",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "no-supplier",
+			opts: &options.Options{
+				OS: options.OSInfo{
+					Name:    "Apko Images, Plc",
+					ID:      "apko-images",
+					Version: "3.0",
+				},
+				FileName: "sbom",
+				Packages: []*apk.InstalledPackage{
+					{
+						Package: apk.Package{
+							Name:    "libattr1",
+							Version: "2.5.1-r2",
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "package-deduplicating",
+			opts: &options.Options{
+				OS: options.OSInfo{
+					Name:    "unknown",
+					ID:      "unknown",
+					Version: "3.0",
+				},
+				FileName: "sbom",
+				Packages: []*apk.InstalledPackage{
+					{
+						Package: apk.Package{
+							Name:    "logstash-8",
+							Version: "8.15.3-r4",
+						},
+					},
+					{
+						Package: apk.Package{
+							Name:    "logstash-8-compat",
+							Version: "8.15.3-r4",
+						},
+					},
+				},
+			},
+		},
+	}
 
-	fsys := apkfs.NewMemFS()
-	fsys.MkdirAll("/var/lib/db/sbom", 0750)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fsys := apkfs.NewMemFS()
+			sbomDir := path.Join("var", "lib", "db", "sbom")
+			err := fsys.MkdirAll(sbomDir, 0750)
+			require.NoError(t, err)
 
-	err = fsys.WriteFile("/var/lib/db/sbom/font-ubuntu.spdx.json", spdx, 0644)
-	require.NoError(t, err)
+			for _, apkPkg := range tt.opts.Packages {
+				apkSBOMName := fmt.Sprintf("%s-%s.spdx.json", apkPkg.Name, apkPkg.Version)
+				apkSBOMTestdataPath := filepath.Join("testdata", "apk_sboms", apkSBOMName)
+				apkSBOMBytes, err := os.ReadFile(apkSBOMTestdataPath)
+				require.NoError(t, err)
 
-	sx := New(fsys)
-	path := filepath.Join(t.TempDir(), testCustomLicenseOpts.FileName+"."+sx.Ext())
-	err = sx.Generate(testCustomLicenseOpts, path)
-	require.NoError(t, err)
+				sbomDestPath := path.Join(sbomDir, apkSBOMName)
+				err = fsys.WriteFile(sbomDestPath, apkSBOMBytes, 0644)
+				require.NoError(t, err)
+			}
 
-	got, err := os.ReadFile(path)
-	require.NoError(t, err)
-	expected, err := os.ReadFile("testdata/expected.ubuntu-font.spdx.json")
-	require.NoError(t, err)
-	require.Equal(t, expected, got, "CustomLicense SPDX")
-}
+			sx := New(fsys)
+			imageSBOMName := fmt.Sprintf("%s.spdx.json", tt.name)
+			imageSBOMDestPath := filepath.Join(t.TempDir(), imageSBOMName)
+			err = sx.Generate(tt.opts, imageSBOMDestPath)
+			require.NoError(t, err)
 
-func TestNoSupplier(t *testing.T) {
-	spdx, err := os.ReadFile("testdata/libattr1.spdx.json")
-	require.NoError(t, err)
+			actual, err := os.ReadFile(imageSBOMDestPath)
+			require.NoError(t, err)
 
-	fsys := apkfs.NewMemFS()
-	fsys.MkdirAll("/var/lib/db/sbom", 0750)
+			expectedImageSBOMPath := filepath.Join("testdata", "expected_image_sboms", imageSBOMName)
+			expected, err := os.ReadFile(expectedImageSBOMPath)
+			require.NoError(t, err)
 
-	err = fsys.WriteFile("/var/lib/db/sbom/libattr1.spdx.json", spdx, 0644)
-	require.NoError(t, err)
+			t.Run("goldenfile diff", func(t *testing.T) {
+				if diff := cmp.Diff(expected, actual); diff != "" {
+					t.Errorf("Unexpected image SBOM (-want, +got): \n%s", diff)
+				}
+			})
 
-	sx := New(fsys)
-	path := filepath.Join(t.TempDir(), testNoSupplierOpts.FileName+"."+sx.Ext())
-	err = sx.Generate(testNoSupplierOpts, path)
-	require.NoError(t, err)
+			t.Run("unique SPDX IDs", func(t *testing.T) {
+				doc := new(Document)
+				err := json.Unmarshal(actual, doc)
+				if err != nil {
+					t.Fatalf("unmarshalling SBOM: %v", err)
+				}
 
-	got, err := os.ReadFile(path)
-	require.NoError(t, err)
-	expected, err := os.ReadFile("testdata/expected.libattr1.spdx.json")
-	require.NoError(t, err)
-	require.Equal(t, expected, got, "NoSupplier SPDX")
+				ids := make(map[string]struct{})
+				for _, p := range doc.Packages {
+					if _, ok := ids[p.ID]; ok {
+						t.Errorf("duplicate SPDX ID found: %s", p.ID)
+					}
+					ids[p.ID] = struct{}{}
+				}
+			})
+		})
+	}
 }
 
 func TestReproducible(t *testing.T) {
diff --git a/pkg/sbom/generator/spdx/testdata/apk_sboms/_generate.sh b/pkg/sbom/generator/spdx/testdata/apk_sboms/_generate.sh
new file mode 100755
index 0000000..31cfd9d
--- /dev/null
+++ b/pkg/sbom/generator/spdx/testdata/apk_sboms/_generate.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -euo pipefail
+
+# Define an array of package name-version strings
+packages=(
+  "font-ubuntu-0.869-r1"
+  "libattr1-2.5.1-r2"
+  "logstash-8-8.15.3-r4"
+  "logstash-8-compat-8.15.3-r4"
+)
+
+# Base URL for downloading APKs
+base_url="https://packages.wolfi.dev/os/x86_64"
+
+# Loop through the array and process each package
+for pkg in "${packages[@]}"; do
+  url="${base_url}/${pkg}.apk"
+  output_path="${pkg}.spdx.json"
+  curl -q "$url" | tar Ozx "var/lib/db/sbom/${pkg}.spdx.json" >"$output_path" 2>/dev/null
+done
diff --git a/pkg/sbom/generator/spdx/testdata/font-ubuntu.spdx.json b/pkg/sbom/generator/spdx/testdata/apk_sboms/font-ubuntu-0.869-r1.spdx.json
similarity index 100%
rename from pkg/sbom/generator/spdx/testdata/font-ubuntu.spdx.json
rename to pkg/sbom/generator/spdx/testdata/apk_sboms/font-ubuntu-0.869-r1.spdx.json
diff --git a/pkg/sbom/generator/spdx/testdata/libattr1.spdx.json b/pkg/sbom/generator/spdx/testdata/apk_sboms/libattr1-2.5.1-r2.spdx.json
similarity index 100%
rename from pkg/sbom/generator/spdx/testdata/libattr1.spdx.json
rename to pkg/sbom/generator/spdx/testdata/apk_sboms/libattr1-2.5.1-r2.spdx.json
diff --git a/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-8.15.3-r4.spdx.json b/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-8.15.3-r4.spdx.json
new file mode 100644
index 0000000..9e75628
--- /dev/null
+++ b/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-8.15.3-r4.spdx.json
@@ -0,0 +1,87 @@
+{
+  "SPDXID": "SPDXRef-DOCUMENT",
+  "name": "apk-logstash-8-8.15.3-r4",
+  "spdxVersion": "SPDX-2.3",
+  "creationInfo": {
+    "created": "2024-10-24T15:16:49Z",
+    "creators": [
+      "Tool: melange (v0.14.7)",
+      "Organization: Chainguard, Inc"
+    ],
+    "licenseListVersion": "3.22"
+  },
+  "dataLicense": "CC0-1.0",
+  "documentNamespace": "https://spdx.org/spdxdocs/chainguard/melange/0434ffabb9f0343f5dada78abdb2e419b2be48fb",
+  "documentDescribes": [
+    "SPDXRef-Package-logstash-8-8.15.3-r4"
+  ],
+  "packages": [
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "name": "logstash-8",
+      "versionInfo": "8.15.3-r4",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "copyrightText": "\n",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:apk/wolfi/logstash-8@8.15.3-r4?arch=x86_64",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "name": "logstash-8.yaml",
+      "versionInfo": "c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/wolfi-dev/os@c7d40faa38ddffa98700cfa4c2f9bde196acc504#logstash-8.yaml",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0",
+      "name": "logstash",
+      "versionInfo": "v8.15.3",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Elastic",
+      "supplier": "Organization: Elastic",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/elastic/logstash@v8.15.3",
+          "referenceType": "purl"
+        }
+      ]
+    }
+  ],
+  "relationships": [
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "relationshipType": "DESCRIBED_BY",
+      "relatedSpdxElement": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "relationshipType": "GENERATED_FROM",
+      "relatedSpdxElement": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0"
+    }
+  ]
+}
diff --git a/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-compat-8.15.3-r4.spdx.json b/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-compat-8.15.3-r4.spdx.json
new file mode 100644
index 0000000..b83d59c
--- /dev/null
+++ b/pkg/sbom/generator/spdx/testdata/apk_sboms/logstash-8-compat-8.15.3-r4.spdx.json
@@ -0,0 +1,87 @@
+{
+  "SPDXID": "SPDXRef-DOCUMENT",
+  "name": "apk-logstash-8-compat-8.15.3-r4",
+  "spdxVersion": "SPDX-2.3",
+  "creationInfo": {
+    "created": "2024-10-24T15:16:49Z",
+    "creators": [
+      "Tool: melange (v0.14.7)",
+      "Organization: Chainguard, Inc"
+    ],
+    "licenseListVersion": "3.22"
+  },
+  "dataLicense": "CC0-1.0",
+  "documentNamespace": "https://spdx.org/spdxdocs/chainguard/melange/0434ffabb9f0343f5dada78abdb2e419b2be48fb",
+  "documentDescribes": [
+    "SPDXRef-Package-logstash-8-compat-8.15.3-r4"
+  ],
+  "packages": [
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "name": "logstash-8-compat",
+      "versionInfo": "8.15.3-r4",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "copyrightText": "\n",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:apk/wolfi/logstash-8-compat@8.15.3-r4?arch=x86_64",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "name": "logstash-8.yaml",
+      "versionInfo": "c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/wolfi-dev/os@c7d40faa38ddffa98700cfa4c2f9bde196acc504#logstash-8.yaml",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0",
+      "name": "logstash",
+      "versionInfo": "v8.15.3",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Elastic",
+      "supplier": "Organization: Elastic",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/elastic/logstash@v8.15.3",
+          "referenceType": "purl"
+        }
+      ]
+    }
+  ],
+  "relationships": [
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "relationshipType": "DESCRIBED_BY",
+      "relatedSpdxElement": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "relationshipType": "GENERATED_FROM",
+      "relatedSpdxElement": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0"
+    }
+  ]
+}
diff --git a/pkg/sbom/generator/spdx/testdata/expected.ubuntu-font.spdx.json b/pkg/sbom/generator/spdx/testdata/expected_image_sboms/custom-license.spdx.json
similarity index 100%
rename from pkg/sbom/generator/spdx/testdata/expected.ubuntu-font.spdx.json
rename to pkg/sbom/generator/spdx/testdata/expected_image_sboms/custom-license.spdx.json
diff --git a/pkg/sbom/generator/spdx/testdata/expected.libattr1.spdx.json b/pkg/sbom/generator/spdx/testdata/expected_image_sboms/no-supplier.spdx.json
similarity index 100%
rename from pkg/sbom/generator/spdx/testdata/expected.libattr1.spdx.json
rename to pkg/sbom/generator/spdx/testdata/expected_image_sboms/no-supplier.spdx.json
diff --git a/pkg/sbom/generator/spdx/testdata/expected_image_sboms/package-deduplicating.spdx.json b/pkg/sbom/generator/spdx/testdata/expected_image_sboms/package-deduplicating.spdx.json
new file mode 100644
index 0000000..3db3159
--- /dev/null
+++ b/pkg/sbom/generator/spdx/testdata/expected_image_sboms/package-deduplicating.spdx.json
@@ -0,0 +1,132 @@
+{
+  "SPDXID": "SPDXRef-DOCUMENT",
+  "name": "sbom",
+  "spdxVersion": "SPDX-2.3",
+  "creationInfo": {
+    "created": "0001-01-01T00:00:00Z",
+    "creators": [
+      "Tool: apko (devel)",
+      "Organization: Chainguard, Inc"
+    ],
+    "licenseListVersion": "3.16"
+  },
+  "dataLicense": "CC0-1.0",
+  "documentNamespace": "https://spdx.org/spdxdocs/apko/",
+  "documentDescribes": [
+    "SPDXRef-Package-"
+  ],
+  "packages": [
+    {
+      "SPDXID": "SPDXRef-Package-",
+      "name": "",
+      "versionInfo": "3.0",
+      "filesAnalyzed": false,
+      "description": "apko operating system layer",
+      "downloadLocation": "NOASSERTION",
+      "supplier": "Organization: unknown",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:oci/image?mediaType=\u0026os=linux",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "name": "logstash-8",
+      "versionInfo": "8.15.3-r4",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "copyrightText": "\n",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:apk/wolfi/logstash-8@8.15.3-r4?arch=x86_64",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "name": "logstash-8.yaml",
+      "versionInfo": "c7d40faa38ddffa98700cfa4c2f9bde196acc504",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/wolfi-dev/os@c7d40faa38ddffa98700cfa4c2f9bde196acc504#logstash-8.yaml",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0",
+      "name": "logstash",
+      "versionInfo": "v8.15.3",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Elastic",
+      "supplier": "Organization: Elastic",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:github/elastic/logstash@v8.15.3",
+          "referenceType": "purl"
+        }
+      ]
+    },
+    {
+      "SPDXID": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "name": "logstash-8-compat",
+      "versionInfo": "8.15.3-r4",
+      "filesAnalyzed": false,
+      "licenseConcluded": "NOASSERTION",
+      "licenseDeclared": "Apache-2.0",
+      "downloadLocation": "NOASSERTION",
+      "originator": "Organization: Wolfi",
+      "supplier": "Organization: Wolfi",
+      "copyrightText": "\n",
+      "externalRefs": [
+        {
+          "referenceCategory": "PACKAGE-MANAGER",
+          "referenceLocator": "pkg:apk/wolfi/logstash-8-compat@8.15.3-r4?arch=x86_64",
+          "referenceType": "purl"
+        }
+      ]
+    }
+  ],
+  "relationships": [
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "relationshipType": "DESCRIBED_BY",
+      "relatedSpdxElement": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-8.15.3-r4",
+      "relationshipType": "GENERATED_FROM",
+      "relatedSpdxElement": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "relationshipType": "DESCRIBED_BY",
+      "relatedSpdxElement": "SPDXRef-Package-logstash-8.yaml-c7d40faa38ddffa98700cfa4c2f9bde196acc504"
+    },
+    {
+      "spdxElementId": "SPDXRef-Package-logstash-8-compat-8.15.3-r4",
+      "relationshipType": "GENERATED_FROM",
+      "relatedSpdxElement": "SPDXRef-Package-github.com-elastic-logstash-v8.15.3-8364c8e89cfb113e38ec3f966df7eb1e9abe9d33-0"
+    }
+  ]
+}
diff --git a/pkg/sbom/generator/spdx/testdata/generate.sh b/pkg/sbom/generator/spdx/testdata/generate.sh
deleted file mode 100755
index d2acb23..0000000
--- a/pkg/sbom/generator/spdx/testdata/generate.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-curl -q https://packages.wolfi.dev/os/x86_64/font-ubuntu-0.869-r1.apk | tar Ozx var/lib/db/sbom/font-ubuntu-0.869-r1.spdx.json >font-ubuntu.spdx.json 2>/dev/null
-curl -q https://packages.wolfi.dev/os/x86_64/libattr1-2.5.1-r2.apk | tar Ozx var/lib/db/sbom/libattr1-2.5.1-r2.spdx.json >libattr1.spdx.json 2>/dev/null