From 38a1de35ae12da4c27bd4db13a0d335e3dcc9ebe Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Mon, 10 Jul 2023 19:53:53 -0400 Subject: [PATCH 1/3] cluster-policy Signed-off-by: Qi Wang --- cmd/machine-config-controller/start.go | 2 + docs/ClusterImagePolicyDesign.md | 48 ++ ...lusterimagepolicy-CustomNoUpgrade.crd.yaml | 243 ++++++++++ ...rimagepolicy-TechPreviewNoUpgrade.crd.yaml | 243 ++++++++++ ...or_01_imagepolicy-CustomNoUpgrade.crd.yaml | 243 ++++++++++ ..._imagepolicy-TechPreviewNoUpgrade.crd.yaml | 243 ++++++++++ ...operator_00_clusterreader_clusterrole.yaml | 10 + .../machineconfigcontroller/clusterrole.yaml | 2 +- pkg/apihelpers/apihelpers.go | 10 + pkg/controller/bootstrap/bootstrap.go | 38 +- .../testdata/bootstrap/featuregate.yaml | 2 + .../container_runtime_config_controller.go | 449 +++++++++++++++--- ...ontainer_runtime_config_controller_test.go | 312 ++++++++++-- .../container-runtime-config/helpers.go | 269 ++++++++++- .../container-runtime-config/helpers_test.go | 371 ++++++++++++++- pkg/daemon/constants/constants.go | 6 + pkg/daemon/update.go | 6 + test/e2e-bootstrap/bootstrap_test.go | 6 +- 18 files changed, 2382 insertions(+), 121 deletions(-) create mode 100644 docs/ClusterImagePolicyDesign.md create mode 100644 install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml create mode 100644 install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml create mode 100644 install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml create mode 100644 install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml diff --git a/cmd/machine-config-controller/start.go b/cmd/machine-config-controller/start.go index e151a5d3b7..b1f7bcac3b 100644 --- a/cmd/machine-config-controller/start.go +++ b/cmd/machine-config-controller/start.go @@ -172,6 +172,8 @@ func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controlle ctx.ConfigInformerFactory.Config().V1().Images(), ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(), ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(), + ctx.ConfigInformerFactory.Config().V1alpha1().ImagePolicies(), + ctx.ConfigInformerFactory.Config().V1alpha1().ClusterImagePolicies(), ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(), ctx.ConfigInformerFactory.Config().V1().ClusterVersions(), ctx.ClientBuilder.KubeClientOrDie("container-runtime-config-controller"), diff --git a/docs/ClusterImagePolicyDesign.md b/docs/ClusterImagePolicyDesign.md new file mode 100644 index 0000000000..d844effd7d --- /dev/null +++ b/docs/ClusterImagePolicyDesign.md @@ -0,0 +1,48 @@ +## Summary +ClusterImagePolicy and ImagePolicy are CRDs that managed by ContainerRuntimeConfig controller. These CRDs allow setting up configurations for CRI-O to verify the container images signed using [Sigstore](https://www.sigstore.dev/) tools. + +## Goals +Generating corresponding CRI-O configuration files for image signature verification. Rollout ClusterImagePolicy to `/etc/containers/policy.json` for cluster wide configuration. Roll out ImagePolicy to `/etc/crio/policies/.json` for pod namespace-separated signature policies configuration. + +## Non-Goals +Rolling out configuration for OCP payload repositories. The (super scope of) OCP payload repositories will not be written to the configuration files. + +## CRD +[ClusterImagePolicy CRD](https://github.com/openshift/api/blob/master/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml) + +[ImagePolicy CRD](https://github.com/openshift/api/blob/master/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml) + +## Example + +## Validation and Troubleshooting + +## Implementation Details +The ContainerRuntimeConfigController would perform the following steps: + +1. Validate the ClusterImagePolicy and ImagePolicy objects on the cluster. Follow the table below to ignore the conflicting scopes. + +| |process the policies from the CRs | | | | +|----------------------------------------------------------------------------------------------------------------- |------------------------------------------------ |----------------------------------------------------------------------------------- |--- |--- | +| same scope in different CRs | ImagePolicy | ClusterImagePolicy | | | +| ClusterImagePolicy ImagePolicy (scope in the ClusterImagePolicy is equal to or broader than in the ImagePolicy) | Do not deploy non-global policy for this scope | Write the cluster policy to `/etc/containers/policy.json` and `.json` | | | +| ClusterImagePolicy ClusterImagePolicy | N/A | Append the policy to existing `etc/containers/policy.json` | | | +| ImagePolicy ImagePolicy | append the policy to .json | N/A | | | + +2. Render the current MachineConfigs (storage.files.contents[policy.json]) into the originalPolicyIgn + +3. Serialize the cluster level policies to `policy.json`. + +4. Copy the cluster policy.json to `.json`, serialize the namespace level policies to `.json`. + +5. Add registries configuration to `/etc/containers/registries.d/sigstore-registries.yaml`. This configuration is used to specify the sigstore is being used as the image signature verification backend. + +6. Update the ignition file `/etc/containers/policy.json` within the `99--generated-registries` MachineConfig. + +7. Create or Update the ignition file `/etc/crio/policies/.json` within the `99--generated-imagepolicies` MachineConfig. + +After deletion all of the ClusterImagePolicy or the ImagePolicy instance the config will be reverted to the original policy.json. + +## See Also +see **[containers-policy.json(5)](https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md)**, **[containers-registries.d(5)](https://github.com/containers/image/blob/main/docs/containers-registries.d.5.md)** for more information. + + diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml new file mode 100644 index 0000000000..c12c81f8c1 --- /dev/null +++ b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml @@ -0,0 +1,243 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: CustomNoUpgrade + name: clusterimagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ClusterImagePolicy + listKind: ClusterImagePolicyList + plural: clusterimagepolicies + singular: clusterimagepolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." + type: object + required: + - spec + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + type: object + required: + - policy + - scopes + properties: + policy: + description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. + type: object + required: + - rootOfTrust + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + type: object + required: + - policyType + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' + type: object + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + type: string + maxLength: 8192 + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. + type: object + required: + - oidcIssuer + - signedEmail + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - rule: isURL(self) + message: oidcIssuer must be a valid URL + signedEmail: + description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - rule: self.matches('^\\S+@\\S+$') + message: invalid email address + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + policyType: + description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. + type: string + enum: + - PublicKey + - FulcioCAWithRekor + publicKey: + description: publicKey defines the root of trust based on a sigstore public key. + type: object + required: + - keyData + properties: + keyData: + description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. + type: string + maxLength: 8192 + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + x-kubernetes-validations: + - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' + message: publicKey is required when policyType is PublicKey, and forbidden otherwise + - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise + signedIdentity: + description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". + type: object + required: + - matchPolicy + properties: + exactRepository: + description: exactRepository is required if matchPolicy is set to "ExactRepository". + type: object + required: + - repository + properties: + repository: + description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + matchPolicy: + description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. + type: string + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + remapIdentity: + description: remapIdentity is required if matchPolicy is set to "RemapIdentity". + type: object + required: + - prefix + - signedPrefix + properties: + prefix: + description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + signedPrefix: + description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + x-kubernetes-validations: + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' + message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' + message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise + scopes: + description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + type: array + maxItems: 256 + items: + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' + message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' + - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' + message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching + - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' + message: invalid repository namespace or image specification in the image scope + x-kubernetes-list-type: set + status: + description: status contains the observed state of the resource. + type: object + properties: + conditions: + description: conditions provide details on the status of this API Resource. + type: array + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml new file mode 100644 index 0000000000..69b69e764b --- /dev/null +++ b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -0,0 +1,243 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: TechPreviewNoUpgrade + name: clusterimagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ClusterImagePolicy + listKind: ClusterImagePolicyList + plural: clusterimagepolicies + singular: clusterimagepolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." + type: object + required: + - spec + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + type: object + required: + - policy + - scopes + properties: + policy: + description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. + type: object + required: + - rootOfTrust + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + type: object + required: + - policyType + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' + type: object + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + type: string + maxLength: 8192 + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. + type: object + required: + - oidcIssuer + - signedEmail + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - rule: isURL(self) + message: oidcIssuer must be a valid URL + signedEmail: + description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - rule: self.matches('^\\S+@\\S+$') + message: invalid email address + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + policyType: + description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. + type: string + enum: + - PublicKey + - FulcioCAWithRekor + publicKey: + description: publicKey defines the root of trust based on a sigstore public key. + type: object + required: + - keyData + properties: + keyData: + description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. + type: string + maxLength: 8192 + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + x-kubernetes-validations: + - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' + message: publicKey is required when policyType is PublicKey, and forbidden otherwise + - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise + signedIdentity: + description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". + type: object + required: + - matchPolicy + properties: + exactRepository: + description: exactRepository is required if matchPolicy is set to "ExactRepository". + type: object + required: + - repository + properties: + repository: + description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + matchPolicy: + description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. + type: string + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + remapIdentity: + description: remapIdentity is required if matchPolicy is set to "RemapIdentity". + type: object + required: + - prefix + - signedPrefix + properties: + prefix: + description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + signedPrefix: + description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + x-kubernetes-validations: + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' + message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' + message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise + scopes: + description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + type: array + maxItems: 256 + items: + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' + message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' + - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' + message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching + - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' + message: invalid repository namespace or image specification in the image scope + x-kubernetes-list-type: set + status: + description: status contains the observed state of the resource. + type: object + properties: + conditions: + description: conditions provide details on the status of this API Resource. + type: array + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml new file mode 100644 index 0000000000..afc22036ce --- /dev/null +++ b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml @@ -0,0 +1,243 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: CustomNoUpgrade + name: imagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ImagePolicy + listKind: ImagePolicyList + plural: imagepolicies + singular: imagepolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." + type: object + required: + - spec + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + type: object + required: + - policy + - scopes + properties: + policy: + description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. + type: object + required: + - rootOfTrust + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + type: object + required: + - policyType + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' + type: object + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + type: string + maxLength: 8192 + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. + type: object + required: + - oidcIssuer + - signedEmail + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - rule: isURL(self) + message: oidcIssuer must be a valid URL + signedEmail: + description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - rule: self.matches('^\\S+@\\S+$') + message: invalid email address + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + policyType: + description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. + type: string + enum: + - PublicKey + - FulcioCAWithRekor + publicKey: + description: publicKey defines the root of trust based on a sigstore public key. + type: object + required: + - keyData + properties: + keyData: + description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. + type: string + maxLength: 8192 + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + x-kubernetes-validations: + - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' + message: publicKey is required when policyType is PublicKey, and forbidden otherwise + - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise + signedIdentity: + description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". + type: object + required: + - matchPolicy + properties: + exactRepository: + description: exactRepository is required if matchPolicy is set to "ExactRepository". + type: object + required: + - repository + properties: + repository: + description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + matchPolicy: + description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. + type: string + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + remapIdentity: + description: remapIdentity is required if matchPolicy is set to "RemapIdentity". + type: object + required: + - prefix + - signedPrefix + properties: + prefix: + description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + signedPrefix: + description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + x-kubernetes-validations: + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' + message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' + message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise + scopes: + description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + type: array + maxItems: 256 + items: + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' + message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' + - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' + message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching + - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' + message: invalid repository namespace or image specification in the image scope + x-kubernetes-list-type: set + status: + description: status contains the observed state of the resource. + type: object + properties: + conditions: + description: conditions provide details on the status of this API Resource. + type: array + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml new file mode 100644 index 0000000000..d800072ba6 --- /dev/null +++ b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -0,0 +1,243 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: TechPreviewNoUpgrade + name: imagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ImagePolicy + listKind: ImagePolicyList + plural: imagepolicies + singular: imagepolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." + type: object + required: + - spec + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + type: object + required: + - policy + - scopes + properties: + policy: + description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. + type: object + required: + - rootOfTrust + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + type: object + required: + - policyType + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' + type: object + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + type: string + maxLength: 8192 + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. + type: object + required: + - oidcIssuer + - signedEmail + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - rule: isURL(self) + message: oidcIssuer must be a valid URL + signedEmail: + description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - rule: self.matches('^\\S+@\\S+$') + message: invalid email address + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + policyType: + description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. + type: string + enum: + - PublicKey + - FulcioCAWithRekor + publicKey: + description: publicKey defines the root of trust based on a sigstore public key. + type: object + required: + - keyData + properties: + keyData: + description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. + type: string + maxLength: 8192 + rekorKeyData: + description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. + type: string + maxLength: 8192 + x-kubernetes-validations: + - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' + message: publicKey is required when policyType is PublicKey, and forbidden otherwise + - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise + signedIdentity: + description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". + type: object + required: + - matchPolicy + properties: + exactRepository: + description: exactRepository is required if matchPolicy is set to "ExactRepository". + type: object + required: + - repository + properties: + repository: + description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + matchPolicy: + description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. + type: string + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + remapIdentity: + description: remapIdentity is required if matchPolicy is set to "RemapIdentity". + type: object + required: + - prefix + - signedPrefix + properties: + prefix: + description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + signedPrefix: + description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' + message: invalid repository or prefix in the signedIdentity, should not include the tag or digest + - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + message: invalid repository or prefix in the signedIdentity + x-kubernetes-validations: + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' + message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise + - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' + message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise + scopes: + description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + type: array + maxItems: 256 + items: + type: string + maxLength: 512 + x-kubernetes-validations: + - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' + message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' + - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' + message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching + - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' + message: invalid repository namespace or image specification in the image scope + x-kubernetes-list-type: set + status: + description: status contains the observed state of the resource. + type: object + properties: + conditions: + description: conditions provide details on the status of this API Resource. + type: array + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + type: object + required: + - lastTransitionTime + - message + - reason + - status + - type + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + type: string + format: date-time + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + type: string + maxLength: 32768 + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + type: integer + format: int64 + minimum: 0 + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + type: string + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + status: + description: status of the condition, one of True, False, Unknown. + type: string + enum: + - "True" + - "False" + - Unknown + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + type: string + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml b/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml index fc831ee40f..93457445ca 100644 --- a/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml +++ b/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml @@ -56,3 +56,13 @@ rules: - get - list - watch + - apiGroups: + - config.openshift.io + resources: + - imagepolicies + - clusterimagepolicies + verbs: + - get + - list + - watch + - update diff --git a/manifests/machineconfigcontroller/clusterrole.yaml b/manifests/machineconfigcontroller/clusterrole.yaml index e4f520a02a..bd0d17975b 100644 --- a/manifests/machineconfigcontroller/clusterrole.yaml +++ b/manifests/machineconfigcontroller/clusterrole.yaml @@ -13,7 +13,7 @@ rules: resources: ["configmaps", "secrets"] verbs: ["*"] - apiGroups: ["config.openshift.io"] - resources: ["images", "clusterversions", "featuregates", "nodes", "nodes/status"] + resources: ["images", "clusterversions", "featuregates", "nodes", "nodes/status", "imagepolicies", "imagepolicies/status", "clusterimagepolicies", "clusterimagepolicies/status"] verbs: ["*"] - apiGroups: ["config.openshift.io"] resources: ["schedulers", "apiservers", "infrastructures", "imagedigestmirrorsets", "imagetagmirrorsets"] diff --git a/pkg/apihelpers/apihelpers.go b/pkg/apihelpers/apihelpers.go index e8f41d2bd1..70caaedaf7 100644 --- a/pkg/apihelpers/apihelpers.go +++ b/pkg/apihelpers/apihelpers.go @@ -99,6 +99,16 @@ func NewKubeletConfigCondition(condType mcfgv1.KubeletConfigStatusConditionType, } } +func NewCondition(condType string, status metav1.ConditionStatus, reason, message string) *metav1.Condition { + return &metav1.Condition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + // NewContainerRuntimeConfigCondition returns an instance of a ContainerRuntimeConfigCondition func NewContainerRuntimeConfigCondition(condType mcfgv1.ContainerRuntimeConfigStatusConditionType, status corev1.ConditionStatus, message string) *mcfgv1.ContainerRuntimeConfigCondition { return &mcfgv1.ContainerRuntimeConfigCondition{ diff --git a/pkg/controller/bootstrap/bootstrap.go b/pkg/controller/bootstrap/bootstrap.go index c432cdd949..cc50397ce6 100644 --- a/pkg/controller/bootstrap/bootstrap.go +++ b/pkg/controller/bootstrap/bootstrap.go @@ -19,6 +19,7 @@ import ( "k8s.io/klog/v2" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" @@ -72,20 +73,25 @@ func (b *Bootstrap) Run(destDir string) error { mcfgv1.Install(scheme) apioperatorsv1alpha1.Install(scheme) apicfgv1.Install(scheme) + apicfgv1alpha1.Install(scheme) codecFactory := serializer.NewCodecFactory(scheme) - decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion) - - var cconfig *mcfgv1.ControllerConfig - var featureGate *apicfgv1.FeatureGate - var nodeConfig *apicfgv1.Node - var kconfigs []*mcfgv1.KubeletConfig - var pools []*mcfgv1.MachineConfigPool - var configs []*mcfgv1.MachineConfig - var crconfigs []*mcfgv1.ContainerRuntimeConfig - var icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy - var idmsRules []*apicfgv1.ImageDigestMirrorSet - var itmsRules []*apicfgv1.ImageTagMirrorSet - var imgCfg *apicfgv1.Image + decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion) + + var ( + cconfig *mcfgv1.ControllerConfig + featureGate *apicfgv1.FeatureGate + nodeConfig *apicfgv1.Node + kconfigs []*mcfgv1.KubeletConfig + pools []*mcfgv1.MachineConfigPool + configs []*mcfgv1.MachineConfig + crconfigs []*mcfgv1.ContainerRuntimeConfig + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsRules []*apicfgv1.ImageDigestMirrorSet + itmsRules []*apicfgv1.ImageTagMirrorSet + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy + imagePolicies []*apicfgv1alpha1.ImagePolicy + imgCfg *apicfgv1.Image + ) for _, info := range infos { if info.IsDir() { continue @@ -132,6 +138,10 @@ func (b *Bootstrap) Run(destDir string) error { itmsRules = append(itmsRules, obj) case *apicfgv1.Image: imgCfg = obj + case *apicfgv1alpha1.ClusterImagePolicy: + clusterImagePolicies = append(clusterImagePolicies, obj) + case *apicfgv1alpha1.ImagePolicy: + imagePolicies = append(imagePolicies, obj) case *apicfgv1.FeatureGate: if obj.GetName() == ctrlcommon.ClusterFeatureInstanceName { featureGate = obj @@ -168,7 +178,7 @@ func (b *Bootstrap) Run(destDir string) error { configs = append(configs, iconfigs...) - rconfigs, err := containerruntimeconfig.RunImageBootstrap(b.templatesDir, cconfig, pools, icspRules, idmsRules, itmsRules, imgCfg, fgAccess) + rconfigs, err := containerruntimeconfig.RunImageBootstrap(b.templatesDir, cconfig, pools, icspRules, idmsRules, itmsRules, imgCfg, clusterImagePolicies, imagePolicies, fgAccess) if err != nil { return err } diff --git a/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml b/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml index 57bcab9d54..eaeda285cd 100644 --- a/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml +++ b/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml @@ -7,3 +7,5 @@ status: - version: 0.0.1-snapshot enabled: - name: OpenShiftPodSecurityAdmission + disabled: + - name: SigstoreImageVerification 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 b3d82196cd..29bb9cba7f 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller.go @@ -9,15 +9,22 @@ import ( "time" "github.com/clarketm/json" + signature "github.com/containers/image/v5/signature" ign3types "github.com/coreos/ignition/v2/config/v3_4/types" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" configclientset "github.com/openshift/client-go/config/clientset/versioned" cligoinformersv1 "github.com/openshift/client-go/config/informers/externalversions/config/v1" + cligoinformersv1alpha1 "github.com/openshift/client-go/config/informers/externalversions/config/v1alpha1" cligolistersv1 "github.com/openshift/client-go/config/listers/config/v1" + cligolistersv1alpha1 "github.com/openshift/client-go/config/listers/config/v1alpha1" + operatorinformersv1alpha1 "github.com/openshift/client-go/operator/informers/externalversions/operator/v1alpha1" + operatorlistersv1alpha1 "github.com/openshift/client-go/operator/listers/operator/v1alpha1" "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + runtimeutils "github.com/openshift/runtime-utils/pkg/registries" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" @@ -41,6 +48,7 @@ import ( "github.com/openshift/client-go/machineconfiguration/clientset/versioned/scheme" mcfginformersv1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1" mcfglistersv1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1" + apihelpers "github.com/openshift/machine-config-operator/pkg/apihelpers" ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" mtmpl "github.com/openshift/machine-config-operator/pkg/controller/template" "github.com/openshift/machine-config-operator/pkg/version" @@ -98,6 +106,12 @@ type Controller struct { itmsLister cligolistersv1.ImageTagMirrorSetLister itmsListerSynced cache.InformerSynced + imagePolicyLister cligolistersv1alpha1.ImagePolicyLister + imagePolicyListerSynced cache.InformerSynced + + clusterImagePolicyLister cligolistersv1alpha1.ClusterImagePolicyLister + clusterImagePolicyListerSynced cache.InformerSynced + mcpLister mcfglistersv1.MachineConfigPoolLister mcpListerSynced cache.InformerSynced @@ -119,6 +133,8 @@ func New( imgInformer cligoinformersv1.ImageInformer, idmsInformer cligoinformersv1.ImageDigestMirrorSetInformer, itmsInformer cligoinformersv1.ImageTagMirrorSetInformer, + imagePolicyInformer cligoinformersv1alpha1.ImagePolicyInformer, + clusterImagePolicyInformer cligoinformersv1alpha1.ClusterImagePolicyInformer, icspInformer operatorinformersv1alpha1.ImageContentSourcePolicyInformer, clusterVersionInformer cligoinformersv1.ClusterVersionInformer, kubeClient clientset.Interface, @@ -169,6 +185,18 @@ func New( DeleteFunc: ctrl.itmsConfDeleted, }) + imagePolicyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: ctrl.imagePolicyAdded, + UpdateFunc: ctrl.imagePolicyUpdated, + DeleteFunc: ctrl.imagePolicyDeleted, + }) + + clusterImagePolicyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: ctrl.clusterImagePolicyAdded, + UpdateFunc: ctrl.clusterImagePolicyUpdated, + DeleteFunc: ctrl.clusterImagePolicyDeleted, + }) + ctrl.syncHandler = ctrl.syncContainerRuntimeConfig ctrl.syncImgHandler = ctrl.syncImageConfig ctrl.enqueueContainerRuntimeConfig = ctrl.enqueue @@ -197,6 +225,12 @@ func New( ctrl.clusterVersionLister = clusterVersionInformer.Lister() ctrl.clusterVersionListerSynced = clusterVersionInformer.Informer().HasSynced + ctrl.imagePolicyLister = imagePolicyInformer.Lister() + ctrl.imagePolicyListerSynced = imagePolicyInformer.Informer().HasSynced + + ctrl.clusterImagePolicyLister = clusterImagePolicyInformer.Lister() + ctrl.clusterImagePolicyListerSynced = clusterVersionInformer.Informer().HasSynced + ctrl.featureGateAccess = featureGateAccess return ctrl @@ -207,9 +241,12 @@ func (ctrl *Controller) Run(workers int, stopCh <-chan struct{}) { defer utilruntime.HandleCrash() defer ctrl.queue.ShutDown() defer ctrl.imgQueue.ShutDown() - - if !cache.WaitForCacheSync(stopCh, ctrl.mcpListerSynced, ctrl.mccrListerSynced, ctrl.ccListerSynced, - ctrl.imgListerSynced, ctrl.icspListerSynced, ctrl.idmsListerSynced, ctrl.itmsListerSynced, ctrl.clusterVersionListerSynced) { + listerCaches := []cache.InformerSynced{ctrl.mcpListerSynced, ctrl.mccrListerSynced, ctrl.ccListerSynced, + ctrl.imgListerSynced, ctrl.icspListerSynced, ctrl.idmsListerSynced, ctrl.itmsListerSynced, ctrl.clusterVersionListerSynced} + if ctrl.sigstoreAPIEnabled() { + listerCaches = append(listerCaches, ctrl.imagePolicyListerSynced, ctrl.clusterImagePolicyListerSynced) + } + if !cache.WaitForCacheSync(stopCh, listerCaches...) { return } @@ -284,6 +321,87 @@ func (ctrl *Controller) itmsConfDeleted(_ interface{}) { ctrl.imgQueue.Add("openshift-config") } +func (ctrl *Controller) imagePolicyAdded(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) imagePolicyUpdated(_, _ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) imagePolicyDeleted(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imagePoliciesDeleted() +} + +func (ctrl *Controller) clusterImagePolicyAdded(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) clusterImagePolicyUpdated(_, _ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) clusterImagePolicyDeleted(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imagePoliciesDeleted() +} + +func (ctrl *Controller) imagePoliciesDeleted() { + clusterImagePolicies, err := ctrl.clusterImagePolicyLister.List(labels.Everything()) + if err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("error listing clusterimagepolicies: %v", err)) + + } + imagepolicies, err := ctrl.imagePolicyLister.List(labels.Everything()) + if err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("error listing imagepolicies: %v", err)) + } + if len(imagepolicies) == 0 && len(clusterImagePolicies) == 0 { + ctrl.deleteImagePolicyMachineConfig() + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) sigstoreAPIEnabled() bool { + featureGates, err := ctrl.featureGateAccess.CurrentFeatureGates() + if err != nil { + utilruntime.HandleError(fmt.Errorf("error getting current featuregates: %v", err)) + return false + } + klog.Infof("imageverification sigstore FeatureGates: %v", featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification)) + return featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification) +} + +func (ctrl *Controller) deleteImagePolicyMachineConfig() { + pools, err := ctrl.mcpLister.List(labels.Everything()) + if err != nil { + utilruntime.HandleError(fmt.Errorf("error listing machineconfigpools: %v", err)) + } + for _, pool := range pools { + imagePolicyMCName := getManagedKeyNamespacedImagePolicy(pool) + if err = ctrl.client.MachineconfigurationV1().MachineConfigs().Delete(context.TODO(), imagePolicyMCName, metav1.DeleteOptions{}); err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("couldn't delete image policy machine config %#v: %w", imagePolicyMCName, err)) + } + } +} + func (ctrl *Controller) updateContainerRuntimeConfig(oldObj, newObj interface{}) { oldCtrCfg := oldObj.(*mcfgv1.ContainerRuntimeConfig) newCtrCfg := newObj.(*mcfgv1.ContainerRuntimeConfig) @@ -791,7 +909,24 @@ func (ctrl *Controller) syncImageConfig(key string) error { var ( registriesBlocked, policyBlocked, allowedRegs []string releaseImage string + imagePolicies []*apicfgv1alpha1.ImagePolicy + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements + clusterScopePolicies map[string]signature.PolicyRequirements ) + if ctrl.sigstoreAPIEnabled() { + // Find all ImagePolicy objects + imagePolicies, err = ctrl.imagePolicyLister.List(labels.Everything()) + if err != nil && errors.IsNotFound(err) { + imagePolicies = []*apicfgv1alpha1.ImagePolicy{} + } + // Find all ClusterImagePolicy objects + clusterImagePolicies, err = ctrl.clusterImagePolicyLister.List(labels.Everything()) + if err != nil && errors.IsNotFound(err) { + clusterImagePolicies = []*apicfgv1alpha1.ClusterImagePolicy{} + } + } + if clusterVersionCfg != nil { // The possibility of releaseImage being "" is very unlikely, will only happen if clusterVersionCfg is nil. If this happens // then there is something very wrong with the cluster and in that situation it would be best to fail here till clusterVersionCfg @@ -804,6 +939,13 @@ func (ctrl *Controller) syncImageConfig(key string) error { } else if err == errParsingReference { return err } + + // Get the valid cluster and namespace imagepolicies, update the skipped error to the status + // skip the scope if the scope is for release image repo or + // skip the scope in the namespaced imagepolicy if there is scope overlap between cluster and namespaced policy + if clusterScopePolicies, scopeNamespacePolicies, err = getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, releaseImage, ctrl); err != nil { + return err + } } // Get ControllerConfig @@ -824,63 +966,33 @@ func (ctrl *Controller) syncImageConfig(key string) error { for _, pool := range mcpPools { // To keep track of whether we "actually" got an updated image config applied := true + // To keep track of whether we "actually" got an updated 99--generated-imagepolicies machine config + appliedImagePolicy := true role := pool.Name // Get MachineConfig managedKey, err := getManagedKeyReg(pool, ctrl.client) if err != nil { return err } + managedKeyNamespaceImagePolicy := getManagedKeyNamespacedImagePolicy(pool) if err := retry.RetryOnConflict(updateBackoff, func() error { - registriesIgn, err := registriesConfigIgnition(ctrl.templatesDir, controllerConfig, role, releaseImage, + registriesIgn, namespacedPoliciesIgn, err := registriesConfigIgnition(ctrl.templatesDir, controllerConfig, role, releaseImage, imgcfg.Spec.RegistrySources.InsecureRegistries, registriesBlocked, policyBlocked, allowedRegs, - imgcfg.Spec.RegistrySources.ContainerRuntimeSearchRegistries, icspRules, idmsRules, itmsRules, ctrl.featureGateAccess) + imgcfg.Spec.RegistrySources.ContainerRuntimeSearchRegistries, icspRules, idmsRules, itmsRules, ctrl.featureGateAccess, clusterScopePolicies, scopeNamespacePolicies) if err != nil { return err } - rawRegistriesIgn, err := json.Marshal(registriesIgn) + + applied, err = ctrl.syncIgnitionConfig(managedKey, registriesIgn, pool, ownerReferenceForImageConfig(imgcfg)) if err != nil { - return fmt.Errorf("could not encode registries Ignition config: %w", err) - } - mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), managedKey, metav1.GetOptions{}) - if err != nil && !errors.IsNotFound(err) { - return fmt.Errorf("could not find MachineConfig: %w", err) + return fmt.Errorf("could not sync registries Ignition config: %w", err) } - isNotFound := errors.IsNotFound(err) - if !isNotFound && equality.Semantic.DeepEqual(rawRegistriesIgn, mc.Spec.Config.Raw) { - // if the configuration for the registries is equal, we still need to compare - // the generated controller version because during an upgrade we need a new one - mcCtrlVersion := mc.Annotations[ctrlcommon.GeneratedByControllerVersionAnnotationKey] - if mcCtrlVersion == version.Hash { - applied = false - return nil - } - } - if isNotFound { - tempIgnCfg := ctrlcommon.NewIgnConfig() - mc, err = ctrlcommon.MachineConfigFromIgnConfig(role, managedKey, tempIgnCfg) + if namespacedPoliciesIgn != nil { + appliedImagePolicy, err = ctrl.syncIgnitionConfig(managedKeyNamespaceImagePolicy, namespacedPoliciesIgn, pool, ownerReferenceForImagePolicy(imagePolicies, clusterImagePolicies)) if err != nil { - return fmt.Errorf("could not create MachineConfig from new Ignition config: %w", err) + return fmt.Errorf("could not sync imagepolicies Ignition config: %w", err) } } - mc.Spec.Config.Raw = rawRegistriesIgn - mc.ObjectMeta.Annotations = map[string]string{ - ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, - } - mc.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ - { - APIVersion: apicfgv1.SchemeGroupVersion.String(), - Kind: "Image", - Name: imgcfg.Name, - UID: imgcfg.UID, - }, - } - // Create or Update, on conflict retry - if isNotFound { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) - } else { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) - } - return err }); err != nil { return fmt.Errorf("could not Create/Update MachineConfig: %w", err) @@ -889,51 +1001,123 @@ func (ctrl *Controller) syncImageConfig(key string) error { klog.Infof("Applied ImageConfig cluster on MachineConfigPool %v", pool.Name) ctrlcommon.UpdateStateMetric(ctrlcommon.MCCSubControllerState, "machine-config-controller-container-runtime-config", "Sync Image Config", pool.Name) } + if appliedImagePolicy { + klog.Infof("Applied (cluster)imagepolicies on MachineConfigPool %v", pool.Name) + } } return nil } +func (ctrl *Controller) syncIgnitionConfig(managedKey string, ignFile *ign3types.Config, pool *mcfgv1.MachineConfigPool, ownerRef metav1.OwnerReference) (bool, error) { + appliedCtrlVersion := true + rawRegistriesIgn, err := json.Marshal(ignFile) + if err != nil { + return false, fmt.Errorf("could not encode Ignition config: %w", err) + } + mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), managedKey, metav1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + return false, fmt.Errorf("could not find MachineConfig: %w", err) + } + isNotFound := errors.IsNotFound(err) + if !isNotFound && equality.Semantic.DeepEqual(rawRegistriesIgn, mc.Spec.Config.Raw) { + // if the configuration for the registries is equal, we still need to compare + // the generated controller version because during an upgrade we need a new one + mcCtrlVersion := mc.Annotations[ctrlcommon.GeneratedByControllerVersionAnnotationKey] + if mcCtrlVersion == version.Hash { + appliedCtrlVersion = false + return appliedCtrlVersion, nil + } + } + if isNotFound { + tempIgnCfg := ctrlcommon.NewIgnConfig() + mc, err = ctrlcommon.MachineConfigFromIgnConfig(pool.Name, managedKey, tempIgnCfg) + if err != nil { + return false, fmt.Errorf("could not create MachineConfig from new Ignition config: %w", err) + } + } + mc.Spec.Config.Raw = rawRegistriesIgn + mc.ObjectMeta.Annotations = map[string]string{ + ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, + } + mc.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerRef} + // Create or Update, on conflict retry + if isNotFound { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) + } else { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) + } + + return appliedCtrlVersion, err +} + func registriesConfigIgnition(templateDir string, controllerConfig *mcfgv1.ControllerConfig, role, releaseImage string, insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs []string, - icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, featureGateAccess featuregates.FeatureGateAccess) (*ign3types.Config, error) { + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, featureGateAccess featuregates.FeatureGateAccess, + clusterScopePolicies map[string]signature.PolicyRequirements, + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) (*ign3types.Config, *ign3types.Config, error) { var ( - registriesTOML []byte - policyJSON []byte + registriesTOML []byte + policyJSON []byte + namespacedJSONBase []byte + namespacedPolicyJSONs map[string][]byte + sigstoreRegistriesConfigYaml []byte + imagePolicyIgn *ign3types.Config + err error ) // Generate the original registries config _, originalRegistriesIgn, originalPolicyIgn, err := generateOriginalContainerRuntimeConfigs(templateDir, controllerConfig, role, featureGateAccess) if err != nil { - return nil, fmt.Errorf("could not generate original ContainerRuntime Configs: %w", err) + return nil, nil, fmt.Errorf("could not generate original ContainerRuntime Configs: %w", err) } if insecureRegs != nil || registriesBlocked != nil || len(icspRules) != 0 || len(idmsRules) != 0 || len(itmsRules) != 0 { if originalRegistriesIgn.Contents.Source == nil { - return nil, fmt.Errorf("original registries config is empty") + return nil, nil, fmt.Errorf("original registries config is empty") } contents, err := ctrlcommon.DecodeIgnitionFileContents(originalRegistriesIgn.Contents.Source, originalRegistriesIgn.Contents.Compression) if err != nil { - return nil, fmt.Errorf("could not decode original registries config: %w", err) + return nil, nil, fmt.Errorf("could not decode original registries config: %w", err) } registriesTOML, err = updateRegistriesConfig(contents, insecureRegs, registriesBlocked, icspRules, idmsRules, itmsRules) if err != nil { - return nil, fmt.Errorf("could not update registries config with new changes: %w", err) + return nil, nil, fmt.Errorf("could not update registries config with new changes: %w", err) } } - if policyBlocked != nil || allowedRegs != nil { + if policyBlocked != nil || allowedRegs != nil || len(scopeNamespacePolicies) > 0 || len(clusterScopePolicies) > 0 { if originalPolicyIgn.Contents.Source == nil { - return nil, fmt.Errorf("original policy json is empty") + return nil, nil, fmt.Errorf("original policy json is empty") } contents, err := ctrlcommon.DecodeIgnitionFileContents(originalPolicyIgn.Contents.Source, originalPolicyIgn.Contents.Compression) if err != nil { - return nil, fmt.Errorf("could not decode original policy json: %w", err) + return nil, nil, fmt.Errorf("could not decode original policy json: %w", err) + } + policyJSON, err = updatePolicyJSON(contents, policyBlocked, allowedRegs, releaseImage, clusterScopePolicies) + if err != nil { + return nil, nil, fmt.Errorf("could not update policy json with new changes: %w", err) + } + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + // Merge the cluster override namespacedJSONBase and namespace policy + namespacedJSONBase = append(namespacedJSONBase, policyJSON...) + namespacedPolicyJSONs, err = generateNamespacedPolicyJSON(namespacedJSONBase, policies) + if err != nil { + return nil, nil, fmt.Errorf("could not generate namespace policy JSON from imagepolicy: %w", err) } - policyJSON, err = updatePolicyJSON(contents, policyBlocked, allowedRegs, releaseImage) + // generates configuration under /etc/containers/registries.d to enable sigstore verification + sigstoreRegistriesConfigYaml, err = generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) if err != nil { - return nil, fmt.Errorf("could not update policy json with new changes: %w", err) + return nil, nil, err } } + + generatedImagePolicyConfigFileList := imagePolicyConfigFileList(namespacedPolicyJSONs, sigstoreRegistriesConfigYaml) + if len(generatedImagePolicyConfigFileList) != 0 { + namespacedPoliciesIgn := createNewIgnition(generatedImagePolicyConfigFileList) + imagePolicyIgn = &namespacedPoliciesIgn + } + generatedConfigFileList := []generatedConfigFile{ {filePath: registriesConfigPath, data: registriesTOML}, {filePath: policyConfigPath, data: policyJSON}, @@ -943,19 +1127,156 @@ func registriesConfigIgnition(templateDir string, controllerConfig *mcfgv1.Contr } registriesIgn := createNewIgnition(generatedConfigFileList) - return ®istriesIgn, nil + return ®istriesIgn, imagePolicyIgn, nil +} + +// getValidScopeNamespacePolicies returns a map[scope][namespace]policyRequirement from ImagePolicy CRs, a map[scope]policyRequirement from ClusterImagePolicy +// not add scope to the map if its release image repo, and syncs to status +func getValidScopeNamespacePolicies(clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, releaseImage string, ctrl *Controller) (map[string]signature.PolicyRequirements, map[string]map[string]signature.PolicyRequirements, error) { + namespacePolices := make(map[string]map[string]signature.PolicyRequirements) + clusterScopePolicies := make(map[string]signature.PolicyRequirements) + + // Get the repository being used by the payload from the releaseImage + ref, err := getPayloadRepo(releaseImage) + if err != nil { + return nil, nil, errParsingReference + } + payloadRepo := ref.Name() + + for _, clusterImagePolicy := range clusterImagePolicies { + sigstoreSignedPolicyItem, err := policyItemFromSpec(clusterImagePolicy.Spec.Policy) + if err != nil { + return nil, nil, err + } + var payloadScopes []string + for _, scope := range clusterImagePolicy.Spec.Scopes { + scopeStr := string(scope) + if runtimeutils.ScopeIsNestedInsideScope(payloadRepo, scopeStr) { + payloadScopes = append(payloadScopes, scopeStr) + continue + } + clusterScopePolicies[scopeStr] = append(clusterScopePolicies[scopeStr], sigstoreSignedPolicyItem) + } + if ctrl != nil && len(payloadScopes) > 0 { + msg := fmt.Sprintf("has conflict scope(s) %q of registry of Openshift payload repository %s, skip the scopes", payloadScopes, payloadRepo) + klog.V(2).Info(msg) + ctrl.syncClusterImagePolicyStatusOnly(clusterImagePolicy.ObjectMeta.Name, apicfgv1alpha1.ImagePolicyPending, reasonConflictScopes, msg, metav1.ConditionTrue) + } + } + + for _, imagePolicy := range imagePolicies { + namespace := imagePolicy.ObjectMeta.Namespace + sigstoreSignedPolicyItem, err := policyItemFromSpec(imagePolicy.Spec.Policy) + if err != nil { + return nil, nil, err + } + var ( + conflictScopes []string + payloadScopes []string + conditionMsg []string + ) + for _, scope := range imagePolicy.Spec.Scopes { + scopeStr := string(scope) + if runtimeutils.ScopeIsNestedInsideScope(payloadRepo, scopeStr) { + payloadScopes = append(payloadScopes, scopeStr) + continue + } + nestedInClusterScope := false + for clusterScope := range clusterScopePolicies { + if runtimeutils.ScopeIsNestedInsideScope(scopeStr, clusterScope) { + conflictScopes = append(conflictScopes, scopeStr) + nestedInClusterScope = true + break + } + } + if nestedInClusterScope { + continue + } + if _, ok := namespacePolices[scopeStr]; !ok { + namespacePolices[scopeStr] = make(map[string]signature.PolicyRequirements) + } + namespacePolices[scopeStr][namespace] = append(namespacePolices[scopeStr][namespace], sigstoreSignedPolicyItem) + } + if ctrl != nil { + if len(conflictScopes) > 0 { + conditionMsg = append(conditionMsg, fmt.Sprintf("has conflict scope(s) %q equal to or nest inside existing clusterimagepolicy, only policy from clusterimagepolicy will be applied", conflictScopes)) + } + if len(payloadScopes) > 0 { + conditionMsg = append(conditionMsg, fmt.Sprintf("has conflict scope(s) %q of registry of Openshift payload repository %s, skip the scopes", payloadScopes, payloadRepo)) + } + if len(conditionMsg) > 0 { + msg := strings.Join(conditionMsg, "; ") + klog.V(2).Info(msg) + ctrl.syncImagePolicyStatusOnly(namespace, imagePolicy.ObjectMeta.Name, apicfgv1alpha1.ImagePolicyPending, reasonConflictScopes, msg, metav1.ConditionTrue) + } + + } + } + return clusterScopePolicies, namespacePolices, nil +} + +func (ctrl *Controller) syncImagePolicyStatusOnly(namespace, imagepolicy, conditionType, reason, msg string, status metav1.ConditionStatus) { + statusUpdateErr := retry.RetryOnConflict(updateBackoff, func() error { + newImagePolicy, getErr := ctrl.configClient.ConfigV1alpha1().ImagePolicies(namespace).Get(context.TODO(), imagepolicy, metav1.GetOptions{}) + if getErr != nil { + return getErr + } + + newCondition := apihelpers.NewCondition(conditionType, status, reason, msg) + if newImagePolicy.GetGeneration() != newCondition.ObservedGeneration { + newCondition.ObservedGeneration = newImagePolicy.GetGeneration() + } + newImagePolicy.Status.Conditions = []metav1.Condition{*newCondition} + _, updateErr := ctrl.configClient.ConfigV1alpha1().ImagePolicies(namespace).UpdateStatus(context.TODO(), newImagePolicy, metav1.UpdateOptions{}) + return updateErr + }) + if statusUpdateErr != nil { + klog.Warningf("error updating imagepolicy status: %v", statusUpdateErr) + } +} + +func (ctrl *Controller) syncClusterImagePolicyStatusOnly(imagepolicy, conditionType, reason, msg string, status metav1.ConditionStatus) { + statusUpdateErr := retry.RetryOnConflict(updateBackoff, func() error { + newClusterImagePolicy, getErr := ctrl.configClient.ConfigV1alpha1().ClusterImagePolicies().Get(context.TODO(), imagepolicy, metav1.GetOptions{}) + if getErr != nil { + return getErr + } + newCondition := apihelpers.NewCondition(conditionType, status, reason, msg) + if newClusterImagePolicy.GetGeneration() != newCondition.ObservedGeneration { + newCondition.ObservedGeneration = newClusterImagePolicy.GetGeneration() + } + newClusterImagePolicy.Status.Conditions = []metav1.Condition{*newCondition} + _, updateErr := ctrl.configClient.ConfigV1alpha1().ClusterImagePolicies().UpdateStatus(context.TODO(), newClusterImagePolicy, metav1.UpdateOptions{}) + return updateErr + }) + if statusUpdateErr != nil { + klog.Warningf("error updating clusterimagepolicy status: %v", statusUpdateErr) + } } // RunImageBootstrap generates MachineConfig objects for mcpPools that would have been generated by syncImageConfig, // except that mcfgv1.Image is not available. func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerConfig, mcpPools []*mcfgv1.MachineConfigPool, icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, - idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, imgCfg *apicfgv1.Image, featureGateAccess featuregates.FeatureGateAccess) ([]*mcfgv1.MachineConfig, error) { + idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, imgCfg *apicfgv1.Image, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, featureGateAccess featuregates.FeatureGateAccess) ([]*mcfgv1.MachineConfig, error) { var ( insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs []string err error ) + clusterScopePolicies := map[string]signature.PolicyRequirements{} + scopeNamespacePolicies := map[string]map[string]signature.PolicyRequirements{} + featureGates, err := featureGateAccess.CurrentFeatureGates() + if err != nil { + return nil, err + } + sigstoreAPIEnabled := featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification) + if sigstoreAPIEnabled { + if clusterScopePolicies, scopeNamespacePolicies, err = getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, controllerConfig.Spec.ReleaseImage, nil); err != nil { + return nil, err + } + } + // Read the search, insecure, blocked, and allowed registries from the cluster-wide Image CR if it is not nil if imgCfg != nil { insecureRegs = imgCfg.Spec.RegistrySources.InsecureRegistries @@ -976,8 +1297,9 @@ func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerCo if err != nil { return nil, err } - registriesIgn, err := registriesConfigIgnition(templateDir, controllerConfig, role, controllerConfig.Spec.ReleaseImage, - insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs, icspRules, idmsRules, itmsRules, featureGateAccess) + managedKeyNamespaceImagePolicy := getManagedKeyNamespacedImagePolicy(pool) + registriesIgn, namespacedPoliciesIgn, err := registriesConfigIgnition(templateDir, controllerConfig, role, controllerConfig.Spec.ReleaseImage, + insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs, icspRules, idmsRules, itmsRules, featureGateAccess, clusterScopePolicies, scopeNamespacePolicies) if err != nil { return nil, err } @@ -995,6 +1317,13 @@ func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerCo }, } res = append(res, mc) + if namespacedPoliciesIgn != nil { + namespacedPoliciesMC, err := ctrlcommon.MachineConfigFromIgnConfig(role, managedKeyNamespaceImagePolicy, namespacedPoliciesIgn) + if err != nil { + return nil, err + } + res = append(res, namespacedPoliciesMC) + } } return res, nil } 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 8719ec4130..bfa96dab7c 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 @@ -3,7 +3,9 @@ package containerruntimeconfig import ( "context" "fmt" + "path/filepath" "reflect" + "strings" "testing" "time" @@ -28,6 +30,7 @@ import ( ign3types "github.com/coreos/ignition/v2/config/v3_4/types" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" fakeconfigv1client "github.com/openshift/client-go/config/clientset/versioned/fake" @@ -62,14 +65,16 @@ type fixture struct { imgClient *fakeconfigv1client.Clientset operatorClient *fakeoperatorclient.Clientset - ccLister []*mcfgv1.ControllerConfig - mcpLister []*mcfgv1.MachineConfigPool - mccrLister []*mcfgv1.ContainerRuntimeConfig - imgLister []*apicfgv1.Image - cvLister []*apicfgv1.ClusterVersion - icspLister []*apioperatorsv1alpha1.ImageContentSourcePolicy - idmsLister []*apicfgv1.ImageDigestMirrorSet - itmsLister []*apicfgv1.ImageTagMirrorSet + ccLister []*mcfgv1.ControllerConfig + mcpLister []*mcfgv1.MachineConfigPool + mccrLister []*mcfgv1.ContainerRuntimeConfig + imgLister []*apicfgv1.Image + cvLister []*apicfgv1.ClusterVersion + icspLister []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsLister []*apicfgv1.ImageDigestMirrorSet + itmsLister []*apicfgv1.ImageTagMirrorSet + clusterImagePolicyLister []*apicfgv1alpha1.ClusterImagePolicy + imagePolicyLister []*apicfgv1alpha1.ImagePolicy actions []core.Action skipActionsValidation bool @@ -86,7 +91,7 @@ func newFixture(t *testing.T) *fixture { f.t = t f.objects = []runtime.Object{} f.fgAccess = featuregates.NewHardcodedFeatureGateAccess( - []apicfgv1.FeatureGateName{}, + []apicfgv1.FeatureGateName{apicfgv1.FeatureGateSigstoreImageVerification}, []apicfgv1.FeatureGateName{ apicfgv1.FeatureGateExternalCloudProvider, apicfgv1.FeatureGateExternalCloudProviderAzure, @@ -210,6 +215,50 @@ func newClusterVersionConfig(name, desiredImage string) *apicfgv1.ClusterVersion } } +func newImagePolicyWithPublicKey(name, namespace string, scopes []string, keyData string) *apicfgv1alpha1.ImagePolicy { + imgScopes := []apicfgv1alpha1.ImageScope{} + for _, scope := range scopes { + imgScopes = append(imgScopes, apicfgv1alpha1.ImageScope(scope)) + } + return &apicfgv1alpha1.ImagePolicy{ + TypeMeta: metav1.TypeMeta{APIVersion: apicfgv1alpha1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: types.UID(utilrand.String(5)), Generation: 1}, + Spec: apicfgv1alpha1.ImagePolicySpec{ + Scopes: imgScopes, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: keyData, + }, + }, + }, + }, + } +} + +func newClusterImagePolicyWithPublicKey(name string, scopes []string, keyData string) *apicfgv1alpha1.ClusterImagePolicy { + imgScopes := []apicfgv1alpha1.ImageScope{} + for _, scope := range scopes { + imgScopes = append(imgScopes, apicfgv1alpha1.ImageScope(scope)) + } + return &apicfgv1alpha1.ClusterImagePolicy{ + TypeMeta: metav1.TypeMeta{APIVersion: apicfgv1alpha1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(utilrand.String(5)), Generation: 1}, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: imgScopes, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: keyData, + }, + }, + }, + }, + } +} + func (f *fixture) newController() *Controller { f.client = fake.NewSimpleClientset(f.objects...) f.imgClient = fakeconfigv1client.NewSimpleClientset(f.imgObjects...) @@ -225,6 +274,8 @@ func (f *fixture) newController() *Controller { ci.Config().V1().Images(), ci.Config().V1().ImageDigestMirrorSets(), ci.Config().V1().ImageTagMirrorSets(), + ci.Config().V1alpha1().ImagePolicies(), + ci.Config().V1alpha1().ClusterImagePolicies(), oi.Operator().V1alpha1().ImageContentSourcePolicies(), ci.Config().V1().ClusterVersions(), k8sfake.NewSimpleClientset(), f.client, f.imgClient, @@ -238,6 +289,8 @@ func (f *fixture) newController() *Controller { c.icspListerSynced = alwaysReady c.idmsListerSynced = alwaysReady c.itmsListerSynced = alwaysReady + c.clusterImagePolicyListerSynced = alwaysReady + c.imagePolicyListerSynced = alwaysReady c.clusterVersionListerSynced = alwaysReady c.eventRecorder = &record.FakeRecorder{} @@ -274,6 +327,12 @@ func (f *fixture) newController() *Controller { for _, c := range f.itmsLister { ci.Config().V1().ImageTagMirrorSets().Informer().GetIndexer().Add(c) } + for _, c := range f.clusterImagePolicyLister { + ci.Config().V1alpha1().ClusterImagePolicies().Informer().GetIndexer().Add(c) + } + for _, c := range f.imagePolicyLister { + ci.Config().V1alpha1().ImagePolicies().Informer().GetIndexer().Add(c) + } return c } @@ -404,7 +463,7 @@ func (f *fixture) expectUpdateContainerRuntimeConfigRoot(config *mcfgv1.Containe f.actions = append(f.actions, core.NewRootUpdateAction(schema.GroupVersionResource{Version: "v1", Group: "machineconfiguration.openshift.io", Resource: "containerruntimeconfigs"}, config)) } -func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcName string, imgcfg *apicfgv1.Image, icsp *apioperatorsv1alpha1.ImageContentSourcePolicy, idms *apicfgv1.ImageDigestMirrorSet, itms *apicfgv1.ImageTagMirrorSet, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin bool) { +func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcName string, imgcfg *apicfgv1.Image, icsp *apioperatorsv1alpha1.ImageContentSourcePolicy, idms *apicfgv1.ImageDigestMirrorSet, itms *apicfgv1.ImageTagMirrorSet, imagepolicy *apicfgv1alpha1.ClusterImagePolicy, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig bool) { icsps := []*apioperatorsv1alpha1.ImageContentSourcePolicy{} if icsp != nil { icsps = append(icsps, icsp) @@ -417,12 +476,75 @@ func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcNa if itms != nil { itmss = append(itmss, itms) } + clusterImagePolicies := []*apicfgv1alpha1.ClusterImagePolicy{} + if imagepolicy != nil { + clusterImagePolicies = append(clusterImagePolicies, imagepolicy) + } updatedMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), mcName, metav1.GetOptions{}) require.NoError(t, err) - verifyRegistriesConfigAndPolicyJSONContents(t, updatedMC, mcName, imgcfg, icsps, idmss, itmss, releaseImageReg, verifyPolicyJSON, verifySearchRegsDropin) + verifyRegistriesConfigAndPolicyJSONContents(t, updatedMC, mcName, imgcfg, icsps, idmss, itmss, clusterImagePolicies, releaseImageReg, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig) } -func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.MachineConfig, mcName string, imgcfg *apicfgv1.Image, icsps []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmss []*apicfgv1.ImageDigestMirrorSet, itmss []*apicfgv1.ImageTagMirrorSet, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin bool) { +func (f *fixture) verifySigstoreRegistriesConfdAndPolicyContents(t *testing.T, imgcfg *apicfgv1.Image, mcName, releaseImageReg string, clusterimagepolicy *apicfgv1alpha1.ClusterImagePolicy, imagepolicy *apicfgv1alpha1.ImagePolicy) { + clusterImagePolicies := []*apicfgv1alpha1.ClusterImagePolicy{} + if clusterimagepolicy != nil { + clusterImagePolicies = append(clusterImagePolicies, clusterimagepolicy) + } + imagePolicies := []*apicfgv1alpha1.ImagePolicy{} + if imagepolicy != nil { + imagePolicies = append(imagePolicies, imagepolicy) + + } + updatedMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), mcName, metav1.GetOptions{}) + require.NoError(t, err) + verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg, updatedMC, clusterImagePolicies, imagePolicies, releaseImageReg) +} + +func verifySigstoreRegistriesConfdAndPolicyContents(t *testing.T, imgcfg *apicfgv1.Image, mc *mcfgv1.MachineConfig, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, releaseImageReg string) { + _, policyBlocked, allowed, _ := getValidBlockedAndAllowedRegistries(releaseImageReg, &imgcfg.Spec, nil, nil) + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, releaseImageReg, nil) + require.NoError(t, err) + expectedRegistriesConfd, err := generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) + require.NoError(t, err) + ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw) + require.NoError(t, err) + var registriesConffile ign3types.File + for _, f := range ignCfg.Storage.Files { + if f.Node.Path == sigstoreRegistriesConfigFilePath { + registriesConffile = f + } + } + + require.Equal(t, sigstoreRegistriesConfigFilePath, registriesConffile.Node.Path) + registriesYaml, err := ctrlcommon.DecodeIgnitionFileContents(registriesConffile.Contents.Source, registriesConffile.Contents.Compression) + require.NoError(t, err) + assert.Equal(t, string(expectedRegistriesConfd), string(registriesYaml)) + + policyJSON, err := updatePolicyJSON(templatePolicyJSON, + policyBlocked, + allowed, releaseImageReg, clusterScopePolicies) + require.NoError(t, err) + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + // Merge the cluster override namespacedJSONBase and namespace policy + namespacedJSONBase := append([]byte{}, policyJSON...) + namespacedPolicyJSONs, err := generateNamespacedPolicyJSON(namespacedJSONBase, policies) + require.NoError(t, err) + require.Len(t, ignCfg.Storage.Files, len(namespacedPolicyJSONs)+1) + for _, policyf := range ignCfg.Storage.Files { + if strings.HasSuffix(policyf.Node.Path, "json") { + actualNamespace := strings.TrimSuffix(filepath.Base(policyf.Node.Path), ".json") + value, ok := namespacedPolicyJSONs[actualNamespace] + require.True(t, ok) + gotpolicyJSON, err := ctrlcommon.DecodeIgnitionFileContents(policyf.Contents.Source, policyf.Contents.Compression) + require.NoError(t, err) + assert.JSONEq(t, string(value), string(gotpolicyJSON), "policy.json for namespace %s", actualNamespace) + } + + } +} + +func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.MachineConfig, mcName string, imgcfg *apicfgv1.Image, icsps []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmss []*apicfgv1.ImageDigestMirrorSet, itmss []*apicfgv1.ImageTagMirrorSet, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig bool) { // This is not testing updateRegistriesConfig, which has its own tests; this verifies the created object contains the expected // configuration file. // First get the valid blocked registries to ensure we don't block the registry where the release image is from @@ -453,12 +575,15 @@ func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.Machin require.NoError(t, err) assert.Equal(t, string(expectedRegistriesConf), string(registriesConf)) + clusterScopePolicies, _, err := getValidScopeNamespacePolicies(clusterImagePolicies, nil, releaseImageReg, nil) + require.NoError(t, err) + // Validate the policy.json contents if a change is expected from the tests if verifyPolicyJSON { allowed = append(allowed, imgcfg.Spec.RegistrySources.AllowedRegistries...) expectedPolicyJSON, err := updatePolicyJSON(templatePolicyJSON, policyBlocked, - allowed, releaseImageReg) + allowed, releaseImageReg, clusterScopePolicies) require.NoError(t, err) policyfile := ignCfg.Storage.Files[1] if policyfile.Node.Path != policyConfigPath { @@ -482,6 +607,7 @@ func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.Machin require.NoError(t, err) assert.Equal(t, string(expectedSearchRegsConf[0].data), string(searchRegsConf)) } + } // The patch bytes to expect when creating/updating a containerruntimeconfig @@ -647,7 +773,7 @@ func TestImageConfigCreate(t *testing.T) { f.run("cluster") for _, mcName := range []string{mcs1.Name, mcs2.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } }) } @@ -702,7 +828,7 @@ func TestImageConfigUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } // Perform Update @@ -743,7 +869,7 @@ func TestImageConfigUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfgUpdate, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfgUpdate, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } }) } @@ -803,7 +929,7 @@ func TestICSPUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icsp, nil, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icsp, nil, nil, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -848,7 +974,7 @@ func TestICSPUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icspUpdate, nil, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icspUpdate, nil, nil, nil, cc.Spec.ReleaseImage, false, false, false) } }) } @@ -906,7 +1032,7 @@ func TestIDMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idms, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idms, nil, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -951,7 +1077,7 @@ func TestIDMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idmsUpdate, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idmsUpdate, nil, nil, cc.Spec.ReleaseImage, false, false, false) } }) } @@ -1009,7 +1135,7 @@ func TestITMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itms, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itms, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -1054,18 +1180,20 @@ func TestITMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itmsUpdate, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itmsUpdate, nil, cc.Spec.ReleaseImage, false, false, false) } }) } } func TestRunImageBootstrap(t *testing.T) { + testClusterImagePolicy := testClusterImagePolicyCRs["test-cr0"] for _, platform := range []apicfgv1.PlatformType{apicfgv1.AWSPlatformType, apicfgv1.NonePlatformType, "unrecognized"} { for _, tc := range []struct { - icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy - idmsRules []*apicfgv1.ImageDigestMirrorSet - itmsRules []*apicfgv1.ImageTagMirrorSet + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsRules []*apicfgv1.ImageDigestMirrorSet + itmsRules []*apicfgv1.ImageTagMirrorSet + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy }{ { idmsRules: []*apicfgv1.ImageDigestMirrorSet{ @@ -1092,6 +1220,11 @@ func TestRunImageBootstrap(t *testing.T) { }), }, }, + { + clusterImagePolicies: []*apicfgv1alpha1.ClusterImagePolicy{ + &testClusterImagePolicy, + }, + }, } { t.Run(string(platform), func(t *testing.T) { @@ -1103,16 +1236,26 @@ func TestRunImageBootstrap(t *testing.T) { // Adding the release-image registry "release-reg.io" to the list of blocked registries to ensure that is it not added to // both registries.conf and policy.json as blocked imgCfg := newImageConfig("cluster", &apicfgv1.RegistrySources{InsecureRegistries: []string{"insecure-reg-1.io", "insecure-reg-2.io"}, BlockedRegistries: []string{"blocked-reg.io", "release-reg.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + // set FeatureGateSigstoreImageVerification enabled for testing + fgAccess := featuregates.NewHardcodedFeatureGateAccess([]apicfgv1.FeatureGateName{apicfgv1.FeatureGateSigstoreImageVerification}, []apicfgv1.FeatureGateName{}) - fgAccess := featuregates.NewHardcodedFeatureGateAccess([]apicfgv1.FeatureGateName{}, []apicfgv1.FeatureGateName{}) - - mcs, err := RunImageBootstrap("../../../templates", cc, pools, tc.icspRules, tc.idmsRules, tc.itmsRules, imgCfg, fgAccess) + mcs, err := RunImageBootstrap("../../../templates", cc, pools, tc.icspRules, tc.idmsRules, tc.itmsRules, imgCfg, tc.clusterImagePolicies, nil, fgAccess) require.NoError(t, err) - require.Len(t, mcs, len(pools)) + if tc.clusterImagePolicies != nil { + require.Len(t, mcs, len(pools)*2) + } else { + require.Len(t, mcs, len(pools)) + } for i := range pools { keyReg, _ := getManagedKeyReg(pools[i], nil) - verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, cc.Spec.ReleaseImage, true, true) + if mcs[i].Name == keyReg { + verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, tc.clusterImagePolicies, cc.Spec.ReleaseImage, true, true, false) + } + keyImagePolicy := getManagedKeyNamespacedImagePolicy(pools[i]) + if mcs[i].Name == keyImagePolicy { + verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, tc.clusterImagePolicies, cc.Spec.ReleaseImage, false, false, true) + } } }) } @@ -1664,3 +1807,112 @@ func TestCleanUpDuplicatedMC(t *testing.T) { }) } } + +func TestClusterImagePolicyCreate(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{"allow.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + + cvcfg1 := newClusterVersionConfig("version", "test.io/myuser/myimage:test") + keyReg1, _ := getManagedKeyReg(mcp, nil) + keyReg2, _ := getManagedKeyReg(mcp2, nil) + clusterimgPolicyKey1 := getManagedKeyNamespacedImagePolicy(mcp) + clusterimgPolicyKey2 := getManagedKeyNamespacedImagePolicy(mcp) + clusterimgPolicyMCs1 := helpers.NewMachineConfig(clusterimgPolicyKey1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + clusterimgPolicyMCs2 := helpers.NewMachineConfig(clusterimgPolicyKey2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + + 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{{}}) + clusterimgPolicy := newClusterImagePolicyWithPublicKey("image-policy", []string{"example.com"}, "Zm9vIGJhcg==") + 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.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(clusterimgPolicyMCs1) + f.expectCreateMachineConfigAction(clusterimgPolicyMCs1) + + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + + f.expectCreateMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(clusterimgPolicyMCs2) + f.expectCreateMachineConfigAction(clusterimgPolicyMCs2) + + f.run("") + + for _, mcName := range []string{mcs1.Name, mcs2.Name} { + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, clusterimgPolicy, cc.Spec.ReleaseImage, true, true, false) + } + for _, mcName := range []string{clusterimgPolicyKey1, clusterimgPolicyKey2} { + f.verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg1, mcName, cc.Spec.ReleaseImage, clusterimgPolicy, nil) + } + }) + } +} + +func TestImagePolicyCreate(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{"allow.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + + cvcfg1 := newClusterVersionConfig("version", "test.io/myuser/myimage:test") + keyReg1, _ := getManagedKeyReg(mcp, nil) + keyReg2, _ := getManagedKeyReg(mcp2, nil) + imgPolicyKey1 := getManagedKeyNamespacedImagePolicy(mcp) + imgPolicyKey2 := getManagedKeyNamespacedImagePolicy(mcp) + imgPolicyMCs1 := helpers.NewMachineConfig(imgPolicyKey1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + imgPolicyMCs2 := helpers.NewMachineConfig(imgPolicyKey2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + + 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{{}}) + imgPolicy := newImagePolicyWithPublicKey("image-policy", "testnamespace", []string{"example.com", "test.io"}, "Zm9vIGJhcg==") + 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.imagePolicyLister = append(f.imagePolicyLister, imgPolicy) + 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(imgPolicyMCs1) + f.expectCreateMachineConfigAction(imgPolicyMCs1) + + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + + f.expectCreateMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(imgPolicyMCs2) + f.expectCreateMachineConfigAction(imgPolicyMCs2) + + f.run("") + + for _, mcName := range []string{imgPolicyKey1, imgPolicyKey2} { + f.verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg1, mcName, cvcfg1.Status.Desired.Image, nil, imgPolicy) + } + }) + } +} diff --git a/pkg/controller/container-runtime-config/helpers.go b/pkg/controller/container-runtime-config/helpers.go index 1e302fcd44..4fc5d48919 100644 --- a/pkg/controller/container-runtime-config/helpers.go +++ b/pkg/controller/container-runtime-config/helpers.go @@ -7,11 +7,14 @@ import ( "errors" "fmt" "os" + "path/filepath" "reflect" "regexp" "strconv" "strings" + b64 "encoding/base64" + "github.com/BurntSushi/toml" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/pkg/sysregistriesv2" @@ -19,7 +22,9 @@ import ( "github.com/containers/image/v5/types" storageconfig "github.com/containers/storage/pkg/config" ign3types "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/ghodss/yaml" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/runtime-utils/pkg/registries" runtimeutils "github.com/openshift/runtime-utils/pkg/registries" @@ -48,14 +53,18 @@ const ( crioDropInFilePathPidsLimit = "/etc/crio/crio.conf.d/01-ctrcfg-pidsLimit" crioDropInFilePathLogSizeMax = "/etc/crio/crio.conf.d/01-ctrcfg-logSizeMax" CRIODropInFilePathDefaultRuntime = "/etc/crio/crio.conf.d/01-ctrcfg-defaultRuntime" + imagepolicyType = "sigstoreSigned" + sigstoreRegistriesConfigFilePath = "/etc/containers/registries.d/sigstore-registries.yaml" ) var ( - errParsingReference = errors.New("error parsing reference of release image") // sourceRegex and mirrorRegex pattern should stay the same with https://github.com/openshift/api/blob/ef62af078a9387e739abd99ec1d80e9129bb5475/config/v1/types_image_digest_mirror_set.go // Validation the source and mirror format for IDMS/ITMS already exists in the CRD. We need to keep this regex validation for ICSP - sourceRegex = regexp.MustCompile(`^\*(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$|^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) - mirrorRegex = regexp.MustCompile(`^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + sourceRegex = regexp.MustCompile(`^\*(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$|^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + mirrorRegex = regexp.MustCompile(`^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + errParsingReference = errors.New("error parsing reference of release image") + namespacedPolicyFilePathFormat = filepath.FromSlash("/etc/crio/policies/%s.json") + reasonConflictScopes = "ConflictScopes" ) // TOML-friendly explicit tables used for conversions. @@ -116,6 +125,13 @@ type tomlConfigCRIODefaultRuntime struct { } `toml:"crio"` } +type DockerConfig struct { + UseSigstoreAttachments bool `json:"use-sigstore-attachments,omitempty"` +} +type RegistriesConfig struct { + Docker map[string]DockerConfig `json:"docker,omitempty"` +} + // generatedConfigFile is a struct that holds the filepath and data of the various configs // Using a struct array ensures that the order of the ignition files always stay the same // ensuring that double MCs are not created due to a change in the order @@ -130,6 +146,9 @@ type updateConfigFunc func(data []byte, internal *mcfgv1.ContainerRuntimeConfigu // new data in the form of a byte array. The function returns the ignition config with the // updated data. func createNewIgnition(configs []generatedConfigFile) ign3types.Config { + if len(configs) == 0 { + return ign3types.Config{} + } tempIgnConfig := ctrlcommon.NewIgnConfig() // Create ignitions for _, ignConf := range configs { @@ -329,6 +348,10 @@ func getManagedKeyReg(pool *mcfgv1.MachineConfigPool, client mcfgclientset.Inter return ctrlcommon.GetManagedKey(pool, client, "99", "registries", getManagedKeyRegDeprecated(pool)) } +func getManagedKeyNamespacedImagePolicy(pool *mcfgv1.MachineConfigPool) string { + return fmt.Sprintf("99-%s-generated-imagepolicies", pool.Name) +} + func wrapErrorWithCondition(err error, args ...interface{}) mcfgv1.ContainerRuntimeConfigCondition { var condition *mcfgv1.ContainerRuntimeConfigCondition if err != nil { @@ -480,7 +503,7 @@ func updateRegistriesConfig(data []byte, internalInsecure, internalBlocked []str // It also returns an error if both allowed and blocked registries are set // WARNING: This can not safely edit policy files with arbitrary complexity, especially files which include signedBy // requirements. It expects the input policy to be generated by templates in this project. -func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, releaseImage string) ([]byte, error) { +func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, releaseImage string, clusterScopePolicies map[string]signature.PolicyRequirements) ([]byte, error) { if len(internalAllowed) != 0 && len(internalBlocked) != 0 { payloadRepo, err := getPayloadRepo(releaseImage) if err != nil { @@ -492,10 +515,14 @@ func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, re return nil, fmt.Errorf("invalid images config: only one of AllowedRegistries or BlockedRegistries may be specified") } } + + if err := validateClusterImagePolicyWithAllowedBlockedRegistries(clusterScopePolicies, internalAllowed, internalBlocked); err != nil { + return nil, err + } // Return original data if neither allowed or blocked registries are configured // Note: this is just for testing, the controller does not call this function till // either allowed or blocked registries are configured - if internalAllowed == nil && internalBlocked == nil { + if internalAllowed == nil && internalBlocked == nil && len(clusterScopePolicies) == 0 { return data, nil } @@ -530,18 +557,21 @@ func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, re } } } - - policyObj.Transports["docker"] = transportScopes - // The “atomic” policy is the same as the “docker” policy, but “atomic” does not support three or more - // scope segments, so filter those scopes out. - policyObj.Transports["atomic"] = make(signature.PolicyTransportScopes) - for reg, config := range transportScopes { - if strings.Count(reg, "/") >= 3 { - continue + if len(transportScopes) > 0 { + policyObj.Transports["docker"] = transportScopes + // The “atomic” policy is the same as the “docker” policy, but “atomic” does not support three or more + // scope segments, so filter those scopes out. + policyObj.Transports["atomic"] = make(signature.PolicyTransportScopes) + for reg, config := range transportScopes { + if strings.Count(reg, "/") >= 3 { + continue + } + policyObj.Transports["atomic"][reg] = config } - policyObj.Transports["atomic"][reg] = config } + updatePolicyObjWithScopeRequirements(policyObj, clusterScopePolicies) + policyJSON, err := json.Marshal(policyObj) if err != nil { return nil, err @@ -812,3 +842,214 @@ func convertICSPToIDMS(icsp *apioperatorsv1alpha1.ImageContentSourcePolicy) *api }, } } + +func ownerReferenceForImageConfig(imageConfig *apicfgv1.Image) metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: apicfgv1.SchemeGroupVersion.String(), + Kind: "Image", + Name: imageConfig.Name, + UID: imageConfig.UID, + } +} + +func ownerReferenceForImagePolicy(imagePolicies []*apicfgv1alpha1.ImagePolicy, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy) metav1.OwnerReference { + if len(imagePolicies) > 0 { + return metav1.OwnerReference{ + APIVersion: apicfgv1alpha1.SchemeGroupVersion.String(), + Kind: "ImagePolicy", + Name: imagePolicies[0].Name, + UID: imagePolicies[0].UID, + } + } + if len(clusterImagePolicies) > 0 { + return metav1.OwnerReference{ + APIVersion: apicfgv1alpha1.SchemeGroupVersion.String(), + Kind: "ClusterImagePolicy", + Name: clusterImagePolicies[0].Name, + UID: clusterImagePolicies[0].UID, + } + } + return metav1.OwnerReference{} +} + +func policyItemFromSpec(policy apicfgv1alpha1.Policy) (signature.PolicyRequirement, error) { + var ( + sigstorePolicyRequirement signature.PolicyRequirement + signedIdentity signature.PolicyReferenceMatch + signedOptions []signature.PRSigstoreSignedOption + err error + ) + switch policy.RootOfTrust.PolicyType { + case apicfgv1alpha1.PublicKeyRootOfTrust: + keyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.PublicKey.KeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode public key: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithKeyData(keyDataDec)) + if policy.RootOfTrust.PublicKey.RekorKeyData != "" { + rekorKeyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.PublicKey.RekorKeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode rekorKeyData: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithRekorPublicKeyData(rekorKeyDataDec)) + } + case apicfgv1alpha1.FulcioCAWithRekorRootOfTrust: + fulcioOptions := []signature.PRSigstoreSignedFulcioOption{} + fulcioDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.FulcioCAWithRekor.FulcioCAData) + if err != nil { + return nil, fmt.Errorf("failed to decode fulcioCAData: %v", err) + } + fulcioOptions = append(fulcioOptions, signature.PRSigstoreSignedFulcioWithCAData(fulcioDataDec), + signature.PRSigstoreSignedFulcioWithOIDCIssuer(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.OIDCIssuer), + signature.PRSigstoreSignedFulcioWithSubjectEmail(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.SignedEmail)) + + prSigstoreSignedFulcio, err := signature.NewPRSigstoreSignedFulcio(fulcioOptions...) + if err != nil { + return nil, err + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithFulcio(prSigstoreSignedFulcio)) + + rekorKeyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.FulcioCAWithRekor.RekorKeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode rekorKeyData: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithRekorPublicKeyData(rekorKeyDataDec)) + } + + switch policy.SignedIdentity.MatchPolicy { + case apicfgv1alpha1.IdentityMatchPolicyRemapIdentity: + identity, err := signature.NewPRMRemapIdentity(string(policy.SignedIdentity.PolicyMatchRemapIdentity.Prefix), string(policy.SignedIdentity.PolicyMatchRemapIdentity.SignedPrefix)) + if err != nil { + return nil, fmt.Errorf("error getting signedIdentity for %s: %v", apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, err) + } + signedIdentity = identity + case apicfgv1alpha1.IdentityMatchPolicyExactRepository: + identity, err := signature.NewPRMExactRepository(string(policy.SignedIdentity.PolicyMatchExactRepository.Repository)) + if err != nil { + return nil, fmt.Errorf("error getting signedIdentity for %s: %v", apicfgv1alpha1.IdentityMatchPolicyExactRepository, err) + } + signedIdentity = identity + case apicfgv1alpha1.IdentityMatchPolicyMatchRepository: + signedIdentity = signature.NewPRMMatchRepository() + case apicfgv1alpha1.IdentityMatchPolicyMatchRepoDigestOrExact, "": + signedIdentity = signature.NewPRMMatchRepoDigestOrExact() + default: + return nil, fmt.Errorf("unknown signedIdentity match policy: %s", policy.SignedIdentity.MatchPolicy) + } + + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithSignedIdentity(signedIdentity)) + + if sigstorePolicyRequirement, err = signature.NewPRSigstoreSigned(signedOptions...); err != nil { + return nil, err + } + + return sigstorePolicyRequirement, nil +} + +func validateClusterImagePolicyWithAllowedBlockedRegistries(clusterScopePolicies map[string]signature.PolicyRequirements, allowedRegs, policyBlocked []string) error { + for _, reg := range allowedRegs { + if _, ok := clusterScopePolicies[reg]; ok { + return fmt.Errorf("allowedRegistries and clusterimagePolicy configured for the same scope %s ", reg) + } + } + + for _, reg := range policyBlocked { + if _, ok := clusterScopePolicies[reg]; ok { + return fmt.Errorf("blockedRegistries and clusterimagePolicy configured for the same scope %s ", reg) + } + } + return nil +} + +func generateSigstoreRegistriesdConfig(clusterScopePolicies map[string]signature.PolicyRequirements, + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) ([]byte, error) { + if len(clusterScopePolicies) == 0 && len(scopeNamespacePolicies) == 0 { + return nil, nil + } + + dockerConfig := make(map[string]DockerConfig) + sigstoreAttachment := DockerConfig{ + UseSigstoreAttachments: true, + } + for scope := range clusterScopePolicies { + dockerConfig[scope] = sigstoreAttachment + } + for scope := range scopeNamespacePolicies { + dockerConfig[scope] = sigstoreAttachment + } + + registriesConfig := &RegistriesConfig{} + registriesConfig.Docker = dockerConfig + data, err := yaml.Marshal(registriesConfig) + if err != nil { + return nil, fmt.Errorf("error marshalling regisres configuration for sigstore (cluster)imagepolicies: %w", err) + } + return data, nil +} + +// updatePolicyObjWithScopeRequirements adds signature requirements to docker transport of the non nil policyObj +func updatePolicyObjWithScopeRequirements(policyObj *signature.Policy, requirements map[string]signature.PolicyRequirements) { + if policyObj.Transports == nil { + policyObj.Transports = make(map[string]signature.PolicyTransportScopes) + } + if _, ok := policyObj.Transports["docker"]; !ok { + policyObj.Transports["docker"] = make(map[string]signature.PolicyRequirements) + } + for scope, requirement := range requirements { + policyObj.Transports["docker"][scope] = append(policyObj.Transports["docker"][scope], requirement...) + } +} + +// namespacePolicyRequirements collects imagepolicies into map[namespace]map[scope]policyRequirements +func namespaceScopePolicyRequirements(scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) map[string]map[string]signature.PolicyRequirements { + policies := make(map[string]map[string]signature.PolicyRequirements) + for scope, namespacePolicies := range scopeNamespacePolicies { + for namespace, requirements := range namespacePolicies { + if policies[namespace] == nil { + policies[namespace] = make(map[string]signature.PolicyRequirements) + } + policies[namespace][scope] = append(policies[namespace][scope], requirements...) + } + } + return policies +} + +func generateNamespacedPolicyJSON(namespaceJSONBase []byte, policies map[string]map[string]signature.PolicyRequirements) (map[string][]byte, error) { + namespaceJSONs := make(map[string][]byte) + decoder := json.NewDecoder(bytes.NewBuffer(namespaceJSONBase)) + + for namespace, requirements := range policies { + + policyObj := &signature.Policy{} + + err := decoder.Decode(policyObj) + if err != nil { + return nil, fmt.Errorf("error decoding policy json for namespaced policies: %w", err) + } + updatePolicyObjWithScopeRequirements(policyObj, requirements) + data, err := json.Marshal(policyObj) + if err != nil { + return nil, fmt.Errorf("error marshalling policy json for namespaced policies: %w", err) + } + namespaceJSONs[namespace] = data + } + return namespaceJSONs, nil +} + +func imagePolicyConfigFileList(namespaceJSONs map[string][]byte, sigstoreRegistriesConfigYaml []byte) []generatedConfigFile { + var namespacedPolicyConfigFileList []generatedConfigFile + for namespace, data := range namespaceJSONs { + namespacedPolicyFilePath := fmt.Sprintf(namespacedPolicyFilePathFormat, namespace) + namespacedPolicyConfigFileList = append(namespacedPolicyConfigFileList, generatedConfigFile{ + filePath: namespacedPolicyFilePath, + data: data, + }) + } + if len(sigstoreRegistriesConfigYaml) > 0 { + namespacedPolicyConfigFileList = append(namespacedPolicyConfigFileList, generatedConfigFile{ + filePath: sigstoreRegistriesConfigFilePath, + data: sigstoreRegistriesConfigYaml, + }) + } + return namespacedPolicyConfigFileList +} diff --git a/pkg/controller/container-runtime-config/helpers_test.go b/pkg/controller/container-runtime-config/helpers_test.go index 50434c03a5..f2cfbc229c 100644 --- a/pkg/controller/container-runtime-config/helpers_test.go +++ b/pkg/controller/container-runtime-config/helpers_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "os" "reflect" "testing" @@ -14,6 +15,7 @@ import ( "github.com/containers/image/v5/types" storageconfig "github.com/containers/storage/pkg/config" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/stretchr/testify/assert" @@ -463,7 +465,86 @@ func TestUpdateRegistriesConfig(t *testing.T) { } } +var testImagePolicyCRs = map[string]apicfgv1alpha1.ImagePolicy{ + "test-cr2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr2", + Namespace: "testnamespace", + }, + Spec: apicfgv1alpha1.ImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com", "test2.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: "dGVzdC1rZXktZGF0YQ==", + }, + }, + }, + }, + }, +} + +var testClusterImagePolicyCRs = map[string]apicfgv1alpha1.ClusterImagePolicy{ + "test-cr0": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr0", + }, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.FulcioCAWithRekorRootOfTrust, + FulcioCAWithRekor: &apicfgv1alpha1.FulcioCAWithRekor{ + FulcioCAData: "dGVzdC1jYS1kYXRhLWRhdGE=", + RekorKeyData: "dGVzdC1yZWtvci1rZXktZGF0YQ==", + FulcioSubject: apicfgv1alpha1.PolicyFulcioSubject{ + OIDCIssuer: "https://OIDC.example.com", + SignedEmail: "test-user@example.com", + }, + }, + }, + SignedIdentity: apicfgv1alpha1.PolicyIdentity{ + MatchPolicy: apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, + PolicyMatchRemapIdentity: &apicfgv1alpha1.PolicyMatchRemapIdentity{ + Prefix: "test-remap-prefix", + SignedPrefix: "test-remap-signed-prefix", + }, + }, + }, + }, + }, + "test-cr1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr1", + }, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com", "test1.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: "dGVzdC1rZXktZGF0YQ==", + RekorKeyData: "dGVzdC1yZWtvci1rZXktZGF0YQ==", + }, + }, + SignedIdentity: apicfgv1alpha1.PolicyIdentity{ + MatchPolicy: apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, + PolicyMatchRemapIdentity: &apicfgv1alpha1.PolicyMatchRemapIdentity{ + Prefix: "test-remap-prefix", + SignedPrefix: "test-remap-signed-prefix", + }, + }, + }, + }, + }, +} + func TestUpdatePolicyJSON(t *testing.T) { + testClusterImagePolicyCR := testClusterImagePolicyCRs["test-cr0"] + expectSigRequirement, policyerr := policyItemFromSpec(testClusterImagePolicyCR.Spec.Policy) + require.NoError(t, policyerr) + templateConfig := signature.Policy{ Default: signature.PolicyRequirements{signature.NewPRInsecureAcceptAnything()}, Transports: map[string]signature.PolicyTransportScopes{ @@ -480,6 +561,7 @@ func TestUpdatePolicyJSON(t *testing.T) { tests := []struct { name string allowed, blocked []string + imagepolicy *apicfgv1alpha1.ClusterImagePolicy errorExpected bool want signature.Policy }{ @@ -626,10 +708,41 @@ func TestUpdatePolicyJSON(t *testing.T) { }, errorExpected: false, }, + { + name: "update global policy with ImagePolicy CR", + allowed: []string{"allow.io"}, + imagepolicy: &testClusterImagePolicyCR, + want: signature.Policy{ + Default: signature.PolicyRequirements{signature.NewPRReject()}, + Transports: map[string]signature.PolicyTransportScopes{ + "atomic": map[string]signature.PolicyRequirements{ + "allow.io": {signature.NewPRInsecureAcceptAnything()}, + }, + "docker": map[string]signature.PolicyRequirements{ + "allow.io": {signature.NewPRInsecureAcceptAnything()}, + "test0.com": {expectSigRequirement}, + }, + "docker-daemon": map[string]signature.PolicyRequirements{ + "": {signature.NewPRInsecureAcceptAnything()}, + }, + }, + }, + errorExpected: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := updatePolicyJSON(templateBytes, tt.blocked, tt.allowed, "release-reg.io/image/release") + + var ( + clusterImagePolicies map[string]signature.PolicyRequirements + policyerr error + ) + + if tt.imagepolicy != nil { + clusterImagePolicies, _, policyerr = getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{tt.imagepolicy}, []*apicfgv1alpha1.ImagePolicy{}, "release-reg.io/image/release", nil) + require.NoError(t, policyerr) + } + got, err := updatePolicyJSON(templateBytes, tt.blocked, tt.allowed, "release-reg.io/image/release", clusterImagePolicies) if err == nil && tt.errorExpected { t.Errorf("updatePolicyJSON() error = %v", err) return @@ -1105,3 +1218,259 @@ func TestUpdateStorageConfig(t *testing.T) { } } } + +func TestGetValidScopeNamespacePolicies(t *testing.T) { + type testcase struct { + name string + clusterImagePolicyCRs []*apicfgv1alpha1.ClusterImagePolicy + imagePolicyCRs []*apicfgv1alpha1.ImagePolicy + releaseImg string + expectedScopePolicies map[string]signature.PolicyRequirements + expectedScopeNamespace map[string]map[string]signature.PolicyRequirements + errorExpected bool + } + + testCR0 := testClusterImagePolicyCRs["test-cr0"] + testCR1 := testClusterImagePolicyCRs["test-cr1"] + testCR2 := testImagePolicyCRs["test-cr2"] + policyreq0, err := policyItemFromSpec(testCR0.Spec.Policy) + require.NoError(t, err) + policyreq1, err := policyItemFromSpec(testCR1.Spec.Policy) + require.NoError(t, err) + policyreq2, err := policyItemFromSpec(testCR2.Spec.Policy) + require.NoError(t, err) + + tests := []testcase{ + { + name: "cluster and namespace CRs contains the same scope", + imagePolicyCRs: []*apicfgv1alpha1.ImagePolicy{&testCR2}, + clusterImagePolicyCRs: []*apicfgv1alpha1.ClusterImagePolicy{&testCR0, &testCR1}, + releaseImg: "release-reg.io/image/release", + expectedScopeNamespace: map[string]map[string]signature.PolicyRequirements{ + "test2.com": { + testCR2.ObjectMeta.Namespace: {policyreq2}, + }, + }, + expectedScopePolicies: map[string]signature.PolicyRequirements{ + "test0.com": {policyreq0, policyreq1}, + "test1.com": {policyreq1}, + }, + errorExpected: false, + }, + { + name: "clusterimagepolicy CRs has scope release image scope nested in", + imagePolicyCRs: []*apicfgv1alpha1.ImagePolicy{&testCR2}, + clusterImagePolicyCRs: []*apicfgv1alpha1.ClusterImagePolicy{&testCR0, &testCR1}, + releaseImg: "test1.com/image/release", + expectedScopeNamespace: map[string]map[string]signature.PolicyRequirements{ + "test2.com": { + testCR2.ObjectMeta.Namespace: {policyreq2}, + }, + }, + expectedScopePolicies: map[string]signature.PolicyRequirements{ + "test0.com": {policyreq0, policyreq1}, + }, + errorExpected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotScopePolicies, gotScopeNamespacePolicies, err := getValidScopeNamespacePolicies(test.clusterImagePolicyCRs, test.imagePolicyCRs, test.releaseImg, nil) + fmt.Println(err) + require.Equal(t, test.errorExpected, err != nil) + if !test.errorExpected { + require.Equal(t, test.expectedScopeNamespace, gotScopeNamespacePolicies) + require.Equal(t, test.expectedScopePolicies, gotScopePolicies) + } + }) + } +} + +func TestGenerateNamespacedPolicyJSON(t *testing.T) { + // Test empty namespacePolicies does not generate any policy + namespacePoliciesJsons, err := generateNamespacedPolicyJSON([]byte{}, make(map[string]map[string]signature.PolicyRequirements)) + require.NoError(t, err) + require.Equal(t, 0, len(namespacePoliciesJsons)) + + testImagePolicyCR0 := testClusterImagePolicyCRs["test-cr0"] + testImagePolicyCR1 := testClusterImagePolicyCRs["test-cr1"] + testImagePolicyCR2 := testImagePolicyCRs["test-cr2"] + + expectClusterPolicy := []byte(` + { + "default": [ + { + "type": "insecureAcceptAnything" + } + ], + "transports": { + "docker": { + "test0.com": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caData": "dGVzdC1jYS1kYXRhLWRhdGE=", + "oidcIssuer": "https://OIDC.example.com", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + }, + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test1.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ] + }, + "docker-daemon": { + "": [ + { + "type": "insecureAcceptAnything" + } + ] + } + } + } + `) + expectnamespacedPolicy := []byte(` + { + "default": [ + { + "type": "insecureAcceptAnything" + } + ], + "transports": { + "docker": { + "test0.com": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caData": "dGVzdC1jYS1kYXRhLWRhdGE=", + "oidcIssuer": "https://OIDC.example.com", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + }, + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test1.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test2.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ] + }, + "docker-daemon": { + "": [ + { + "type": "insecureAcceptAnything" + } + ] + } + } + } +`) + + expectRet := map[string][]byte{ + testImagePolicyCR2.ObjectMeta.Namespace: expectnamespacedPolicy, + } + + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testImagePolicyCR0, &testImagePolicyCR1}, []*apicfgv1alpha1.ImagePolicy{&testImagePolicyCR2}, "release-reg.io/image/release", nil) + require.NoError(t, err) + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + templateConfig := signature.Policy{ + Default: signature.PolicyRequirements{signature.NewPRInsecureAcceptAnything()}, + Transports: map[string]signature.PolicyTransportScopes{ + "docker-daemon": map[string]signature.PolicyRequirements{ + "": {signature.NewPRInsecureAcceptAnything()}, + }, + }, + } + buf := bytes.Buffer{} + err = json.NewEncoder(&buf).Encode(templateConfig) + require.NoError(t, err) + templateBytes := buf.Bytes() + + baseData, err := updatePolicyJSON(templateBytes, []string{}, []string{}, "release-reg.io/image/release", clusterScopePolicies) + require.NoError(t, err) + require.JSONEq(t, string(expectClusterPolicy), string(baseData)) + got, err := generateNamespacedPolicyJSON(baseData, policies) + require.NoError(t, err) + for namespace, v := range got { + require.JSONEq(t, string(expectRet[namespace]), string(v)) + } +} + +func TestGenerateSigstoreRegistriesConfig(t *testing.T) { + testImagePolicyCR0 := testClusterImagePolicyCRs["test-cr0"] + testImagePolicyCR1 := testClusterImagePolicyCRs["test-cr1"] + testImagePolicyCR2 := testImagePolicyCRs["test-cr2"] + + expectSigstoreRegistriesConfig := []byte( + `docker: + test0.com: + use-sigstore-attachments: true + test1.com: + use-sigstore-attachments: true + test2.com: + use-sigstore-attachments: true +`) + + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testImagePolicyCR0, &testImagePolicyCR1}, []*apicfgv1alpha1.ImagePolicy{&testImagePolicyCR2}, "release-reg.io/image/release", nil) + require.NoError(t, err) + got, err := generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) + require.NoError(t, err) + require.Equal(t, string(expectSigstoreRegistriesConfig), string(got)) +} diff --git a/pkg/daemon/constants/constants.go b/pkg/daemon/constants/constants.go index b77c1339de..aabea9707d 100644 --- a/pkg/daemon/constants/constants.go +++ b/pkg/daemon/constants/constants.go @@ -93,6 +93,12 @@ const ( // changes to registries.conf will cause a crio reload and require extra logic about whether to drain ContainerRegistryConfPath = "/etc/containers/registries.conf" + // changes to registries.d will cause a crio reload + SigstoreRegistriesConfigDir = "/etc/containers/registries.d" + + // changes to /etc/crio/policies will cause a crio reload + CrioPoliciesDir = "/etc/crio/policies" + // SSH Keys for user "core" will only be written at /home/core/.ssh CoreUserSSHPath = "/home/" + CoreUserName + "/.ssh" diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index 38433d608f..c8ae2baeeb 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -427,6 +427,10 @@ func calculatePostConfigChangeActionFromFileDiffs(diffFileSet []string) (actions filesPostConfigChangeActionRestartCrio := []string{ "/etc/pki/ca-trust/source/anchors/openshift-config-user-ca-bundle.crt", } + dirsPostConfigChangeActionReloadCrio := []string{ + constants.CrioPoliciesDir, + constants.SigstoreRegistriesConfigDir, + } actions = []string{postConfigChangeActionNone} for _, path := range diffFileSet { @@ -436,6 +440,8 @@ func calculatePostConfigChangeActionFromFileDiffs(diffFileSet []string) (actions actions = []string{postConfigChangeActionReloadCrio} } else if ctrlcommon.InSlice(path, filesPostConfigChangeActionRestartCrio) { actions = []string{postConfigChangeActionRestartCrio} + } else if ctrlcommon.InSlice(filepath.Dir(path), dirsPostConfigChangeActionReloadCrio) { + actions = []string{postConfigChangeActionReloadCrio} } else { actions = []string{postConfigChangeActionReboot} return diff --git a/test/e2e-bootstrap/bootstrap_test.go b/test/e2e-bootstrap/bootstrap_test.go index 11795b97fb..0cb9f2ca65 100644 --- a/test/e2e-bootstrap/bootstrap_test.go +++ b/test/e2e-bootstrap/bootstrap_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" configv1 "github.com/openshift/api/config/v1" + configv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" featuregatescontroller "github.com/openshift/cluster-config-operator/pkg/operator/featuregates" @@ -65,6 +66,7 @@ func TestE2EBootstrap(t *testing.T) { testEnv := framework.NewTestEnv(t) configv1.Install(scheme.Scheme) + configv1alpha1.Install(scheme.Scheme) mcfgv1.Install(scheme.Scheme) apioperatorsv1alpha1.Install(scheme.Scheme) @@ -486,6 +488,8 @@ func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controlle ctx.ConfigInformerFactory.Config().V1().Images(), ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(), ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(), + ctx.ConfigInformerFactory.Config().V1alpha1().ImagePolicies(), + ctx.ConfigInformerFactory.Config().V1alpha1().ClusterImagePolicies(), ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(), ctx.ConfigInformerFactory.Config().V1().ClusterVersions(), ctx.ClientBuilder.KubeClientOrDie("container-runtime-config-controller"), @@ -615,7 +619,7 @@ func loadBaseTestManifests(t *testing.T) []runtime.Object { func loadRawManifests(t *testing.T, rawObjs [][]byte) []runtime.Object { codecFactory := serializer.NewCodecFactory(scheme.Scheme) - decoder := codecFactory.UniversalDecoder(corev1GroupVersion, mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, configv1.GroupVersion) + decoder := codecFactory.UniversalDecoder(corev1GroupVersion, mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, configv1alpha1.GroupVersion, configv1.GroupVersion) objs := []runtime.Object{} for _, raw := range rawObjs { From a041bc8d2dfd2a68d6d9f3129386bca49a22fffa Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Sat, 10 Feb 2024 01:26:46 -0500 Subject: [PATCH 2/3] Update FulcioCAData type to []byte Signed-off-by: Qi Wang --- go.mod | 6 +- go.sum | 8 +- ...lusterimagepolicy-CustomNoUpgrade.crd.yaml | 592 +++++++++++------- ...rimagepolicy-TechPreviewNoUpgrade.crd.yaml | 592 +++++++++++------- ...or_01_imagepolicy-CustomNoUpgrade.crd.yaml | 592 +++++++++++------- ..._imagepolicy-TechPreviewNoUpgrade.crd.yaml | 592 +++++++++++------- .../container_runtime_config_controller.go | 2 +- .../container-runtime-config/helpers.go | 6 +- .../container-runtime-config/helpers_test.go | 2 +- ...onfig-operator_01_network-Default.crd.yaml | 75 +++ .../config/v1/stable.network.testsuite.yaml | 10 +- .../openshift/api/config/v1/types_network.go | 1 - ...lusterimagepolicy-CustomNoUpgrade.crd.yaml | 1 + ...rimagepolicy-TechPreviewNoUpgrade.crd.yaml | 1 + ...or_01_imagepolicy-CustomNoUpgrade.crd.yaml | 1 + ..._imagepolicy-TechPreviewNoUpgrade.crd.yaml | 1 + .../api/config/v1alpha1/types_image_policy.go | 2 +- .../config/v1alpha1/zz_generated.deepcopy.go | 7 +- ...0_90_cluster_csi_driver_01_config.crd.yaml | 1 + ...luster_csi_driver_01_config.crd.yaml-patch | 1 + .../operator/v1/types_csi_cluster_driver.go | 1 + .../config/v1alpha1/fulciocawithrekor.go | 14 +- .../applyconfigurations/internal/internal.go | 1 - .../applyconfigurations/internal/internal.go | 4 +- .../applyconfigurations/internal/internal.go | 16 +- .../operator/v1/ipsecconfig.go | 27 + .../operator/v1/ovnkubernetesconfig.go | 10 +- .../k8s.io/code-generator/generate-groups.sh | 0 .../generate-internal-groups.sh | 0 vendor/modules.txt | 6 +- 30 files changed, 1648 insertions(+), 924 deletions(-) create mode 100644 vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go mode change 100755 => 100644 vendor/k8s.io/code-generator/generate-groups.sh mode change 100755 => 100644 vendor/k8s.io/code-generator/generate-internal-groups.sh diff --git a/go.mod b/go.mod index caf9926b5c..d8d8149a92 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/google/renameio v0.1.0 github.com/imdario/mergo v0.3.13 github.com/opencontainers/go-digest v1.0.0 - github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 + github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe github.com/openshift/library-go v0.0.0-20231020125034-5a2d9fe760b3 @@ -330,3 +330,7 @@ require ( ) replace k8s.io/kube-openapi => github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 + +replace github.com/openshift/api => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f + +replace github.com/openshift/client-go => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 diff --git a/go.sum b/go.sum index 42820aab6e..c8f6e2e5fb 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,10 @@ github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/ github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f h1:V8tMeJUo24MoTX0Ac8Ud6y7AgcYJ53fk8av43ylbqN4= +github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= +github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 h1:/el4UT01Tg+5nnQaZSLzeZYBmVLhaFSDLqdp+lHFa9w= +github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765/go.mod h1:abbgYykRixaLLqDXiSdoMd8/sIm5bE1kfaMU1rGHJ7c= github.com/ajeddeloh/go-json v0.0.0-20170920214419-6a2fe990e083/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= github.com/ajeddeloh/go-json v0.0.0-20200220154158-5ae607161559 h1:4SPQljF/GJ8Q+QlCWMWxRBepub4DresnOm4eI2ebFGc= github.com/ajeddeloh/go-json v0.0.0-20200220154158-5ae607161559/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= @@ -689,10 +693,6 @@ github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQB github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 h1:OrjG0Lt0pXNYd6LUfGD3BYoBWXhfRMAMK0LYu8pdyqQ= -github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 h1:JQ/TO3bSDowReecFDvz+Nr0Fx8aoJI0zdo01y2W5NKk= -github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630/go.mod h1:8W4atsD8vBtEK0qplKpFWo+7XsQwzHTlqL7o/XpagRM= github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe h1:wDQtyIbJJIoif2Ux0S+9MJWIWEGV0oG+iLm8WtqwdSw= github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe/go.mod h1:SGUtv1pKZSzSVr2YCxXFvhE+LbGfI+vcetEhNicKayw= github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 h1:GPlAy197Jkr+D0T2FNWanamraTdzS/r9ZkT29lxvHaA= diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml index c12c81f8c1..79c1767755 100644 --- a/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml +++ b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml @@ -17,227 +17,379 @@ spec: singular: clusterimagepolicy scope: Cluster versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "ClusterImagePolicy holds cluster-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: spec contains the configuration for the cluster image policy. - type: object - required: - - policy - - scopes - properties: - policy: - description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. - type: object - required: - - rootOfTrust - properties: - rootOfTrust: - description: rootOfTrust specifies the root of trust for the policy. - type: object - required: - - policyType - properties: - fulcioCAWithRekor: - description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' - type: object - required: - - fulcioCAData - - fulcioSubject - - rekorKeyData - properties: - fulcioCAData: - description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. - type: string - maxLength: 8192 - fulcioSubject: - description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. - type: object - required: - - oidcIssuer - - signedEmail - properties: - oidcIssuer: - description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' - type: string - x-kubernetes-validations: - - rule: isURL(self) - message: oidcIssuer must be a valid URL - signedEmail: - description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' - type: string - x-kubernetes-validations: - - rule: self.matches('^\\S+@\\S+$') - message: invalid email address - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - policyType: - description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. - type: string - enum: - - PublicKey - - FulcioCAWithRekor - publicKey: - description: publicKey defines the root of trust based on a sigstore public key. - type: object - required: - - keyData - properties: - keyData: - description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. - type: string - maxLength: 8192 - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - x-kubernetes-validations: - - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' - message: publicKey is required when policyType is PublicKey, and forbidden otherwise - - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise - signedIdentity: - description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". - type: object - required: - - matchPolicy - properties: - exactRepository: - description: exactRepository is required if matchPolicy is set to "ExactRepository". - type: object - required: - - repository - properties: - repository: - description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - matchPolicy: - description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. - type: string - enum: - - MatchRepoDigestOrExact - - MatchRepository - - ExactRepository - - RemapIdentity - remapIdentity: - description: remapIdentity is required if matchPolicy is set to "RemapIdentity". - type: object - required: - - prefix - - signedPrefix - properties: - prefix: - description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - signedPrefix: - description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - x-kubernetes-validations: - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' - message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' - message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise - scopes: - description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' - type: array - maxItems: 256 - items: - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' - message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' - - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' - message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching - - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' - message: invalid repository namespace or image specification in the image scope - x-kubernetes-list-type: set - status: - description: status contains the observed state of the resource. - type: object - properties: - conditions: - description: conditions provide details on the status of this API Resource. - type: array - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - type: object - required: - - lastTransitionTime - - message - - reason - - status - - type + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image + signature verification \n Compatibility level 4: No compatibility is provided, + the API can change at any point for any reason. These capabilities should + not be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - type: string - format: date-time - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - type: string - maxLength: 32768 - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - type: integer - format: int64 - minimum: 0 - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - type: string - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - status: - description: status of the condition, one of True, False, Unknown. + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. enum: - - "True" - - "False" - - Unknown - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity type: string - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - served: true - storage: true - subresources: - status: {} + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml index 69b69e764b..538c44ace1 100644 --- a/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -17,227 +17,379 @@ spec: singular: clusterimagepolicy scope: Cluster versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "ClusterImagePolicy holds cluster-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: spec contains the configuration for the cluster image policy. - type: object - required: - - policy - - scopes - properties: - policy: - description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. - type: object - required: - - rootOfTrust - properties: - rootOfTrust: - description: rootOfTrust specifies the root of trust for the policy. - type: object - required: - - policyType - properties: - fulcioCAWithRekor: - description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' - type: object - required: - - fulcioCAData - - fulcioSubject - - rekorKeyData - properties: - fulcioCAData: - description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. - type: string - maxLength: 8192 - fulcioSubject: - description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. - type: object - required: - - oidcIssuer - - signedEmail - properties: - oidcIssuer: - description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' - type: string - x-kubernetes-validations: - - rule: isURL(self) - message: oidcIssuer must be a valid URL - signedEmail: - description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' - type: string - x-kubernetes-validations: - - rule: self.matches('^\\S+@\\S+$') - message: invalid email address - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - policyType: - description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. - type: string - enum: - - PublicKey - - FulcioCAWithRekor - publicKey: - description: publicKey defines the root of trust based on a sigstore public key. - type: object - required: - - keyData - properties: - keyData: - description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. - type: string - maxLength: 8192 - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - x-kubernetes-validations: - - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' - message: publicKey is required when policyType is PublicKey, and forbidden otherwise - - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise - signedIdentity: - description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". - type: object - required: - - matchPolicy - properties: - exactRepository: - description: exactRepository is required if matchPolicy is set to "ExactRepository". - type: object - required: - - repository - properties: - repository: - description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - matchPolicy: - description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. - type: string - enum: - - MatchRepoDigestOrExact - - MatchRepository - - ExactRepository - - RemapIdentity - remapIdentity: - description: remapIdentity is required if matchPolicy is set to "RemapIdentity". - type: object - required: - - prefix - - signedPrefix - properties: - prefix: - description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - signedPrefix: - description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - x-kubernetes-validations: - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' - message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' - message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise - scopes: - description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' - type: array - maxItems: 256 - items: - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' - message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' - - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' - message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching - - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' - message: invalid repository namespace or image specification in the image scope - x-kubernetes-list-type: set - status: - description: status contains the observed state of the resource. - type: object - properties: - conditions: - description: conditions provide details on the status of this API Resource. - type: array - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - type: object - required: - - lastTransitionTime - - message - - reason - - status - - type + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image + signature verification \n Compatibility level 4: No compatibility is provided, + the API can change at any point for any reason. These capabilities should + not be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - type: string - format: date-time - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - type: string - maxLength: 32768 - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - type: integer - format: int64 - minimum: 0 - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - type: string - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - status: - description: status of the condition, one of True, False, Unknown. + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. enum: - - "True" - - "False" - - Unknown - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity type: string - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - served: true - storage: true - subresources: - status: {} + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml index afc22036ce..0d62e2cdae 100644 --- a/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml +++ b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml @@ -17,227 +17,379 @@ spec: singular: imagepolicy scope: Namespaced versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "ImagePolicy holds namespace-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: spec holds user settable values for configuration - type: object - required: - - policy - - scopes - properties: - policy: - description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. - type: object - required: - - rootOfTrust - properties: - rootOfTrust: - description: rootOfTrust specifies the root of trust for the policy. - type: object - required: - - policyType - properties: - fulcioCAWithRekor: - description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' - type: object - required: - - fulcioCAData - - fulcioSubject - - rekorKeyData - properties: - fulcioCAData: - description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. - type: string - maxLength: 8192 - fulcioSubject: - description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. - type: object - required: - - oidcIssuer - - signedEmail - properties: - oidcIssuer: - description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' - type: string - x-kubernetes-validations: - - rule: isURL(self) - message: oidcIssuer must be a valid URL - signedEmail: - description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' - type: string - x-kubernetes-validations: - - rule: self.matches('^\\S+@\\S+$') - message: invalid email address - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - policyType: - description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. - type: string - enum: - - PublicKey - - FulcioCAWithRekor - publicKey: - description: publicKey defines the root of trust based on a sigstore public key. - type: object - required: - - keyData - properties: - keyData: - description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. - type: string - maxLength: 8192 - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - x-kubernetes-validations: - - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' - message: publicKey is required when policyType is PublicKey, and forbidden otherwise - - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise - signedIdentity: - description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". - type: object - required: - - matchPolicy - properties: - exactRepository: - description: exactRepository is required if matchPolicy is set to "ExactRepository". - type: object - required: - - repository - properties: - repository: - description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - matchPolicy: - description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. - type: string - enum: - - MatchRepoDigestOrExact - - MatchRepository - - ExactRepository - - RemapIdentity - remapIdentity: - description: remapIdentity is required if matchPolicy is set to "RemapIdentity". - type: object - required: - - prefix - - signedPrefix - properties: - prefix: - description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - signedPrefix: - description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - x-kubernetes-validations: - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' - message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' - message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise - scopes: - description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' - type: array - maxItems: 256 - items: - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' - message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' - - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' - message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching - - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' - message: invalid repository namespace or image specification in the image scope - x-kubernetes-list-type: set - status: - description: status contains the observed state of the resource. - type: object - properties: - conditions: - description: conditions provide details on the status of this API Resource. - type: array - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - type: object - required: - - lastTransitionTime - - message - - reason - - status - - type + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature + verification \n Compatibility level 4: No compatibility is provided, the + API can change at any point for any reason. These capabilities should not + be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - type: string - format: date-time - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - type: string - maxLength: 32768 - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - type: integer - format: int64 - minimum: 0 - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - type: string - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - status: - description: status of the condition, one of True, False, Unknown. + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. enum: - - "True" - - "False" - - Unknown - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity type: string - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - served: true - storage: true - subresources: - status: {} + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml index d800072ba6..3cb1164875 100644 --- a/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -17,227 +17,379 @@ spec: singular: imagepolicy scope: Namespaced versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: "ImagePolicy holds namespace-wide configuration for image signature verification \n Compatibility level 4: No compatibility is provided, the API can change at any point for any reason. These capabilities should not be used by applications needing long term support." - type: object - required: - - spec - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: spec holds user settable values for configuration - type: object - required: - - policy - - scopes - properties: - policy: - description: policy contains configuration to allow scopes to be verified, and defines how images not matching the verification policy will be treated. - type: object - required: - - rootOfTrust - properties: - rootOfTrust: - description: rootOfTrust specifies the root of trust for the policy. - type: object - required: - - policyType - properties: - fulcioCAWithRekor: - description: 'fulcioCAWithRekor defines the root of trust based on the Fulcio certificate and the Rekor public key. For more information about Fulcio and Rekor, please refer to the document at: https://github.com/sigstore/fulcio and https://github.com/sigstore/rekor' - type: object - required: - - fulcioCAData - - fulcioSubject - - rekorKeyData - properties: - fulcioCAData: - description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. - type: string - maxLength: 8192 - fulcioSubject: - description: fulcioSubject specifies OIDC issuer and the email of the Fulcio authentication configuration. - type: object - required: - - oidcIssuer - - signedEmail - properties: - oidcIssuer: - description: 'oidcIssuer contains the expected OIDC issuer. It will be verified that the Fulcio-issued certificate contains a (Fulcio-defined) certificate extension pointing at this OIDC issuer URL. When Fulcio issues certificates, it includes a value based on an URL inside the client-provided ID token. Example: "https://expected.OIDC.issuer/"' - type: string - x-kubernetes-validations: - - rule: isURL(self) - message: oidcIssuer must be a valid URL - signedEmail: - description: 'signedEmail holds the email address the the Fulcio certificate is issued for. Example: "expected-signing-user@example.com"' - type: string - x-kubernetes-validations: - - rule: self.matches('^\\S+@\\S+$') - message: invalid email address - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - policyType: - description: policyType serves as the union's discriminator. Users are required to assign a value to this field, choosing one of the policy types that define the root of trust. "PublicKey" indicates that the policy relies on a sigstore publicKey and may optionally use a Rekor verification. "FulcioCAWithRekor" indicates that the policy is based on the Fulcio certification and incorporates a Rekor verification. - type: string - enum: - - PublicKey - - FulcioCAWithRekor - publicKey: - description: publicKey defines the root of trust based on a sigstore public key. - type: object - required: - - keyData - properties: - keyData: - description: keyData contains inline base64-encoded data for the PEM format public key. KeyData must be at most 8192 characters. - type: string - maxLength: 8192 - rekorKeyData: - description: rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. rekorKeyData must be at most 8192 characters. - type: string - maxLength: 8192 - x-kubernetes-validations: - - rule: 'has(self.policyType) && self.policyType == ''PublicKey'' ? has(self.publicKey) : !has(self.publicKey)' - message: publicKey is required when policyType is PublicKey, and forbidden otherwise - - rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, and forbidden otherwise - signedIdentity: - description: signedIdentity specifies what image identity the signature claims about the image. The required matchPolicy field specifies the approach used in the verification process to verify the identity in the signature and the actual image identity, the default matchPolicy is "MatchRepoDigestOrExact". - type: object - required: - - matchPolicy - properties: - exactRepository: - description: exactRepository is required if matchPolicy is set to "ExactRepository". - type: object - required: - - repository - properties: - repository: - description: repository is the reference of the image identity to be matched. The value should be a repository name (by omitting the tag or digest) in a registry implementing the "Docker Registry HTTP API V2". For example, docker.io/library/busybox - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - matchPolicy: - description: matchPolicy sets the type of matching to be used. Valid values are "MatchRepoDigestOrExact", "MatchRepository", "ExactRepository", "RemapIdentity". When omitted, the default value is "MatchRepoDigestOrExact". If set matchPolicy to ExactRepository, then the exactRepository must be specified. If set matchPolicy to RemapIdentity, then the remapIdentity must be specified. "MatchRepoDigestOrExact" means that the identity in the signature must be in the same repository as the image identity if the image identity is referenced by a digest. Otherwise, the identity in the signature must be the same as the image identity. "MatchRepository" means that the identity in the signature must be in the same repository as the image identity. "ExactRepository" means that the identity in the signature must be in the same repository as a specific identity specified by "repository". "RemapIdentity" means that the signature must be in the same as the remapped image identity. Remapped image identity is obtained by replacing the "prefix" with the specified “signedPrefix” if the the image identity matches the specified remapPrefix. - type: string - enum: - - MatchRepoDigestOrExact - - MatchRepository - - ExactRepository - - RemapIdentity - remapIdentity: - description: remapIdentity is required if matchPolicy is set to "RemapIdentity". - type: object - required: - - prefix - - signedPrefix - properties: - prefix: - description: prefix is the prefix of the image identity to be matched. If the image identity matches the specified prefix, that prefix is replaced by the specified “signedPrefix” (otherwise it is used as unchanged and no remapping takes place). This useful when verifying signatures for a mirror of some other repository namespace that preserves the vendor’s repository structure. The prefix and signedPrefix values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - signedPrefix: - description: signedPrefix is the prefix of the image identity to be matched in the signature. The format is the same as "prefix". The values can be either host[:port] values (matching exactly the same host[:port], string), repository namespaces, or repositories (i.e. they must not contain tags/digests), and match as prefixes of the fully expanded form. For example, docker.io/library/busybox (not busybox) to specify that single repository, or docker.io/library (not an empty string) to specify the parent namespace of docker.io/library/busybox. - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? self.matches(''^(localhost:[0-9]+)$''): true' - message: invalid repository or prefix in the signedIdentity, should not include the tag or digest - - rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') - message: invalid repository or prefix in the signedIdentity - x-kubernetes-validations: - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') ? has(self.exactRepository) : !has(self.exactRepository)' - message: exactRepository is required when matchPolicy is ExactRepository, and forbidden otherwise - - rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') ? has(self.remapIdentity) : !has(self.remapIdentity)' - message: remapIdentity is required when matchPolicy is RemapIdentity, and forbidden otherwise - scopes: - description: 'scopes defines the list of image identities assigned to a policy. Each item refers to a scope in a registry implementing the "Docker Registry HTTP API V2". Scopes matching individual images are named Docker references in the fully expanded form, either using a tag or digest. For example, docker.io/library/busybox:latest (not busybox:latest). More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), a repository namespace, or a registry host (by only specifying the host name and possibly a port number) or a wildcard expression starting with `*.`, for matching all subdomains (not including a port number). Wildcards are only supported for subdomain matching, and may not be used in the middle of the host, i.e. *.example.com is a valid case, but example*.*.com is not. Please be aware that the scopes should not be nested under the repositories of OpenShift Container Platform images. If configured, the policies for OpenShift Container Platform repositories will not be in effect. For additional details about the format, please refer to the document explaining the docker transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' - type: array - maxItems: 256 - items: - type: string - maxLength: 512 - x-kubernetes-validations: - - rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] == ''localhost'' : true' - message: invalid image scope format, scope must contain a fully qualified domain name or 'localhost' - - rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') : true' - message: invalid image scope with wildcard, a wildcard can only be at the start of the domain and is only supported for subdomain matching, not path matching - - rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') : true' - message: invalid repository namespace or image specification in the image scope - x-kubernetes-list-type: set - status: - description: status contains the observed state of the resource. - type: object - properties: - conditions: - description: conditions provide details on the status of this API Resource. - type: array - items: - description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - type: object - required: - - lastTransitionTime - - message - - reason - - status - - type + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature + verification \n Compatibility level 4: No compatibility is provided, the + API can change at any point for any reason. These capabilities should not + be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - type: string - format: date-time - message: - description: message is a human readable message indicating details about the transition. This may be an empty string. - type: string - maxLength: 32768 - observedGeneration: - description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. - type: integer - format: int64 - minimum: 0 - reason: - description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. - type: string - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - status: - description: status of the condition, one of True, False, Unknown. + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. enum: - - "True" - - "False" - - Unknown - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity type: string - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - served: true - storage: true - subresources: - status: {} + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} 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 29bb9cba7f..a87413f43b 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller.go @@ -360,7 +360,7 @@ func (ctrl *Controller) clusterImagePolicyDeleted(_ interface{}) { if !ctrl.sigstoreAPIEnabled() { return } - ctrl.imagePoliciesDeleted() + ctrl.imgQueue.Add("openshift-config") } func (ctrl *Controller) imagePoliciesDeleted() { diff --git a/pkg/controller/container-runtime-config/helpers.go b/pkg/controller/container-runtime-config/helpers.go index 4fc5d48919..b4683e20da 100644 --- a/pkg/controller/container-runtime-config/helpers.go +++ b/pkg/controller/container-runtime-config/helpers.go @@ -895,11 +895,7 @@ func policyItemFromSpec(policy apicfgv1alpha1.Policy) (signature.PolicyRequireme } case apicfgv1alpha1.FulcioCAWithRekorRootOfTrust: fulcioOptions := []signature.PRSigstoreSignedFulcioOption{} - fulcioDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.FulcioCAWithRekor.FulcioCAData) - if err != nil { - return nil, fmt.Errorf("failed to decode fulcioCAData: %v", err) - } - fulcioOptions = append(fulcioOptions, signature.PRSigstoreSignedFulcioWithCAData(fulcioDataDec), + fulcioOptions = append(fulcioOptions, signature.PRSigstoreSignedFulcioWithCAData(policy.RootOfTrust.FulcioCAWithRekor.FulcioCAData), signature.PRSigstoreSignedFulcioWithOIDCIssuer(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.OIDCIssuer), signature.PRSigstoreSignedFulcioWithSubjectEmail(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.SignedEmail)) diff --git a/pkg/controller/container-runtime-config/helpers_test.go b/pkg/controller/container-runtime-config/helpers_test.go index f2cfbc229c..ff08490d84 100644 --- a/pkg/controller/container-runtime-config/helpers_test.go +++ b/pkg/controller/container-runtime-config/helpers_test.go @@ -496,7 +496,7 @@ var testClusterImagePolicyCRs = map[string]apicfgv1alpha1.ClusterImagePolicy{ RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ PolicyType: apicfgv1alpha1.FulcioCAWithRekorRootOfTrust, FulcioCAWithRekor: &apicfgv1alpha1.FulcioCAWithRekor{ - FulcioCAData: "dGVzdC1jYS1kYXRhLWRhdGE=", + FulcioCAData: []byte("dGVzdC1jYS1kYXRhLWRhdGE="), RekorKeyData: "dGVzdC1yZWtvci1rZXktZGF0YQ==", FulcioSubject: apicfgv1alpha1.PolicyFulcioSubject{ OIDCIssuer: "https://OIDC.example.com", diff --git a/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml b/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml index 303fc63019..d71799f595 100644 --- a/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml @@ -147,6 +147,81 @@ spec: clusterNetworkMTU: description: ClusterNetworkMTU is the MTU for inter-pod networking. type: integer + conditions: + description: 'conditions represents the observations of a network.config + current state. Known .status.conditions.type are: "NetworkTypeMigrationInProgress", + "NetworkTypeMigrationMTUReady", "NetworkTypeMigrationTargetCNIAvailable", + "NetworkTypeMigrationTargetCNIInUse" and "NetworkTypeMigrationOriginalCNIPurged"' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map migration: description: Migration contains the cluster network migration configuration. properties: diff --git a/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml b/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml index 7922d44812..c85d122a65 100644 --- a/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml +++ b/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml @@ -12,7 +12,7 @@ tests: apiVersion: config.openshift.io/v1 kind: Network spec: {} - - name: Should not be able to set status conditions + - name: Should be able to set status conditions initial: | apiVersion: config.openshift.io/v1 kind: Network @@ -28,4 +28,10 @@ tests: apiVersion: config.openshift.io/v1 kind: Network spec: {} - status: {} + status: + conditions: + - type: NetworkTypeMigrationInProgress + status: "False" + reason: "Reason" + message: "Message" + lastTransitionTime: "2023-10-25T12:00:00Z" diff --git a/vendor/github.com/openshift/api/config/v1/types_network.go b/vendor/github.com/openshift/api/config/v1/types_network.go index 3d345b2d60..794f3db7b7 100644 --- a/vendor/github.com/openshift/api/config/v1/types_network.go +++ b/vendor/github.com/openshift/api/config/v1/types_network.go @@ -95,7 +95,6 @@ type NetworkStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - // +openshift:enable:FeatureSets=CustomNoUpgrade;TechPreviewNoUpgrade Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml index 607b85698d..79c1767755 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml index c5129d7b4a..538c44ace1 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml index a94542da44..0d62e2cdae 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml index 11f72b1724..3cb1164875 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go b/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go index b93f17c5da..e9bc278636 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go +++ b/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go @@ -110,7 +110,7 @@ type FulcioCAWithRekor struct { // fulcioCAData must be at most 8192 characters. // +kubebuilder:validation:Required // +kubebuilder:validation:MaxLength=8192 - FulcioCAData string `json:"fulcioCAData"` + FulcioCAData []byte `json:"fulcioCAData"` // rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. // rekorKeyData must be at most 8192 characters. // +kubebuilder:validation:Required diff --git a/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go index 21b08cf333..2060517364 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go @@ -230,6 +230,11 @@ func (in *EtcdBackupSpec) DeepCopy() *EtcdBackupSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FulcioCAWithRekor) DeepCopyInto(out *FulcioCAWithRekor) { *out = *in + if in.FulcioCAData != nil { + in, out := &in.FulcioCAData, &out.FulcioCAData + *out = make([]byte, len(*in)) + copy(*out, *in) + } out.FulcioSubject = in.FulcioSubject return } @@ -568,7 +573,7 @@ func (in *PolicyRootOfTrust) DeepCopyInto(out *PolicyRootOfTrust) { if in.FulcioCAWithRekor != nil { in, out := &in.FulcioCAWithRekor, &out.FulcioCAWithRekor *out = new(FulcioCAWithRekor) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml index 6911ce89c8..52f6f37ea5 100644 --- a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml @@ -54,6 +54,7 @@ spec: - vpc.block.csi.ibm.io - powervs.csi.ibm.com - secrets-store.csi.k8s.io + - smb.csi.k8s.io type: string type: object spec: diff --git a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch index 2a02f97f2e..ce0db8be98 100644 --- a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch +++ b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch @@ -20,3 +20,4 @@ - vpc.block.csi.ibm.io - powervs.csi.ibm.com - secrets-store.csi.k8s.io + - smb.csi.k8s.io diff --git a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go index 8e9853b06f..00a36015e3 100644 --- a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go +++ b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go @@ -84,6 +84,7 @@ const ( IBMVPCBlockCSIDriver CSIDriverName = "vpc.block.csi.ibm.io" IBMPowerVSBlockCSIDriver CSIDriverName = "powervs.csi.ibm.com" SecretsStoreCSIDriver CSIDriverName = "secrets-store.csi.k8s.io" + SambaCSIDriver CSIDriverName = "smb.csi.k8s.io" ) // ClusterCSIDriverSpec is the desired behavior of CSI driver operator diff --git a/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go b/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go index 6fe09c0eb4..5469397374 100644 --- a/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go +++ b/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go @@ -5,7 +5,7 @@ package v1alpha1 // FulcioCAWithRekorApplyConfiguration represents an declarative configuration of the FulcioCAWithRekor type for use // with apply. type FulcioCAWithRekorApplyConfiguration struct { - FulcioCAData *string `json:"fulcioCAData,omitempty"` + FulcioCAData []byte `json:"fulcioCAData,omitempty"` RekorKeyData *string `json:"rekorKeyData,omitempty"` FulcioSubject *PolicyFulcioSubjectApplyConfiguration `json:"fulcioSubject,omitempty"` } @@ -16,11 +16,13 @@ func FulcioCAWithRekor() *FulcioCAWithRekorApplyConfiguration { return &FulcioCAWithRekorApplyConfiguration{} } -// WithFulcioCAData sets the FulcioCAData field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the FulcioCAData field is set to the value of the last call. -func (b *FulcioCAWithRekorApplyConfiguration) WithFulcioCAData(value string) *FulcioCAWithRekorApplyConfiguration { - b.FulcioCAData = &value +// WithFulcioCAData adds the given value to the FulcioCAData field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the FulcioCAData field. +func (b *FulcioCAWithRekorApplyConfiguration) WithFulcioCAData(values ...byte) *FulcioCAWithRekorApplyConfiguration { + for i := range values { + b.FulcioCAData = append(b.FulcioCAData, values[i]) + } return b } diff --git a/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go index 328e4df340..da3007f6b5 100644 --- a/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go @@ -3619,7 +3619,6 @@ var schemaYAML = typed.YAMLObject(`types: - name: fulcioCAData type: scalar: string - default: "" - name: fulcioSubject type: namedType: com.github.openshift.api.config.v1alpha1.PolicyFulcioSubject diff --git a/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go index cdbebac7c3..edbc7323bf 100644 --- a/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go @@ -238,7 +238,9 @@ var schemaYAML = typed.YAMLObject(`types: list: elementType: namedType: com.github.openshift.api.machine.v1.VSphereFailureDomain - elementRelationship: atomic + elementRelationship: associative + keys: + - name unions: - discriminator: platform fields: diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go index 067f820eda..900c47c231 100644 --- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go @@ -1398,16 +1398,10 @@ var schemaYAML = typed.YAMLObject(`types: elementRelationship: atomic - name: com.github.openshift.api.operator.v1.IPsecConfig map: - elementType: - scalar: untyped - list: - elementType: - namedType: __untyped_atomic_ - elementRelationship: atomic - map: - elementType: - namedType: __untyped_deduced_ - elementRelationship: separable + fields: + - name: mode + type: + scalar: string - name: com.github.openshift.api.operator.v1.IPv4GatewayConfig map: fields: @@ -2506,6 +2500,8 @@ var schemaYAML = typed.YAMLObject(`types: - name: ipsecConfig type: namedType: com.github.openshift.api.operator.v1.IPsecConfig + default: + mode: Disabled - name: mtu type: scalar: numeric diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go new file mode 100644 index 0000000000..864010dffc --- /dev/null +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go @@ -0,0 +1,27 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + v1 "github.com/openshift/api/operator/v1" +) + +// IPsecConfigApplyConfiguration represents an declarative configuration of the IPsecConfig type for use +// with apply. +type IPsecConfigApplyConfiguration struct { + Mode *v1.IPsecMode `json:"mode,omitempty"` +} + +// IPsecConfigApplyConfiguration constructs an declarative configuration of the IPsecConfig type for use with +// apply. +func IPsecConfig() *IPsecConfigApplyConfiguration { + return &IPsecConfigApplyConfiguration{} +} + +// WithMode sets the Mode field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Mode field is set to the value of the last call. +func (b *IPsecConfigApplyConfiguration) WithMode(value v1.IPsecMode) *IPsecConfigApplyConfiguration { + b.Mode = &value + return b +} diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go index 6b5f7a4781..61701f52c6 100644 --- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go @@ -2,17 +2,13 @@ package v1 -import ( - operatorv1 "github.com/openshift/api/operator/v1" -) - // OVNKubernetesConfigApplyConfiguration represents an declarative configuration of the OVNKubernetesConfig type for use // with apply. type OVNKubernetesConfigApplyConfiguration struct { MTU *uint32 `json:"mtu,omitempty"` GenevePort *uint32 `json:"genevePort,omitempty"` HybridOverlayConfig *HybridOverlayConfigApplyConfiguration `json:"hybridOverlayConfig,omitempty"` - IPsecConfig *operatorv1.IPsecConfig `json:"ipsecConfig,omitempty"` + IPsecConfig *IPsecConfigApplyConfiguration `json:"ipsecConfig,omitempty"` PolicyAuditConfig *PolicyAuditConfigApplyConfiguration `json:"policyAuditConfig,omitempty"` GatewayConfig *GatewayConfigApplyConfiguration `json:"gatewayConfig,omitempty"` V4InternalSubnet *string `json:"v4InternalSubnet,omitempty"` @@ -53,8 +49,8 @@ func (b *OVNKubernetesConfigApplyConfiguration) WithHybridOverlayConfig(value *H // WithIPsecConfig sets the IPsecConfig field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the IPsecConfig field is set to the value of the last call. -func (b *OVNKubernetesConfigApplyConfiguration) WithIPsecConfig(value operatorv1.IPsecConfig) *OVNKubernetesConfigApplyConfiguration { - b.IPsecConfig = &value +func (b *OVNKubernetesConfigApplyConfiguration) WithIPsecConfig(value *IPsecConfigApplyConfiguration) *OVNKubernetesConfigApplyConfiguration { + b.IPsecConfig = value return b } diff --git a/vendor/k8s.io/code-generator/generate-groups.sh b/vendor/k8s.io/code-generator/generate-groups.sh old mode 100755 new mode 100644 diff --git a/vendor/k8s.io/code-generator/generate-internal-groups.sh b/vendor/k8s.io/code-generator/generate-internal-groups.sh old mode 100755 new mode 100644 diff --git a/vendor/modules.txt b/vendor/modules.txt index d8d80ed7b4..80fe9319a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -815,7 +815,7 @@ github.com/opencontainers/runc/libcontainer/user # github.com/opencontainers/runtime-spec v1.1.0 ## explicit github.com/opencontainers/runtime-spec/specs-go -# github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 +# github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f ## explicit; go 1.21 github.com/openshift/api github.com/openshift/api/apiserver @@ -888,7 +888,7 @@ github.com/openshift/api/template github.com/openshift/api/template/v1 github.com/openshift/api/user github.com/openshift/api/user/v1 -# github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 +# github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 ## explicit; go 1.21 github.com/openshift/client-go/build/applyconfigurations/build/v1 github.com/openshift/client-go/build/applyconfigurations/internal @@ -2288,3 +2288,5 @@ sigs.k8s.io/structured-merge-diff/v4/value sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 # k8s.io/kube-openapi => github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 +# github.com/openshift/api => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f +# github.com/openshift/client-go => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 From 41fa961f753acff9ed521da394aab2d3c43caaae Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Tue, 20 Feb 2024 12:58:43 -0500 Subject: [PATCH 3/3] ownerreference --- .../container_runtime_config_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a87413f43b..ef0defa61d 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller.go @@ -988,7 +988,7 @@ func (ctrl *Controller) syncImageConfig(key string) error { return fmt.Errorf("could not sync registries Ignition config: %w", err) } if namespacedPoliciesIgn != nil { - appliedImagePolicy, err = ctrl.syncIgnitionConfig(managedKeyNamespaceImagePolicy, namespacedPoliciesIgn, pool, ownerReferenceForImagePolicy(imagePolicies, clusterImagePolicies)) + appliedImagePolicy, err = ctrl.syncIgnitionConfig(managedKeyNamespaceImagePolicy, namespacedPoliciesIgn, pool, ownerReferenceForImageConfig(imgcfg)) if err != nil { return fmt.Errorf("could not sync imagepolicies Ignition config: %w", err) }