Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
17 changes: 15 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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 All @@ -77,6 +78,18 @@ Supported keys include:
| `storage.grafeas.notehint` (optional) | This field is used to set the [human_readable_name](https://github.com/grafeas/grafeas/blob/cd23d4dc1bef740d6d6d90d5007db5c9a2431c41/proto/v1/attestation.proto#L49) field in the Grafeas ATTESTATION note. If it is not provided, the default `This attestation note was generated by Tekton Chains` will be used. | | |
| `storage.archivista.url` | The URL endpoint for the Archivista service. | A valid HTTPS URL pointing to your Archivista instance (e.g. `https://archivista.testifysec.io`). | None |

> [!WARNING]
> **Security Considerations for `storage.oci.repository.insecure`**
>
> The `storage.oci.repository.insecure` flag allows connecting to OCI registries without TLS certificate verification. This feature is designed to ease developer overhead during testing and development where setting up HTTPS might be cumbersome.
>
> **Security Risks:**
> - **Production Environment Risk**: Enabling this flag in production environments can lead to serious security compromises. Administrators must ensure this flag is only enabled for development and testing purposes.
> - **Man-in-the-Middle Attacks**: Skipping TLS certificate verification makes the connection vulnerable to man-in-the-middle attacks where provenance could be tampered with.
> - **SLSA Guarantees Violation**: Tampered provenance can lead to violation of SLSA (Supply chain Levels for Software Artifacts) guarantees that Tekton Chains promises to provide.
>
> **Recommendation**: Only use `storage.oci.repository.insecure: true` in development or test environments. For production deployments, always use secure HTTPS connections with valid TLS certificates (`storage.oci.repository.insecure: false`, which is the default).

#### docstore

You can read about the go-cloud docstore URI format [here](https://gocloud.dev/howto/docstore/). Tekton Chains supports the following docstore services:
Expand Down Expand Up @@ -106,9 +119,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