Skip to content

Commit c115260

Browse files
committed
feat(oci): support insecure OCI registry
- Add support for connecting to insecure OCI registries with self-signed certificates - Refactor remote options building into a dedicated method - Add comprehensive test coverage for both secure and insecure modes
1 parent b915d16 commit c115260

File tree

3 files changed

+145
-5
lines changed

3 files changed

+145
-5
lines changed

docs/config.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Supported keys include:
6666
|:-------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
6767
| `storage.gcs.bucket` | The GCS bucket for storage | | |
6868
| `storage.oci.repository` | The OCI repo to store OCI signatures and attestation in | If left undefined _and_ one of `artifacts.{oci,taskrun}.storage` includes `oci` storage, attestations will be stored alongside the stored OCI artifact itself. ([example on GCP](../images/attestations-in-artifact-registry.png)) Defining this value results in the OCI bundle stored in the designated location _instead of_ alongside the image. See [cosign documentation](https://github.com/sigstore/cosign#specifying-registry) for additional information. | |
69+
| `storage.oci.repository.insecure` | Whether to use insecure connection when connecting to the OCI repository | `true`, `false` | `false` |
6970
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
7071
| `storage.docdb.mongo-server-url` (optional) | The value of MONGO_SERVER_URL env var with the MongoDB connection URI | Example: `mongodb://[USER]:[PASSWORD]@[HOST]:[PORT]/[DATABASE]` | |
7172
| `storage.docdb.mongo-server-url-dir` (optional) | The path of the directory that contains the file named MONGO_SERVER_URL that stores the value of MONGO_SERVER_URL env var | If the file `/mnt/mongo-creds-secret/MONGO_SERVER_URL` has the value of MONGO_SERVER_URL, then set `storage.docdb.mongo-server-url-dir: /mnt/mongo-creds-secret` | |
@@ -104,9 +105,9 @@ You can provide MongoDB connection through different options
104105
* This field overrides all others (`mongo-server-url-dir, mongo-server-url, and MONGO_SERVER_URL env var`)
105106
* For instance, if `/mnt/mongo-creds-secret/mongo-server-url` contains the MongoDB URL, then set `storage.docdb.mongo-server-url-path`: `/mnt/mongo-creds-secret/mongo-server-url`
106107

107-
**NOTE** :-
108+
**NOTE** :-
108109
* When using `storage.docdb.mongo-server-url-dir` or `storage.docdb.mongo-server-url-path` field, store the value of mongo server url in a secret and mount the secret. When the secret is updated, the new value will be fetched by Tekton Chains controller
109-
* Also using `storage.docdb.mongo-server-url-dir` or `storage.docdb.mongo-server-url-path` field are recommended, using `storage.docdb.mongo-server-url` should be avoided since credentials are stored in a ConfigMap instead of a secret
110+
* Also using `storage.docdb.mongo-server-url-dir` or `storage.docdb.mongo-server-url-path` field are recommended, using `storage.docdb.mongo-server-url` should be avoided since credentials are stored in a ConfigMap instead of a secret
110111

111112
#### Grafeas
112113

pkg/chains/storage/oci/legacy.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ package oci
1515

1616
import (
1717
"context"
18+
"crypto/tls"
1819
"encoding/base64"
1920
"encoding/json"
2021
"fmt"
22+
"net/http"
2123

2224
"github.com/tektoncd/chains/pkg/chains/formats"
2325
"github.com/tektoncd/chains/pkg/chains/objects"
@@ -75,7 +77,7 @@ func NewStorageBackend(ctx context.Context, client kubernetes.Interface, cfg con
7577
// StorePayload implements the storage.Backend interface.
7678
func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, storageOpts config.StorageOpts) error {
7779
logger := logging.FromContext(ctx)
78-
auth, err := b.getAuthenticator(ctx, obj, b.client)
80+
remoteOpts, err := b.buildRemoteOptions(ctx, obj, storageOpts)
7981
if err != nil {
8082
return err
8183
}
@@ -87,7 +89,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
8789
if err := json.Unmarshal(rawPayload, &format); err != nil {
8890
return errors.Wrap(err, "unmarshal simplesigning")
8991
}
90-
return b.uploadSignature(ctx, format, rawPayload, signature, storageOpts, auth)
92+
return b.uploadSignature(ctx, format, rawPayload, signature, storageOpts, remoteOpts...)
9193
}
9294

9395
if _, ok := formats.IntotoAttestationSet[storageOpts.PayloadFormat]; ok {
@@ -105,14 +107,28 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
105107
return nil
106108
}
107109

108-
return b.uploadAttestation(ctx, &attestation, signature, storageOpts, auth)
110+
return b.uploadAttestation(ctx, &attestation, signature, storageOpts, remoteOpts...)
109111
}
110112

111113
// Fallback in case unsupported payload format is used or the deprecated "tekton" format
112114
logger.Info("Skipping upload to OCI registry, OCI storage backend is only supported for OCI images and in-toto attestations")
113115
return nil
114116
}
115117

