From 71f1080b4153d18447e1cbc605e2310f345f80d3 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Thu, 20 Jul 2023 02:21:14 +0530 Subject: [PATCH 1/2] ocirepo: add cosign support for insecure http registries Add support for verifying insecure HTTP OCI repositories with cosign. If `.spec.insecure` set to true, then cosign uses plain HTTP connections to communicate with the registry. Signed-off-by: Sanskar Jaiswal --- internal/controller/ocirepository_controller.go | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index 7b282979e..9c7c0fed3 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -425,16 +425,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation || conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) { - // Insecure is not supported for verification - if obj.Spec.Insecure { - e := serror.NewGeneric( - fmt.Errorf("cosign does not support insecure registries"), - sourcev1.VerificationError, - ) - conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error()) - return sreconcile.ResultEmpty, e - } - err := r.verifySignature(ctx, obj, url, opts.verifyOpts...) if err != nil { provider := obj.Spec.Verify.Provider @@ -633,7 +623,11 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *ociv soci.WithRemoteOptions(opt...), } - ref, err := name.ParseReference(url) + var nameOpts []name.Option + if obj.Spec.Insecure { + nameOpts = append(nameOpts, name.Insecure) + } + ref, err := name.ParseReference(url, nameOpts...) if err != nil { return err } From fce7c10fc07bdddf44b826ad1ed93554421dcc4e Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Thu, 20 Jul 2023 02:43:24 +0530 Subject: [PATCH 2/2] oci: add tests for insecure cosign support; refactor test utils Add tests to test Cosign support for insecure registries. Furthermore, refactor OCI test utils to be more user friendly and enable accurate testing of HTTPS and HTTP OCI registries by circumnavigating Docker's automatic connection downgrade for registries hosted on localhost. Signed-off-by: Sanskar Jaiswal --- go.mod | 2 + go.sum | 10 + .../controller/helmchart_controller_test.go | 11 +- .../helmrepository_controller_oci_test.go | 4 + .../ocirepository_controller_test.go | 352 +++++++++--------- internal/controller/suite_test.go | 69 +++- internal/controller/testdata/certs/Makefile | 12 +- .../controller/testdata/certs/client-csr.json | 9 + .../controller/testdata/certs/client-key.pem | 5 + internal/controller/testdata/certs/client.csr | 8 + internal/controller/testdata/certs/client.pem | 13 + 11 files changed, 311 insertions(+), 184 deletions(-) create mode 100644 internal/controller/testdata/certs/client-csr.json create mode 100644 internal/controller/testdata/certs/client-key.pem create mode 100644 internal/controller/testdata/certs/client.csr create mode 100644 internal/controller/testdata/certs/client.pem diff --git a/go.mod b/go.mod index 73c0fe542..dd879376d 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/fluxcd/pkg/testserver v0.4.0 github.com/fluxcd/pkg/version v0.2.2 github.com/fluxcd/source-controller/api v1.0.0 + github.com/foxcpp/go-mockdns v1.0.0 github.com/go-git/go-billy/v5 v5.4.1 github.com/go-git/go-git/v5 v5.8.1 github.com/go-logr/logr v1.2.4 @@ -251,6 +252,7 @@ require ( github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.50 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect diff --git a/go.sum b/go.sum index 1eb6a1371..e84dedd58 100644 --- a/go.sum +++ b/go.sum @@ -418,6 +418,7 @@ github.com/fluxcd/pkg/testserver v0.4.0/go.mod h1:gjOKX41okmrGYOa4oOF2fiLedDAfPo github.com/fluxcd/pkg/version v0.2.2 h1:ZpVXECeLA5hIQMft11iLp6gN3cKcz6UNuVTQPw/bRdI= github.com/fluxcd/pkg/version v0.2.2/go.mod h1:NGnh/no8S6PyfCDxRFrPY3T5BUnqP48MxfxNRU0z8C0= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= +github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= @@ -862,7 +863,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -1261,6 +1264,7 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1341,6 +1345,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1367,6 +1372,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1431,6 +1437,8 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1549,6 +1557,7 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1591,6 +1600,7 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 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= diff --git a/internal/controller/helmchart_controller_test.go b/internal/controller/helmchart_controller_test.go index b7002245a..35d73695d 100644 --- a/internal/controller/helmchart_controller_test.go +++ b/internal/controller/helmchart_controller_test.go @@ -2285,8 +2285,12 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) { workspaceDir := t.TempDir() + tt.registryOpts.disableDNSMocking = true server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts) g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) // Load a test chart chartData, err := os.ReadFile(chartPath) @@ -2395,8 +2399,13 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T g := NewWithT(t) tmpDir := t.TempDir() - server, err := setupRegistryServer(ctx, tmpDir, registryOptions{}) + server, err := setupRegistryServer(ctx, tmpDir, registryOptions{ + disableDNSMocking: true, + }) g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) const ( chartPath = "testdata/charts/helmchart-0.1.0.tgz" diff --git a/internal/controller/helmrepository_controller_oci_test.go b/internal/controller/helmrepository_controller_oci_test.go index b2f11ccdf..2a5768026 100644 --- a/internal/controller/helmrepository_controller_oci_test.go +++ b/internal/controller/helmrepository_controller_oci_test.go @@ -250,8 +250,12 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) { WithStatusSubresource(&helmv1.HelmRepository{}) workspaceDir := t.TempDir() + tt.registryOpts.disableDNSMocking = true server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts) g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) obj := &helmv1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go index 15a2888f9..12350c377 100644 --- a/internal/controller/ocirepository_controller_test.go +++ b/internal/controller/ocirepository_controller_test.go @@ -18,7 +18,6 @@ package controller import ( "crypto/rand" - "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" @@ -26,9 +25,7 @@ import ( "errors" "fmt" "math/big" - "net" "net/http" - "net/http/httptest" "net/url" "os" "path" @@ -39,7 +36,6 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" - "github.com/google/go-containerregistry/pkg/registry" gcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" . "github.com/onsi/gomega" @@ -80,8 +76,11 @@ func TestOCIRepository_Reconcile(t *testing.T) { if err != nil { g.Expect(err).ToNot(HaveOccurred()) } + t.Cleanup(func() { + regServer.Close() + }) - podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, "6.1.4", "6.1.5", "6.1.6") + podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, true, "6.1.4", "6.1.5", "6.1.6") tests := []struct { name string @@ -146,6 +145,7 @@ func TestOCIRepository_Reconcile(t *testing.T) { URL: tt.url, Interval: metav1.Duration{Duration: 60 * time.Minute}, Reference: &ociv1.OCIRepositoryRef{}, + Insecure: true, }, } obj := origObj.DeepCopy() @@ -262,8 +262,11 @@ func TestOCIRepository_Reconcile_MediaType(t *testing.T) { if err != nil { g.Expect(err).ToNot(HaveOccurred()) } + t.Cleanup(func() { + regServer.Close() + }) - podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, "6.1.4", "6.1.5", "6.1.6") + podinfoVersions, err := pushMultiplePodinfoImages(regServer.registryHost, true, "6.1.4", "6.1.5", "6.1.6") tests := []struct { name string @@ -314,6 +317,7 @@ func TestOCIRepository_Reconcile_MediaType(t *testing.T) { LayerSelector: &ociv1.OCILayerSelector{ MediaType: tt.mediaType, }, + Insecure: true, }, } @@ -373,6 +377,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { craneOpts []crane.Option secretOpts secretOptions tlsCertSecret *corev1.Secret + insecure bool provider string providerImg string want sreconcile.Result @@ -380,8 +385,10 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { assertConditions []metav1.Condition }{ { - name: "HTTP without basic auth", - want: sreconcile.ResultSuccess, + name: "HTTP without basic auth", + want: sreconcile.ResultSuccess, + craneOpts: []crane.Option{crane.Insecure}, + insecure: true, assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), @@ -393,10 +400,13 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { registryOpts: registryOptions{ withBasicAuth: true, }, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - }), + insecure: true, + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, }, secretOpts: secretOptions{ username: testRegistryUsername, @@ -414,10 +424,13 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { registryOpts: registryOptions{ withBasicAuth: true, }, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - }), + insecure: true, + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, }, secretOpts: secretOptions{ username: testRegistryUsername, @@ -435,11 +448,14 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { registryOpts: registryOptions{ withBasicAuth: true, }, - wantErr: true, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - }), + insecure: true, + wantErr: true, + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, }, assertConditions: []metav1.Condition{ *conditions.TrueCondition(sourcev1.FetchFailedCondition, ociv1.OCIPullFailedReason, "failed to determine artifact digest"), @@ -452,10 +468,13 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { registryOpts: registryOptions{ withBasicAuth: true, }, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - }), + insecure: true, + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, }, secretOpts: secretOptions{ username: "wrong-pass", @@ -467,16 +486,19 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { }, }, { - name: "HTTP registry - basic auth with invalid serviceaccount", - want: sreconcile.ResultEmpty, - wantErr: true, + name: "HTTP registry - basic auth with invalid serviceaccount", + want: sreconcile.ResultEmpty, + wantErr: true, + insecure: true, registryOpts: registryOptions{ withBasicAuth: true, }, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - }), + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, }, secretOpts: secretOptions{ username: "wrong-pass", @@ -559,25 +581,32 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { wantErr: true, provider: "aws", providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test", + craneOpts: []crane.Option{ + crane.Insecure, + }, assertConditions: []metav1.Condition{ *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"), }, }, { - name: "with contextual login provider and secretRef", + name: "secretRef takes precedence over provider", want: sreconcile.ResultSuccess, registryOpts: registryOptions{ withBasicAuth: true, }, - craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{ - Username: testRegistryUsername, - Password: testRegistryPassword, - })}, + craneOpts: []crane.Option{ + crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + }), + crane.Insecure, + }, secretOpts: secretOptions{ username: testRegistryUsername, password: testRegistryPassword, includeSecret: true, }, + insecure: true, provider: "azure", assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), @@ -607,8 +636,10 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { workspaceDir := t.TempDir() server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts) - g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) img, err := createPodinfoImageFromTar("podinfo-6.1.6.tar", "6.1.6", server.registryHost, tt.craneOpts...) g.Expect(err).ToNot(HaveOccurred()) @@ -664,6 +695,9 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { Name: tt.tlsCertSecret.Name, } } + if tt.insecure { + obj.Spec.Insecure = true + } r := &OCIRepositoryReconciler{ Client: clientBuilder.Build(), @@ -672,7 +706,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { patchOptions: getPatchOptions(ociRepositoryReadyCondition.Owned, "sc"), } - opts := craneOptions(ctx, true) + opts := craneOptions(ctx, tt.insecure) opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain)) repoURL, err := r.getArtifactURL(obj, opts) g.Expect(err).To(BeNil()) @@ -706,34 +740,36 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { func TestOCIRepository_CertSecret(t *testing.T) { g := NewWithT(t) - srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err := createTLSServer() + tmpDir := t.TempDir() + regServer, err := setupRegistryServer(ctx, tmpDir, registryOptions{ + withTLS: true, + withClientCertAuth: true, + }) g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + regServer.Close() + }) - srv.StartTLS() - defer srv.Close() - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{}, - } - // Use the server cert as a CA cert, so the client trusts the - // server cert. (Only works because the server uses the same - // cert in both roles). pool := x509.NewCertPool() - pool.AddCert(srv.Certificate()) - transport.TLSClientConfig.RootCAs = pool - transport.TLSClientConfig.Certificates = []tls.Certificate{clientTLSCert} + pool.AppendCertsFromPEM(tlsCA) + clientTLSCert, err := tls.X509KeyPair(clientPublicKey, clientPrivateKey) + g.Expect(err).ToNot(HaveOccurred()) - srv.Client().Transport = transport - pi, err := createPodinfoImageFromTar("podinfo-6.1.5.tar", "6.1.5", srv.URL, []crane.Option{ - crane.WithTransport(srv.Client().Transport), + transport := http.DefaultTransport.(*http.Transport) + transport.TLSClientConfig = &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{clientTLSCert}, + } + pi, err := createPodinfoImageFromTar("podinfo-6.1.5.tar", "6.1.5", regServer.registryHost, []crane.Option{ + crane.WithTransport(transport), }...) g.Expect(err).NotTo(HaveOccurred()) tlsSecretClientCert := corev1.Secret{ - StringData: map[string]string{ - oci.CACert: string(rootCertPEM), - oci.ClientCert: string(clientCertPEM), - oci.ClientKey: string(clientKeyPEM), + Data: map[string][]byte{ + oci.CACert: tlsCA, + oci.ClientCert: clientPublicKey, + oci.ClientKey: clientPrivateKey, }, } @@ -758,17 +794,17 @@ func TestOCIRepository_CertSecret(t *testing.T) { url: pi.url, digest: pi.digest, expectreadyconition: false, - expectedstatusmessage: "unexpected status code 400 Bad Request: Client sent an HTTP request to an HTTPS server", + expectedstatusmessage: "tls: failed to verify certificate: x509:", }, { name: "test connection with with incorrect private key", url: pi.url, digest: pi.digest, certSecret: &corev1.Secret{ - StringData: map[string]string{ - oci.CACert: string(rootCertPEM), - oci.ClientCert: string(clientCertPEM), - oci.ClientKey: string("invalid-key"), + Data: map[string][]byte{ + oci.CACert: tlsCA, + oci.ClientCert: clientPublicKey, + oci.ClientKey: []byte("invalid-key"), }, }, expectreadyconition: false, @@ -859,8 +895,11 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { tmpDir := t.TempDir() server, err := setupRegistryServer(ctx, tmpDir, registryOptions{}) g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) - podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, "6.1.4", "6.1.5", "6.1.6") + podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, true, "6.1.4", "6.1.5", "6.1.6") img6 := podinfoVersions["6.1.6"] img5 := podinfoVersions["6.1.5"] @@ -1001,6 +1040,7 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { URL: fmt.Sprintf("oci://%s/podinfo", server.registryHost), Interval: metav1.Duration{Duration: interval}, Timeout: &metav1.Duration{Duration: timeout}, + Insecure: true, }, } @@ -1034,26 +1074,16 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) { func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { g := NewWithT(t) - tmpDir := t.TempDir() - server, err := setupRegistryServer(ctx, tmpDir, registryOptions{}) - g.Expect(err).ToNot(HaveOccurred()) - - podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, "6.1.4", "6.1.5") - g.Expect(err).ToNot(HaveOccurred()) - img4 := podinfoVersions["6.1.4"] - img5 := podinfoVersions["6.1.5"] - tests := []struct { name string reference *ociv1.OCIRepositoryRef insecure bool - digest string want sreconcile.Result wantErr bool wantErrMsg string shouldSign bool keyless bool - beforeFunc func(obj *ociv1.OCIRepository) + beforeFunc func(obj *ociv1.OCIRepository, tag, revision string) assertConditions []metav1.Condition }{ { @@ -1061,7 +1091,6 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { reference: &ociv1.OCIRepositoryRef{ Tag: "6.1.4", }, - digest: img4.digest.String(), shouldSign: true, want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ @@ -1075,7 +1104,6 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { reference: &ociv1.OCIRepositoryRef{ Tag: "6.1.5", }, - digest: img5.digest.String(), wantErr: true, wantErrMsg: "failed to verify the signature using provider 'cosign': no matching signatures were found for ''", want: sreconcile.ResultEmpty, @@ -1090,7 +1118,6 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { reference: &ociv1.OCIRepositoryRef{ Tag: "6.1.5", }, - digest: img5.digest.String(), wantErr: true, want: sreconcile.ResultEmpty, keyless: true, @@ -1103,21 +1130,19 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { { name: "verify failed before, removed from spec, remove condition", reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"}, - digest: img4.digest.String(), - beforeFunc: func(obj *ociv1.OCIRepository) { + beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) { conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg") obj.Spec.Verify = nil - obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", img4.tag, img4.digest.String())} + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)} }, want: sreconcile.ResultSuccess, }, { name: "same artifact, verified before, change in obj gen verify again", reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"}, - digest: img4.digest.String(), shouldSign: true, - beforeFunc: func(obj *ociv1.OCIRepository) { - obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", img4.tag, img4.digest.String())} + beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) { + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)} // Set Verified with old observed generation and different reason/message. conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified") // Set new object generation. @@ -1131,11 +1156,10 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { { name: "no verify for already verified, verified condition remains the same", reference: &ociv1.OCIRepositoryRef{Tag: "6.1.4"}, - digest: img4.digest.String(), shouldSign: true, - beforeFunc: func(obj *ociv1.OCIRepository) { + beforeFunc: func(obj *ociv1.OCIRepository, tag, revision string) { // Artifact present and custom verified condition reason/message. - obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", img4.tag, img4.digest.String())} + obj.Status.Artifact = &sourcev1.Artifact{Revision: fmt.Sprintf("%s@%s", tag, revision)} conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified") }, want: sreconcile.ResultSuccess, @@ -1144,19 +1168,17 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { }, }, { - name: "insecure registries are not supported", + name: "signed image on an insecure registry passes verification", reference: &ociv1.OCIRepositoryRef{ - Tag: "6.1.4", + Tag: "6.1.6", }, - digest: img4.digest.String(), shouldSign: true, insecure: true, - wantErr: true, - want: sreconcile.ResultEmpty, + want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), *conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new revision '' for ''"), - *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "cosign does not support insecure registries"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of revision "), }, }, } @@ -1179,6 +1201,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { keys, err := cosign.GenerateKeyPair(pf) g.Expect(err).ToNot(HaveOccurred()) + tmpDir := t.TempDir() err = os.WriteFile(path.Join(tmpDir, "cosign.key"), keys.PrivateBytes, 0600) g.Expect(err).ToNot(HaveOccurred()) @@ -1190,15 +1213,34 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { "cosign.pub": keys.PublicBytes, }} - err = r.Create(ctx, secret) - if err != nil { - g.Expect(err).NotTo(HaveOccurred()) + g.Expect(r.Create(ctx, secret)).NotTo(HaveOccurred()) + + caSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ca-cert-cosign", + Generation: 1, + }, + Data: map[string][]byte{ + "caFile": tlsCA, + }, } + g.Expect(r.Create(ctx, caSecret)).ToNot(HaveOccurred()) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + workspaceDir := t.TempDir() + regOpts := registryOptions{ + withTLS: !tt.insecure, + } + server, err := setupRegistryServer(ctx, workspaceDir, regOpts) + g.Expect(err).NotTo(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) + obj := &ociv1.OCIRepository{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "verify-oci-source-signature-", @@ -1216,6 +1258,10 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { if tt.insecure { obj.Spec.Insecure = true + } else { + obj.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: "ca-cert-cosign", + } } if !tt.keyless { @@ -1226,12 +1272,15 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { obj.Spec.Reference = tt.reference } + podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, tt.insecure, tt.reference.Tag) + g.Expect(err).ToNot(HaveOccurred()) + keychain, err := r.keychain(ctx, obj) if err != nil { g.Expect(err).ToNot(HaveOccurred()) } - opts := craneOptions(ctx, true) + opts := craneOptions(ctx, false) opts = append(opts, crane.WithAuthFromKeychain(keychain)) artifactURL, err := r.getArtifactURL(obj, opts) g.Expect(err).ToNot(HaveOccurred()) @@ -1250,21 +1299,22 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) { SkipConfirmation: true, TlogUpload: false, - Registry: coptions.RegistryOptions{Keychain: keychain, AllowInsecure: true}, + Registry: coptions.RegistryOptions{Keychain: keychain, AllowInsecure: true, AllowHTTPRegistry: tt.insecure}, }, []string{artifactURL}) g.Expect(err).ToNot(HaveOccurred()) } + image := podinfoVersions[tt.reference.Tag] assertConditions := tt.assertConditions for k := range assertConditions { - assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", fmt.Sprintf("%s@%s", tt.reference.Tag, tt.digest)) + assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", fmt.Sprintf("%s@%s", tt.reference.Tag, image.digest.String())) assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", artifactURL) assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "", "cosign") } if tt.beforeFunc != nil { - tt.beforeFunc(obj) + tt.beforeFunc(obj, image.tag, image.digest.String()) } g.Expect(r.Client.Create(ctx, obj)).ToNot(HaveOccurred()) @@ -1297,8 +1347,11 @@ func TestOCIRepository_reconcileSource_noop(t *testing.T) { tmpDir := t.TempDir() server, err := setupRegistryServer(ctx, tmpDir, registryOptions{}) g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) - _, err = pushMultiplePodinfoImages(server.registryHost, "6.1.5") + _, err = pushMultiplePodinfoImages(server.registryHost, true, "6.1.5") g.Expect(err).ToNot(HaveOccurred()) // NOTE: The following verifies if it was a noop run by checking the @@ -1431,6 +1484,7 @@ func TestOCIRepository_reconcileSource_noop(t *testing.T) { Reference: &ociv1.OCIRepositoryRef{Tag: "6.1.5"}, Interval: metav1.Duration{Duration: interval}, Timeout: &metav1.Duration{Duration: timeout}, + Insecure: true, }, } @@ -1709,9 +1763,11 @@ func TestOCIRepository_getArtifactURL(t *testing.T) { tmpDir := t.TempDir() server, err := setupRegistryServer(ctx, tmpDir, registryOptions{}) - g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + server.Close() + }) - imgs, err := pushMultiplePodinfoImages(server.registryHost, "6.1.4", "6.1.5", "6.1.6") + imgs, err := pushMultiplePodinfoImages(server.registryHost, true, "6.1.4", "6.1.5", "6.1.6") g.Expect(err).ToNot(HaveOccurred()) tests := []struct { @@ -1778,6 +1834,7 @@ func TestOCIRepository_getArtifactURL(t *testing.T) { URL: tt.url, Interval: metav1.Duration{Duration: interval}, Timeout: &metav1.Duration{Duration: timeout}, + Insecure: true, }, } @@ -2299,11 +2356,25 @@ func createPodinfoImageFromTar(tarFileName, tag, registryURL string, opts ...cra }, nil } -func pushMultiplePodinfoImages(serverURL string, versions ...string) (map[string]podinfoImage, error) { +func pushMultiplePodinfoImages(serverURL string, insecure bool, versions ...string) (map[string]podinfoImage, error) { podinfoVersions := make(map[string]podinfoImage) + var opts []crane.Option + // If the registry is insecure then instruct configure an insecure HTTP client, + // otherwise add the root CA certificate since the HTTPS server is self signed. + if insecure { + opts = append(opts, crane.Insecure) + } else { + transport := http.DefaultTransport.(*http.Transport) + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(tlsCA) + transport.TLSClientConfig = &tls.Config{ + RootCAs: pool, + } + opts = append(opts, crane.WithTransport(transport)) + } for i := 0; i < len(versions); i++ { - pi, err := createPodinfoImageFromTar(fmt.Sprintf("podinfo-%s.tar", versions[i]), versions[i], serverURL) + pi, err := createPodinfoImageFromTar(fmt.Sprintf("podinfo-%s.tar", versions[i]), versions[i], serverURL, opts...) if err != nil { return nil, err } @@ -2362,75 +2433,6 @@ func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv return } -func createTLSServer() (*httptest.Server, []byte, []byte, []byte, tls.Certificate, error) { - var clientTLSCert tls.Certificate - var rootCertPEM, clientCertPEM, clientKeyPEM []byte - - srv := httptest.NewUnstartedServer(registry.New()) - - // Create a self-signed cert to use as the CA and server cert. - rootKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - rootCertTmpl, err := certTemplate() - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - rootCertTmpl.IsCA = true - rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature - rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} - rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")} - var rootCert *x509.Certificate - rootCert, rootCertPEM, err = createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey) - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - - rootKeyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey), - }) - - // Create a TLS cert using the private key and certificate. - rootTLSCert, err := tls.X509KeyPair(rootCertPEM, rootKeyPEM) - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - - // To trust a client certificate, the server must be given a - // CA cert pool. - pool := x509.NewCertPool() - pool.AddCert(rootCert) - - srv.TLS = &tls.Config{ - ClientAuth: tls.RequireAndVerifyClientCert, - Certificates: []tls.Certificate{rootTLSCert}, - ClientCAs: pool, - } - - // Create a client cert, signed by the "CA". - clientKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - clientCertTmpl, err := certTemplate() - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature - clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} - _, clientCertPEM, err = createCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey) - if err != nil { - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err - } - // Encode and load the cert and private key for the client. - clientKeyPEM = pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey), - }) - clientTLSCert, err = tls.X509KeyPair(clientCertPEM, clientKeyPEM) - return srv, rootCertPEM, clientCertPEM, clientKeyPEM, clientTLSCert, err -} - func TestOCIContentConfigChanged(t *testing.T) { tests := []struct { name string diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 2602e5545..e9434f20f 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -21,12 +21,16 @@ import ( "context" "fmt" "io" + "io/ioutil" + "log" "math/rand" + "net" "os" "path/filepath" "testing" "time" + "github.com/foxcpp/go-mockdns" "github.com/phayes/freeport" "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" @@ -95,9 +99,11 @@ var ( ) var ( - tlsPublicKey []byte - tlsPrivateKey []byte - tlsCA []byte + tlsPublicKey []byte + tlsPrivateKey []byte + tlsCA []byte + clientPublicKey []byte + clientPrivateKey []byte ) var ( @@ -114,11 +120,18 @@ type registryClientTestServer struct { registryHost string workspaceDir string registryClient *helmreg.Client + dnsServer *mockdns.Server } type registryOptions struct { - withBasicAuth bool - withTLS bool + withBasicAuth bool + withTLS bool + withClientCertAuth bool + // Allow disbaling DNS mocking since Helm OCI doesn't yet suppot + // insecure OCI registries, which means we need Docker's automatic + // connection downgrading if the registry is hosted on localhost. + // Once Helm OCI supports insecure registries, we can get rid of this. + disableDNSMocking bool } func setupRegistryServer(ctx context.Context, workspaceDir string, opts registryOptions) (*registryClientTestServer, error) { @@ -150,7 +163,28 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry } server.registryHost = fmt.Sprintf("localhost:%d", port) - config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) + + // Change the registry host to a host which is not localhost and + // mock DNS to map example.com to 127.0.0.1. + // This is required because Docker enforces HTTP if the registry + // is hosted on localhost/127.0.0.1. + if !opts.disableDNSMocking { + server.registryHost = fmt.Sprintf("example.com:%d", port) + // Disable DNS server logging as it is extremely chatty. + dnsLog := log.Default() + dnsLog.SetOutput(ioutil.Discard) + server.dnsServer, err = mockdns.NewServerWithLogger(map[string]mockdns.Zone{ + "example.com.": { + A: []string{"127.0.0.1"}, + }, + }, dnsLog, false) + if err != nil { + return nil, err + } + server.dnsServer.PatchNet(net.DefaultResolver) + } + + config.HTTP.Addr = fmt.Sprintf(":%d", port) config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} @@ -178,6 +212,10 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry if opts.withTLS { config.HTTP.TLS.Certificate = "testdata/certs/server.pem" config.HTTP.TLS.Key = "testdata/certs/server-key.pem" + // Configure CA certificates only if client cert authentication is enabled. + if opts.withClientCertAuth { + config.HTTP.TLS.ClientCAs = []string{"testdata/certs/ca.pem"} + } } // setup logger options @@ -198,6 +236,13 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry return server, nil } +func (r *registryClientTestServer) Close() { + if r.dnsServer != nil { + mockdns.UnpatchNet(net.DefaultResolver) + r.dnsServer.Close() + } +} + func TestMain(m *testing.M) { initTestTLS() @@ -229,11 +274,13 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("failed to create workspace directory: %v", err)) } testRegistryServer, err = setupRegistryServer(ctx, testWorkspaceDir, registryOptions{ - withBasicAuth: true, + withBasicAuth: true, + disableDNSMocking: true, }) if err != nil { panic(fmt.Sprintf("Failed to create a test registry server: %v", err)) } + defer testRegistryServer.Close() if err := (&GitRepositoryReconciler{ Client: testEnv, @@ -355,6 +402,14 @@ func initTestTLS() { if err != nil { panic(err) } + clientPrivateKey, err = os.ReadFile("testdata/certs/client-key.pem") + if err != nil { + panic(err) + } + clientPublicKey, err = os.ReadFile("testdata/certs/client.pem") + if err != nil { + panic(err) + } } func newTestStorage(s *testserver.HTTPServer) (*Storage, error) { diff --git a/internal/controller/testdata/certs/Makefile b/internal/controller/testdata/certs/Makefile index dca2408c3..22b40466b 100644 --- a/internal/controller/testdata/certs/Makefile +++ b/internal/controller/testdata/certs/Makefile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -all: server-key.pem +all: server-key.pem client-key.pem ca-key.pem: ca-csr.json cfssl gencert -initca ca-csr.json | cfssljson -bare ca – @@ -28,3 +28,13 @@ server-key.pem: server-csr.json ca-config.json ca-key.pem server-csr.json | cfssljson -bare server sever.pem: server-key.pem server.csr: server-key.pem + +client-key.pem: client-csr.json ca-config.json ca-key.pem + cfssl gencert \ + -ca=ca.pem \ + -ca-key=ca-key.pem \ + -config=ca-config.json \ + -profile=web-servers \ + client-csr.json | cfssljson -bare client +client.pem: client-key.pem +client.csr: client-key.pem diff --git a/internal/controller/testdata/certs/client-csr.json b/internal/controller/testdata/certs/client-csr.json new file mode 100644 index 000000000..0baf11601 --- /dev/null +++ b/internal/controller/testdata/certs/client-csr.json @@ -0,0 +1,9 @@ +{ + "CN": "example.com", + "hosts": [ + "127.0.0.1", + "localhost", + "example.com", + "www.example.com" + ] +} diff --git a/internal/controller/testdata/certs/client-key.pem b/internal/controller/testdata/certs/client-key.pem new file mode 100644 index 000000000..b39c483d0 --- /dev/null +++ b/internal/controller/testdata/certs/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICpqb1p1TH98yoFXEEt6JmWc/Snb8NaYyz8jfTOVDBLOoAoGCCqGSM49 +AwEHoUQDQgAERjzob4CCuyv+cYPyTYCPHwGuqSNGNuX3UGWpxvzwEqjYEWiePlOz +eJLk4DWaVX8CmVakNLsK/EHnBv9ErG7QYQ== +-----END EC PRIVATE KEY----- diff --git a/internal/controller/testdata/certs/client.csr b/internal/controller/testdata/certs/client.csr new file mode 100644 index 000000000..41f498804 --- /dev/null +++ b/internal/controller/testdata/certs/client.csr @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBHDCBwwIBADAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABEY86G+Agrsr/nGD8k2Ajx8BrqkjRjbl91Blqcb88BKo2BFo +nj5Ts3iS5OA1mlV/AplWpDS7CvxB5wb/RKxu0GGgSzBJBgkqhkiG9w0BCQ4xPDA6 +MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxl +LmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiAHmtr9fDDx5eyFfY7r5m8xA4Wh +Jm+TB6/czvXRNNOKzAIhAN7ln6BpneEm2oqIBGqvfc3pETC6jdGJxCfYw+X+7von +-----END CERTIFICATE REQUEST----- diff --git a/internal/controller/testdata/certs/client.pem b/internal/controller/testdata/certs/client.pem new file mode 100644 index 000000000..4a85663ea --- /dev/null +++ b/internal/controller/testdata/certs/client.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7DCCAZKgAwIBAgIUPJmKtZ6CfSxybx2BSsVS5EVun0swCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjMwNzE5MTExMzAwWhcNMzMw +NzE2MTExMzAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABEY86G+Agrsr/nGD8k2Ajx8BrqkjRjbl91Blqcb88BKo2BFo +nj5Ts3iS5OA1mlV/AplWpDS7CvxB5wb/RKxu0GGjgbowgbcwDgYDVR0PAQH/BAQD +AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA +MB0GA1UdDgQWBBTgAyCQoH/EJqz/nY5DJa/uvWWshzAfBgNVHSMEGDAWgBQGyUiU +1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu +Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSAAwRQIgKSJH +YvhKiXcUUzRoL6FsXQeAlhemSg3lD9se+BhRF8ECIQDx2UpWFLDe5NOPqhrcR1Sd +haFriAG8eR1yD3u3nJvY6g== +-----END CERTIFICATE-----