Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Supported keys include:
|:-------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
| `storage.gcs.bucket` | The GCS bucket for storage | | |
| `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. | |
| `storage.oci.repository.insecure` | Whether to use insecure connection when connecting to the OCI repository | `true`, `false` | `false` |
| `storage.docdb.url` | The go-cloud URI reference to a docstore collection | `firestore://projects/[PROJECT]/databases/(default)/documents/[COLLECTION]?name_field=name` | |
| `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]` | |
| `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` | |
Expand Down Expand Up @@ -104,9 +105,9 @@ You can provide MongoDB connection through different options
* This field overrides all others (`mongo-server-url-dir, mongo-server-url, and MONGO_SERVER_URL env var`)
* 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`

**NOTE** :-
**NOTE** :-
* 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
* 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
* 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

#### Grafeas

Expand Down
2 changes: 1 addition & 1 deletion pkg/chains/storage/oci/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (s *AttestationStorer) Store(ctx context.Context, req *api.StoreRequest[nam
se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
var entityNotFoundError *ociremote.EntityNotFoundError
if errors.As(err, &entityNotFoundError) {
se = ociremote.SignedUnknown(req.Artifact)
se = ociremote.SignedUnknown(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
Copy link
Member Author

Choose a reason for hiding this comment

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

I understand that the method added in #1395 requires these opts to be passed.

} else if err != nil {
return nil, errors.Wrap(err, "getting signed image")
}
Expand Down
30 changes: 27 additions & 3 deletions pkg/chains/storage/oci/legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ package oci

import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"

"github.com/tektoncd/chains/pkg/chains/formats"
"github.com/tektoncd/chains/pkg/chains/objects"
Expand Down Expand Up @@ -75,7 +77,7 @@ func NewStorageBackend(ctx context.Context, client kubernetes.Interface, cfg con
// StorePayload implements the storage.Backend interface.
func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, rawPayload []byte, signature string, storageOpts config.StorageOpts) error {
logger := logging.FromContext(ctx)
auth, err := b.getAuthenticator(ctx, obj, b.client)
remoteOpts, err := b.buildRemoteOptions(ctx, obj)
if err != nil {
return errors.Wrap(err, "getting oci authenticator")
}
Expand All @@ -87,7 +89,7 @@ func (b *Backend) StorePayload(ctx context.Context, obj objects.TektonObject, ra
if err := json.Unmarshal(rawPayload, &format); err != nil {
return errors.Wrap(err, "unmarshal simplesigning")
}
return b.uploadSignature(ctx, format, rawPayload, signature, storageOpts, auth)
return b.uploadSignature(ctx, format, rawPayload, signature, storageOpts, remoteOpts...)
}

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

return b.uploadAttestation(ctx, &attestation, signature, storageOpts, auth)
return b.uploadAttestation(ctx, &attestation, signature, storageOpts, remoteOpts...)
}

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

// buildRemoteOptions build remote options for OCI storage backend
func (b *Backend) buildRemoteOptions(ctx context.Context, obj objects.TektonObject) ([]remote.Option, error) {
opts := []remote.Option{}
auth, err := b.getAuthenticator(ctx, obj, b.client)
if err != nil {
return nil, err
}
opts = append(opts, auth)
if b.cfg.Storage.OCI.Insecure {
logger := logging.FromContext(ctx)
logger.Warn("Using insecure OCI registry connection. This skips TLS certificate verification and poses security risks. Only use this in testing or development environments.")
// InsecureSkipVerify is used only when explicitly configured for testing or development environments
// This is controlled by the user through configuration and should not be used in production
opts = append(opts, remote.WithTransport(&http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // #nosec G402
},
}))
}
return opts, nil
}

func (b *Backend) uploadSignature(ctx context.Context, format simple.SimpleContainerImage, rawPayload []byte, signature string, storageOpts config.StorageOpts, remoteOpts ...remote.Option) error {
logger := logging.FromContext(ctx)

Expand Down
134 changes: 134 additions & 0 deletions pkg/chains/storage/oci/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ package oci

import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

"github.com/tektoncd/chains/pkg/chains/formats"
"github.com/tektoncd/chains/pkg/chains/formats/simple"
Expand All @@ -41,6 +44,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
logtesting "knative.dev/pkg/logging/testing"
"knative.dev/pkg/webhook/certificates/resources"
)

const namespace = "oci-test"
Expand Down Expand Up @@ -239,3 +243,133 @@ func TestBackend_StorePayload(t *testing.T) {
})
}
}

// TestBackend_StorePayload_Insecure tests the StorePayload functionality with both secure and insecure configurations.
// It verifies that:
// 1. In secure mode, the backend should reject connections to untrusted registries due to TLS certificate verification failure
// 2. In insecure mode, the backend should successfully connect and upload signatures, bypassing TLS verification
func TestBackend_StorePayload_Insecure(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

🌟

// Setup test registry with self-signed certificate
s, registryURL := setupTestRegistry(t)
defer s.Close()

testCases := []struct {
name string
insecure bool
wantErr bool
wantErrMsg string
description string
}{
{
name: "secure mode with untrusted certificate",
insecure: false,
wantErr: true,
wantErrMsg: "tls: failed to verify certificate: x509:",
description: "Should reject connection to registry with self-signed certificate",
},
{
name: "insecure mode bypassing TLS verification",
insecure: true,
wantErr: false,
wantErrMsg: "",
description: "Should successfully connect and upload signature despite untrusted certificate",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Initialize backend with test configuration
b := &Backend{
cfg: config.Config{
Storage: config.StorageConfigs{
OCI: config.OCIStorageConfig{
Insecure: tc.insecure,
},
},
},
getAuthenticator: func(context.Context, objects.TektonObject, kubernetes.Interface) (remote.Option, error) {
return remote.WithAuthFromKeychain(authn.DefaultKeychain), nil
},
}

// Create test reference and payload
ref := registryURL + "/task/test@sha256:0000000000000000000000000000000000000000000000000000000000000000"
simple := simple.SimpleContainerImage{
Critical: payload.Critical{
Identity: payload.Identity{
DockerReference: registryURL + "/task/test",
},
Image: payload.Image{
DockerManifestDigest: strings.Split(ref, "@")[1],
},
Type: payload.CosignSignatureType,
},
}

rawPayload, err := json.Marshal(simple)
if err != nil {
t.Fatalf("failed to marshal payload: %v", err)
}

// Test StorePayload functionality
ctx := logtesting.TestContextWithLogger(t)
err = b.StorePayload(ctx, objects.NewTaskRunObjectV1(tr), rawPayload, "test", config.StorageOpts{
PayloadFormat: formats.PayloadTypeSimpleSigning,
})

// Validate test results based on expected outcome
if tc.wantErr {
if err == nil {
t.Errorf("%s: expected error but got nil", tc.description)
return
}
if tc.wantErrMsg != "" && !strings.Contains(err.Error(), tc.wantErrMsg) {
t.Errorf("%s: error message mismatch\ngot: %v\nwant: %v", tc.description, err, tc.wantErrMsg)
}
} else if err != nil {
t.Errorf("%s: expected success but got error: %v", tc.description, err)
}
})
}
}

// setupTestRegistry sets up a test registry with TLS configuration
func setupTestRegistry(t *testing.T) (*httptest.Server, string) {
t.Helper()

cert, err := generateSelfSignedCert()
if err != nil {
t.Fatalf("failed to generate self-signed cert: %v", err)
}

reg := registry.New()
s := httptest.NewUnstartedServer(reg)
s.TLS = &tls.Config{
Certificates: []tls.Certificate{cert},
}
s.StartTLS()

u, _ := url.Parse(s.URL)
return s, u.Host
}

// generateSelfSignedCert generates a self-signed certificate for testing purposes
// It uses knative's certificate generation utilities to create a proper certificate chain
func generateSelfSignedCert() (tls.Certificate, error) {
// Generate certificates with 24 hour validity
notAfter := time.Now().Add(24 * time.Hour)

// Use test service name and namespace
serverKey, serverCert, _, err := resources.CreateCerts(context.Background(), "test-registry", "test-namespace", notAfter)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to generate certificates: %w", err)
}

// Parse the generated certificates
cert, err := tls.X509KeyPair(serverCert, serverKey)
if err != nil {
return tls.Certificate{}, fmt.Errorf("failed to parse certificate: %w", err)
}

return cert, nil
}
2 changes: 1 addition & 1 deletion pkg/chains/storage/oci/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (s *SimpleStorer) Store(ctx context.Context, req *api.StoreRequest[name.Dig
se, err := ociremote.SignedEntity(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
var entityNotFoundError *ociremote.EntityNotFoundError
if errors.As(err, &entityNotFoundError) {
se = ociremote.SignedUnknown(req.Artifact)
se = ociremote.SignedUnknown(req.Artifact, ociremote.WithRemoteOptions(s.remoteOpts...))
} else if err != nil {
return nil, errors.Wrap(err, "getting signed image")
}
Expand Down
Loading