118+
// buildRemoteOptions build remote options for OCI storage backend
119+
func (b *Backend) buildRemoteOptions(ctx context.Context, obj objects.TektonObject, storageOpts config.StorageOpts) ([]remote.Option, error) {
120+
opts := []remote.Option{}
121+
auth, err := b.getAuthenticator(ctx, obj, b.client)
122+
if err != nil {
123+
return nil, err
124+
}
125+
opts = append(opts, auth)
126+
if b.cfg.Storage.OCI.Insecure {
127+
opts = append(opts, remote.WithTransport(&http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}))
128+
}
129+
return opts, nil
130+
}
131+
116132
func (b *Backend) uploadSignature(ctx context.Context, format simple.SimpleContainerImage, rawPayload []byte, signature string, storageOpts config.StorageOpts, remoteOpts ...remote.Option) error {
117133
logger := logging.FromContext(ctx)
118134

pkg/chains/storage/oci/oci_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ package oci
1515

1616
import (
1717
"context"
18+
"crypto/tls"
1819
"encoding/json"
20+
"fmt"
1921
"net/http/httptest"
2022
"net/url"
2123
"strings"
2224
"testing"
25+
"time"
2326

2427
"github.com/tektoncd/chains/pkg/chains/formats"
2528
"github.com/tektoncd/chains/pkg/chains/formats/simple"
@@ -41,6 +44,7 @@ import (
4144
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4245
"k8s.io/client-go/kubernetes"
4346
logtesting "knative.dev/pkg/logging/testing"
47+
"knative.dev/pkg/webhook/certificates/resources"
4448
)
4549

4650
const namespace = "oci-test"
@@ -239,3 +243,122 @@ func TestBackend_StorePayload(t *testing.T) {
239243
})
240244
}
241245
}
246+
247+
// TestBackend_StorePayload_Insecure tests the StorePayload functionality with both secure and insecure configurations.
248+
// It verifies that:
249+
// 1. In secure mode, the backend should reject connections to untrusted registries
250+
// 2. In insecure mode, the backend should attempt to connect but fail due to missing image
251+
func TestBackend_StorePayload_Insecure(t *testing.T) {
252+
// Setup test registry with self-signed certificate
253+
s, registryURL := setupTestRegistry(t)
254+
defer s.Close()
255+
256+
testCases := []struct {
257+
name string
258+
insecure bool
259+
wantErrMsg string
260+
}{
261+
{
262+
name: "secure mode - should reject untrusted registry",
263+
insecure: false,
264+
wantErrMsg: "tls: failed to verify certificate: x509:",
265+
},
266+
{
267+
name: "insecure mode - should attempt connection but fail due to missing image",
268+
insecure: true,
269+
wantErrMsg: "getting signed image: entity not found in registry",
270+
},
271+
}
272+
273+
for _, tc := range testCases {
274+
t.Run(tc.name, func(t *testing.T) {
275+
// Initialize backend with test configuration
276+
b := &Backend{
277+
cfg: config.Config{
278+
Storage: config.StorageConfigs{
279+
OCI: config.OCIStorageConfig{
280+
Insecure: tc.insecure,
281+
},
282+
},
283+
},
284+
getAuthenticator: func(context.Context, objects.TektonObject, kubernetes.Interface) (remote.Option, error) {
285+
return remote.WithAuthFromKeychain(authn.DefaultKeychain), nil
286+
},
287+
}
288+
289+
// Create test reference and payload
290+
ref := registryURL + "/task/test@sha256:0000000000000000000000000000000000000000000000000000000000000000"
291+
simple := simple.SimpleContainerImage{
292+
Critical: payload.Critical{
293+
Identity: payload.Identity{
294+
DockerReference: registryURL + "/task/test",
295+
},
296+
Image: payload.Image{
297+
DockerManifestDigest: strings.Split(ref, "@")[1],
298+
},
299+
Type: payload.CosignSignatureType,
300+
},
301+
}
302+
303+
rawPayload, err := json.Marshal(simple)
304+
if err != nil {
305+
t.Fatalf("failed to marshal payload: %v", err)
306+
}
307+
308+
// Test StorePayload functionality
309+
ctx := logtesting.TestContextWithLogger(t)
310+
err = b.StorePayload(ctx, objects.NewTaskRunObjectV1Beta1(tr), rawPayload, "test", config.StorageOpts{ //nolint:staticcheck
311+
PayloadFormat: formats.PayloadTypeSimpleSigning,
312+
})
313+
314+
if err == nil {
315+
t.Error("expected error but got nil")
316+
return
317+
}
318+
if !strings.Contains(err.Error(), tc.wantErrMsg) {
319+
t.Errorf("error message mismatch\ngot: %v\nwant: %v", err, tc.wantErrMsg)
320+
}
321+
})
322+
}
323+
}
324+
325+
// setupTestRegistry sets up a test registry with TLS configuration
326+
func setupTestRegistry(t *testing.T) (*httptest.Server, string) {
327+
t.Helper()
328+
329+
cert, err := generateSelfSignedCert()
330+
if err != nil {
331+
t.Fatalf("failed to generate self-signed cert: %v", err)
332+
}
333+
334+
reg := registry.New()
335+
s := httptest.NewUnstartedServer(reg)
336+
s.TLS = &tls.Config{
337+
Certificates: []tls.Certificate{cert},
338+
}
339+
s.StartTLS()
340+
341+
u, _ := url.Parse(s.URL)
342+
return s, u.Host
343+
}
344+
345+
// generateSelfSignedCert generates a self-signed certificate for testing purposes
346+
// It uses knative's certificate generation utilities to create a proper certificate chain
347+
func generateSelfSignedCert() (tls.Certificate, error) {
348+
// Generate certificates with 24 hour validity
349+
notAfter := time.Now().Add(24 * time.Hour)
350+
351+
// Use test service name and namespace
352+
serverKey, serverCert, _, err := resources.CreateCerts(context.Background(), "test-registry", "test-namespace", notAfter)
353+
if err != nil {
354+
return tls.Certificate{}, fmt.Errorf("failed to generate certificates: %v", err)
355+
}
356+
357+
// Parse the generated certificates
358+
cert, err := tls.X509KeyPair(serverCert, serverKey)
359+
if err != nil {
360+
return tls.Certificate{}, fmt.Errorf("failed to parse certificate: %v", err)
361+
}
362+
363+
return cert, nil
364+
}

0 commit comments

Comments
 (0)