diff --git a/pkg/controller/container-runtime-config/container_runtime_config_controller.go b/pkg/controller/container-runtime-config/container_runtime_config_controller.go index de9832c163..132ede057d 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller.go @@ -1007,7 +1007,7 @@ func registriesConfigIgnition(templateDir string, controllerConfig *mcfgv1.Contr return nil, fmt.Errorf("could not update policy json with new changes: %w", err) } // generates configuration under /etc/containers/registries.d to enable sigstore verification - sigstoreRegistriesConfigYaml, err = generateSigstoreRegistriesdConfig(clusterScopePolicies) + sigstoreRegistriesConfigYaml, err = generateSigstoreRegistriesdConfig(clusterScopePolicies, registriesTOML) if err != nil { return nil, err } diff --git a/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go b/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go index 21a1641d2c..f81414d18a 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go @@ -534,7 +534,7 @@ func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.Machin } if verifyImagePoliciesRegistriesConfig { - expectedRegistriesConfd, err := generateSigstoreRegistriesdConfig(clusterScopePolicies) + expectedRegistriesConfd, err := generateSigstoreRegistriesdConfig(clusterScopePolicies, expectedRegistriesConf) require.NoError(t, err) foundFile := false @@ -1794,3 +1794,54 @@ func TestClusterImagePolicyCreate(t *testing.T) { }) } } + +func TestSigstoreRegistriesConfigIDMSandCIPCreate(t *testing.T) { + for _, platform := range []apicfgv1.PlatformType{apicfgv1.AWSPlatformType, apicfgv1.NonePlatformType, "unrecognized"} { + t.Run(string(platform), func(t *testing.T) { + f := newFixture(t) + + cc := newControllerConfig(ctrlcommon.ControllerConfigName, platform) + mcp := helpers.NewMachineConfigPool("master", nil, helpers.MasterSelector, "v0") + mcp2 := helpers.NewMachineConfigPool("worker", nil, helpers.WorkerSelector, "v0") + imgcfg1 := newImageConfig("cluster", &apicfgv1.RegistrySources{InsecureRegistries: []string{"blah.io"}, AllowedRegistries: []string{"example.com"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + + cvcfg1 := newClusterVersionConfig("version", "test.io/myuser/myimage:test") + keyReg1, _ := getManagedKeyReg(mcp, nil) + keyReg2, _ := getManagedKeyReg(mcp2, nil) + + mcs1 := helpers.NewMachineConfig(keyReg1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + mcs2 := helpers.NewMachineConfig(keyReg2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + + // idms source is the same as cip scope + idms := newIDMS("built-in", []apicfgv1.ImageDigestMirrors{ + {Source: "built-in-source.example.com", Mirrors: []apicfgv1.ImageMirror{"built-in-mirror.example.com"}}, + }) + clusterimgPolicy := newClusterImagePolicyWithPublicKey("built-in-source.example.com", []string{"example.com"}, []byte("foo bar")) + f.ccLister = append(f.ccLister, cc) + f.mcpLister = append(f.mcpLister, mcp) + f.mcpLister = append(f.mcpLister, mcp2) + f.imgLister = append(f.imgLister, imgcfg1) + f.idmsLister = append(f.idmsLister, idms) + f.clusterImagePolicyLister = append(f.clusterImagePolicyLister, clusterimgPolicy) + f.cvLister = append(f.cvLister, cvcfg1) + f.imgObjects = append(f.imgObjects, imgcfg1) + + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectCreateMachineConfigAction(mcs1) + + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + + f.expectCreateMachineConfigAction(mcs2) + + f.run("") + + for _, mcName := range []string{mcs1.Name, mcs2.Name} { + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idms, nil, clusterimgPolicy, cc.Spec.ReleaseImage, true, true, true) + } + }) + } +} diff --git a/pkg/controller/container-runtime-config/helpers.go b/pkg/controller/container-runtime-config/helpers.go index 1d11282629..0555f0cd6f 100644 --- a/pkg/controller/container-runtime-config/helpers.go +++ b/pkg/controller/container-runtime-config/helpers.go @@ -20,6 +20,7 @@ import ( storageconfig "github.com/containers/storage/pkg/config" ign3types "github.com/coreos/ignition/v2/config/v3_4/types" "github.com/ghodss/yaml" + "github.com/opencontainers/go-digest" apicfgv1 "github.com/openshift/api/config/v1" apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" @@ -933,17 +934,69 @@ func validateClusterImagePolicyWithAllowedBlockedRegistries(clusterScopePolicies return nil } -func generateSigstoreRegistriesdConfig(clusterScopePolicies map[string]signature.PolicyRequirements) ([]byte, error) { +func generateSigstoreRegistriesdConfig(clusterScopePolicies map[string]signature.PolicyRequirements, registriesTOML []byte) ([]byte, error) { if len(clusterScopePolicies) == 0 { return nil, nil } - registriesDockerConfig := make(map[string]dockerConfig) - sigstoreAttachment := dockerConfig{ - UseSigstoreAttachments: true, + var ( + err error + tmpFile *os.File + registriesConf = sysregistriesv2.V2RegistriesConf{} + registriesDockerConfig = make(map[string]dockerConfig) + sigstoreAttachment = dockerConfig{ + UseSigstoreAttachments: true, + } + ) + + if registriesTOML != nil { + tmpFile, err = os.CreateTemp("", "regtemp") + if err != nil { + return nil, err + } + defer func() { + tmpFile.Close() + os.Remove(tmpFile.Name()) + }() + + _, err = tmpFile.Write(registriesTOML) + if err != nil { + return nil, err + } + + if _, err := toml.NewDecoder(bytes.NewBuffer(registriesTOML)).Decode(®istriesConf); err != nil { + return nil, fmt.Errorf("error decoding registries config: %w", err) + } } - for scope := range clusterScopePolicies { - registriesDockerConfig[scope] = sigstoreAttachment + + for policyScope := range clusterScopePolicies { + registriesDockerConfig[policyScope] = sigstoreAttachment + if registriesTOML == nil { + continue + } + parentReg, err := sysregistriesv2.FindRegistry(&types.SystemContext{SystemRegistriesConfPath: tmpFile.Name()}, policyScope) + if err != nil { + return nil, err + } + + if err = addScopeMirrorsSigstoreRegistriesdConfig(registriesDockerConfig, policyScope, parentReg, sigstoreAttachment); err != nil { + return nil, fmt.Errorf("error adding clusterimagepolicy scope %s relevant mirrors: %w", policyScope, err) + } + for _, reg := range registriesConf.Registries { + scope := reg.Location + if reg.Prefix != "" { + scope = reg.Prefix + } + if runtimeutils.ScopeIsNestedInsideScope(scope, policyScope) && scope != policyScope { + nestedReg, err := sysregistriesv2.FindRegistry(&types.SystemContext{SystemRegistriesConfPath: tmpFile.Name()}, scope) + if err != nil { + return nil, err + } + if err = addScopeMirrorsSigstoreRegistriesdConfig(registriesDockerConfig, scope, nestedReg, sigstoreAttachment); err != nil { + return nil, fmt.Errorf("error adding clusterimagepolicy scope %s relevant mirrors: %w", policyScope, err) + } + } + } } registriesConfig := ®istriesConfig{} @@ -954,3 +1007,62 @@ func generateSigstoreRegistriesdConfig(clusterScopePolicies map[string]signature } return data, nil } + +func addScopeMirrorsSigstoreRegistriesdConfig(registriesDockerConfig map[string]dockerConfig, scope string, reg *sysregistriesv2.Registry, sigstoreAttachment dockerConfig) error { + if reg == nil { + return nil + } + dummyDigest := "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + dummyTag := "aaa" + dummyPath := "/a" + dummyPrefix := "matched" + addedDummy := false + hasWildcard := false + + if strings.HasPrefix(scope, "*.") { + scope = strings.Replace(scope, "*", dummyPrefix, 1) + hasWildcard = true + } + + if !strings.Contains(scope, "/") { + scope += dummyPath + addedDummy = true + } + + scopeRef, err := reference.Parse(scope) + if err != nil { + return fmt.Errorf("error parsing scope %s: %w", scope, err) + } + + repo := reference.TrimNamed(scopeRef.(reference.Named)) + + d, _ := digest.Parse(dummyDigest) + digestRef, err := reference.WithDigest(repo, d) + if err != nil { + return fmt.Errorf("error parsing digest name for scope %s: %w", scope, err) + } + tagRef, err := reference.WithTag(repo, dummyTag) + if err != nil { + return fmt.Errorf("error parsing tag name for scope %s: %w", scope, err) + } + + digestSources, err := reg.PullSourcesFromReference(digestRef) + if err != nil { + return fmt.Errorf("error getting digest sources for scope %s: %w", scope, err) + } + tagSources, err := reg.PullSourcesFromReference(tagRef) + if err != nil { + return fmt.Errorf("error getting tag sources for scope %s: %w", scope, err) + } + for _, s := range append(digestSources, tagSources...) { + endpoint := s.Reference.Name() + if addedDummy { + endpoint = strings.TrimSuffix(endpoint, dummyPath) + } + if hasWildcard { + endpoint = strings.Replace(endpoint, dummyPrefix, "*", 1) + } + registriesDockerConfig[endpoint] = sigstoreAttachment + } + return nil +} diff --git a/pkg/controller/container-runtime-config/helpers_test.go b/pkg/controller/container-runtime-config/helpers_test.go index 6754409c50..5dd539988b 100644 --- a/pkg/controller/container-runtime-config/helpers_test.go +++ b/pkg/controller/container-runtime-config/helpers_test.go @@ -523,6 +523,23 @@ func clusterImagePolicyTestCRs() map[string]apicfgv1alpha1.ClusterImagePolicy { }, }, }, + "test-cr2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr2", + }, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"a.com/a1/a2", "a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000", "*.example.com", "policy.scope"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: testKeyData, + RekorKeyData: testRekorKeyData, + }, + }, + }, + }, + }, } return testClusterImagePolicyCRs } @@ -1245,22 +1262,223 @@ func TestGetValidScopePolicies(t *testing.T) { } func TestGenerateSigstoreRegistriesConfig(t *testing.T) { - testClusterImagePolicyCR0 := clusterImagePolicyTestCRs()["test-cr0"] - testClusterImagePolicyCR1 := clusterImagePolicyTestCRs()["test-cr1"] - expectSigstoreRegistriesConfig := []byte( - `docker: - test0.com: + // testcase CIP scopes: + // "a.com/a1/a2", + // "a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000", + // "*.example.com", + // "policy.scope" + testClusterImagePolicyCR2 := clusterImagePolicyTestCRs()["test-cr2"] + + clusterScopePolicies, err := getValidScopePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testClusterImagePolicyCR2}) + require.NoError(t, err) + + type testcase struct { + name string + clusterScopePolices map[string]signature.PolicyRequirements + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsRules []*apicfgv1.ImageDigestMirrorSet + itmsRules []*apicfgv1.ImageTagMirrorSet + expectedSigstoreRegistriesConfig []byte + } + + testcases := []testcase{ + { + name: "cip objects exist, icsp/idms/itms are empty", + clusterScopePolices: clusterScopePolicies, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true +`), + }, + { + name: "cip has no revelant registries mirrors", + clusterScopePolices: clusterScopePolicies, + icspRules: []*apioperatorsv1alpha1.ImageContentSourcePolicy{ + { + Spec: apioperatorsv1alpha1.ImageContentSourcePolicySpec{ + RepositoryDigestMirrors: []apioperatorsv1alpha1.RepositoryDigestMirrors{ + {Source: "x.com/x1/x2", Mirrors: []string{"x-x1-x2.mirror/x1/x2"}}, + }, + }, + }, + }, + idmsRules: []*apicfgv1.ImageDigestMirrorSet{ + { + Spec: apicfgv1.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{ // other.com is neither insecure nor blocked + {Source: "y.com/y1/y2", Mirrors: []apicfgv1.ImageMirror{"y-y1-y2.mirror/y1/y2"}}, + }, + }, + }, + }, + itmsRules: []*apicfgv1.ImageTagMirrorSet{ + { + Spec: apicfgv1.ImageTagMirrorSetSpec{ + ImageTagMirrors: []apicfgv1.ImageTagMirrors{ + {Source: "z.com/z1/z2", Mirrors: []apicfgv1.ImageMirror{"z-z1-z2.mirror/z1/z2"}}, + }, + }, + }, + }, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true +`), + }, + { + name: "cip scope a.com/a1/a2 is super scope of registries sources", + clusterScopePolices: clusterScopePolicies, + idmsRules: []*apicfgv1.ImageDigestMirrorSet{ + { + Spec: apicfgv1.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{ + {Source: "a.com/a1/a2", Mirrors: []apicfgv1.ImageMirror{"a-a1-a2.mirror/a1/a2"}}, + {Source: "a.com/a1/a2/a3", Mirrors: []apicfgv1.ImageMirror{"a-a1-a2-a3.mirror/m1/m2/m3"}}, + {Source: "a.com/a1/a2/a3-1", Mirrors: []apicfgv1.ImageMirror{"a-a1-a2-a3-1.mirror/m1/m2/m3"}}, + {Source: "x.com/x1/x2", Mirrors: []apicfgv1.ImageMirror{"x-x1-x2.mirror/m1/m2"}}, + }, + }, + }, + }, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + a-a1-a2-a3-1.mirror/m1/m2/m3: use-sigstore-attachments: true - test1.com: + a-a1-a2-a3.mirror/m1/m2/m3: use-sigstore-attachments: true -`) + a-a1-a2.mirror/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2/a3: + use-sigstore-attachments: true + a.com/a1/a2/a3-1: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true +`), + }, + { + name: "cip scope a.com/a1/a2 nested in registries sources", + clusterScopePolices: clusterScopePolicies, + idmsRules: []*apicfgv1.ImageDigestMirrorSet{ + { + Spec: apicfgv1.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{ + {Source: "a.com", Mirrors: []apicfgv1.ImageMirror{"a.mirror"}}, + {Source: "a.com/a1", Mirrors: []apicfgv1.ImageMirror{"a-a1.mirror/a1"}}, + {Source: "x.com/x1/x2", Mirrors: []apicfgv1.ImageMirror{"x-x1-x2.mirror/x1/x2"}}, + }, + }, + }, + }, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + a-a1.mirror/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true +`), + }, + { + name: "cip wildcard scope *.example.com is the mirror source", + clusterScopePolices: clusterScopePolicies, + idmsRules: []*apicfgv1.ImageDigestMirrorSet{ + { + Spec: apicfgv1.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{ + {Source: "*.example.com", Mirrors: []apicfgv1.ImageMirror{"star-x-example.mirror"}}, + }, + }, + }, + }, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true + star-x-example.mirror: + use-sigstore-attachments: true +`), + }, + { + name: "cip scope *.example.com and mirror source have wildcard matching", + clusterScopePolices: clusterScopePolicies, + idmsRules: []*apicfgv1.ImageDigestMirrorSet{ + { + Spec: apicfgv1.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{ + {Source: "a.example.com", Mirrors: []apicfgv1.ImageMirror{"a-example.mirror"}}, + {Source: "*.x.example.com", Mirrors: []apicfgv1.ImageMirror{"star-x-example.mirror"}}, + {Source: "*.scope", Mirrors: []apicfgv1.ImageMirror{"start-scope.mirror"}}, + {Source: "x/x1/x2", Mirrors: []apicfgv1.ImageMirror{"x-x1-x2.mirror/x1/x2"}}, + }, + }, + }, + }, + expectedSigstoreRegistriesConfig: []byte( + `docker: + '*.example.com': + use-sigstore-attachments: true + '*.x.example.com': + use-sigstore-attachments: true + a-example.mirror: + use-sigstore-attachments: true + a.com/a1/a2: + use-sigstore-attachments: true + a.com/a1/a2@sha256:0000000000000000000000000000000000000000000000000000000000000000: + use-sigstore-attachments: true + a.example.com: + use-sigstore-attachments: true + policy.scope: + use-sigstore-attachments: true + star-x-example.mirror: + use-sigstore-attachments: true + start-scope.mirror: + use-sigstore-attachments: true +`), + }, + } - clusterScopePolicies, err := getValidScopePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testClusterImagePolicyCR0, &testClusterImagePolicyCR1}) - require.NoError(t, err) - got, err := generateSigstoreRegistriesdConfig(clusterScopePolicies) - require.NoError(t, err) - require.Equal(t, string(expectSigstoreRegistriesConfig), string(got)) + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + registriesTOML, err := updateRegistriesConfig(templateRegistriesConfig, nil, nil, tc.icspRules, tc.idmsRules, tc.itmsRules) + require.NoError(t, err) + got, err := generateSigstoreRegistriesdConfig(tc.clusterScopePolices, registriesTOML) + require.NoError(t, err) + require.Equal(t, string(tc.expectedSigstoreRegistriesConfig), string(got)) + }) + } } func TestGeneratePolicyJSON(t *testing.T) {