Skip to content

Commit 132e79f

Browse files
committed
Auto-detect sigstore ClusterImagePolicy for EDPM signature verification
When a disconnected environment has a ClusterImagePolicy configured with sigstore (cosign) signature verification for a mirror registry, the openstack-operator now auto-detects it and passes the necessary ansible variables to edpm-ansible for configuring signature verification on EDPM data plane nodes. if ClusterImagePolicy CRD is not installed or no relevant policy exists, the operator continues without enabling signature verification. This maintains backward compatibility. Requires: OCP 4.20+ (sigstore GA) and oc-mirror v2. There would be a follow-up edpm-ansible patch to use these ansible vars. jira: OSPRH-28852 Change-Id: I2cbc4e83884562bd17065ee7158e00e5c9b12160 Signed-off-by: rabi <ramishra@redhat.com>
1 parent 5c74e7f commit 132e79f

6 files changed

Lines changed: 715 additions & 10 deletions

File tree

bindata/rbac/rbac.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ rules:
304304
- apiGroups:
305305
- config.openshift.io
306306
resources:
307+
- clusterimagepolicies
307308
- imagedigestmirrorsets
308309
- images
309310
- networks

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ rules:
255255
- apiGroups:
256256
- config.openshift.io
257257
resources:
258+
- clusterimagepolicies
258259
- imagedigestmirrorsets
259260
- images
260261
- networks

internal/controller/dataplane/openstackdataplanenodeset_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func (r *OpenStackDataPlaneNodeSetReconciler) GetLogger(ctx context.Context) log
133133

134134
// RBAC for ImageContentSourcePolicy and MachineConfig
135135
// +kubebuilder:rbac:groups="operator.openshift.io",resources=imagecontentsourcepolicies,verbs=get;list;watch
136+
// +kubebuilder:rbac:groups="config.openshift.io",resources=clusterimagepolicies,verbs=get;list;watch
136137
// +kubebuilder:rbac:groups="config.openshift.io",resources=imagedigestmirrorsets,verbs=get;list;watch
137138
// +kubebuilder:rbac:groups="config.openshift.io",resources=images,verbs=get;list;watch
138139
// +kubebuilder:rbac:groups="machineconfiguration.openshift.io",resources=machineconfigs,verbs=get;list;watch

internal/dataplane/inventory.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,27 +145,38 @@ func GenerateNodeSetInventory(ctx context.Context, helper *helper.Helper,
145145
registryConfig, err := util.GetMCRegistryConf(ctx, helper)
146146
if err != nil {
147147
// CRD not installed (non-OpenShift or no MCO) - log warning and continue.
148-
// This allows graceful degradation when running on non-OpenShift clusters.
149148
// Users can manually configure registries.conf via ansibleVars.
150149
if util.IsNoMatchError(err) {
151-
helper.GetLogger().Info("Disconnected environment detected but MachineConfig CRD not available. "+
152-
"Registry configuration will not be propagated to dataplane nodes. "+
153-
"You may need to configure registries.conf manually using ansibleVars "+
154-
"(edpm_podman_disconnected_ocp and edpm_podman_registries_conf).",
150+
helper.GetLogger().Info("MachineConfig CRD not available; registry config will not be propagated",
155151
"error", err.Error())
156152
} else {
157-
// CRD exists but resource not found, or other errors (network issues,
158-
// permissions, etc.) - return the error. If MCO is installed but the
159-
// registry MachineConfig doesn't exist, this indicates a misconfiguration.
160153
return "", fmt.Errorf("failed to get MachineConfig registry configuration: %w", err)
161154
}
162155
} else {
163156
helper.GetLogger().Info("Mirror registries detected via IDMS/ICSP. Using OCP registry configuration.")
164-
165-
// Use OCP registries.conf for mirror registry deployments
166157
nodeSetGroup.Vars["edpm_podman_registries_conf"] = registryConfig
167158
nodeSetGroup.Vars["edpm_podman_disconnected_ocp"] = hasMirrorRegistries
168159
}
160+
161+
mirrorScopes, sourceByMirror, err := util.GetMirrorRegistryScopes(ctx, helper)
162+
if err != nil {
163+
return "", fmt.Errorf("failed to get mirror registries for sigstore verification: %w", err)
164+
}
165+
166+
sigstorePolicy, err := util.GetSigstoreImagePolicy(ctx, helper, mirrorScopes, sourceByMirror)
167+
if err != nil {
168+
return "", fmt.Errorf("failed to get ClusterImagePolicy for sigstore verification: %w", err)
169+
}
170+
if sigstorePolicy != nil {
171+
nodeSetGroup.Vars["edpm_container_signature_verification"] = true
172+
nodeSetGroup.Vars["edpm_container_signature_registry_mappings"] = sigstorePolicy.RegistryMappings
173+
nodeSetGroup.Vars["edpm_container_signature_cosign_key_data"] = sigstorePolicy.CosignKeyData
174+
if sigstorePolicy.SignedPrefix != "" {
175+
nodeSetGroup.Vars["edpm_container_signature_signed_prefix"] = sigstorePolicy.SignedPrefix
176+
}
177+
} else {
178+
helper.GetLogger().Info("No matching ClusterImagePolicy found; skipping sigstore verification")
179+
}
169180
}
170181

171182
// add TLS ansible variable

internal/dataplane/util/image_registry.go

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"fmt"
8+
"sort"
89
"strings"
910

1011
ocpconfigv1 "github.com/openshift/api/config/v1"
1112
mc "github.com/openshift/api/machineconfiguration/v1"
1213
ocpicsp "github.com/openshift/api/operator/v1alpha1"
1314
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
1415
corev1 "k8s.io/api/core/v1"
16+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1517
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20+
"k8s.io/apimachinery/pkg/runtime/schema"
1721
"k8s.io/apimachinery/pkg/types"
1822
)
1923

@@ -65,6 +69,86 @@ func HasMirrorRegistries(ctx context.Context, helper *helper.Helper) (bool, erro
6569
return false, nil
6670
}
6771

72+
func addNormalizedScopes(set map[string]struct{}, values []string) {
73+
for _, value := range values {
74+
if scope := normalizeImageScope(value); scope != "" {
75+
set[scope] = struct{}{}
76+
}
77+
}
78+
}
79+
80+
func sortedSetKeys(set map[string]struct{}) []string {
81+
if len(set) == 0 {
82+
return nil
83+
}
84+
result := make([]string, 0, len(set))
85+
for k := range set {
86+
result = append(result, k)
87+
}
88+
sort.Strings(result)
89+
return result
90+
}
91+
92+
// GetMirrorRegistryScopes returns the configured mirror scopes and their
93+
// source registry mapping, preferring IDMS and falling back to ICSP.
94+
// The returned scopes are normalized and de-duplicated for policy matching.
95+
// The sourceByMirror map links each mirror scope back to its IDMS/ICSP source.
96+
func GetMirrorRegistryScopes(ctx context.Context, helper *helper.Helper) ([]string, map[string]string, error) {
97+
idmsList := &ocpconfigv1.ImageDigestMirrorSetList{}
98+
if err := helper.GetClient().List(ctx, idmsList); err != nil {
99+
if !IsNoMatchError(err) {
100+
return nil, nil, err
101+
}
102+
} else {
103+
scopes := map[string]struct{}{}
104+
sourceByMirror := map[string]string{}
105+
for _, idms := range idmsList.Items {
106+
for _, mirrorSet := range idms.Spec.ImageDigestMirrors {
107+
source := normalizeImageScope(string(mirrorSet.Source))
108+
for _, mirror := range mirrorSet.Mirrors {
109+
m := normalizeImageScope(string(mirror))
110+
if m != "" {
111+
scopes[m] = struct{}{}
112+
if source != "" {
113+
sourceByMirror[m] = source
114+
}
115+
}
116+
}
117+
}
118+
}
119+
if result := sortedSetKeys(scopes); len(result) > 0 {
120+
return result, sourceByMirror, nil
121+
}
122+
}
123+
124+
icspList := &ocpicsp.ImageContentSourcePolicyList{}
125+
if err := helper.GetClient().List(ctx, icspList); err != nil {
126+
if !IsNoMatchError(err) {
127+
return nil, nil, err
128+
}
129+
} else {
130+
scopes := map[string]struct{}{}
131+
sourceByMirror := map[string]string{}
132+
for _, icsp := range icspList.Items {
133+
for _, mirrorSet := range icsp.Spec.RepositoryDigestMirrors {
134+
source := normalizeImageScope(mirrorSet.Source)
135+
for _, mirror := range mirrorSet.Mirrors {
136+
m := normalizeImageScope(mirror)
137+
if m != "" {
138+
scopes[m] = struct{}{}
139+
if source != "" {
140+
sourceByMirror[m] = source
141+
}
142+
}
143+
}
144+
}
145+
}
146+
return sortedSetKeys(scopes), sourceByMirror, nil
147+
}
148+
149+
return nil, nil, nil
150+
}
151+
68152
// IsNoMatchError checks if the error indicates that a CRD/resource type doesn't exist
69153
func IsNoMatchError(err error) bool {
70154
errStr := err.Error()
@@ -151,6 +235,214 @@ func getMachineConfig(ctx context.Context, helper *helper.Helper) (mc.MachineCon
151235
return masterMachineConfig, nil
152236
}
153237

238+
// RegistryMapping pairs a mirror registry with its upstream source from IDMS/ICSP.
239+
type RegistryMapping struct {
240+
Mirror string `json:"mirror"`
241+
Source string `json:"source"`
242+
}
243+
244+
// SigstorePolicyInfo contains the EDPM-relevant parts of a ClusterImagePolicy.
245+
// A single RemapIdentity signedPrefix covers all mirrors under the same registry
246+
// root — the container runtime replaces only the prefix, preserving namespace paths.
247+
type SigstorePolicyInfo struct {
248+
RegistryMappings []RegistryMapping
249+
CosignKeyData string
250+
SignedPrefix string
251+
}
252+
253+
const (
254+
clusterImagePolicyCRDName = "clusterimagepolicies.config.openshift.io"
255+
clusterImagePolicyGroup = "config.openshift.io"
256+
clusterImagePolicyKind = "ClusterImagePolicy"
257+
clusterImagePolicyV1 = "v1"
258+
clusterImagePolicyV1Alpha1 = "v1alpha1"
259+
publicKeyRootOfTrustPolicyType = "PublicKey"
260+
remapIdentityMatchPolicy = "RemapIdentity"
261+
)
262+
263+
func normalizeImageScope(scope string) string {
264+
return strings.TrimSuffix(strings.TrimSpace(scope), "/")
265+
}
266+
267+
func clusterImagePolicyScopeMatchesMirror(policyScope string, mirrorScope string) bool {
268+
policyScope = normalizeImageScope(policyScope)
269+
mirrorScope = normalizeImageScope(mirrorScope)
270+
271+
if policyScope == "" || mirrorScope == "" {
272+
return false
273+
}
274+
275+
if strings.HasPrefix(policyScope, "*.") {
276+
mirrorHostPort := strings.SplitN(mirrorScope, "/", 2)[0]
277+
mirrorHost := strings.SplitN(mirrorHostPort, ":", 2)[0]
278+
suffix := strings.TrimPrefix(policyScope, "*")
279+
return strings.HasSuffix(mirrorHost, suffix)
280+
}
281+
282+
return mirrorScope == policyScope || strings.HasPrefix(mirrorScope, policyScope+"/")
283+
}
284+
285+
func getServedClusterImagePolicyVersion(ctx context.Context, helper *helper.Helper) (string, error) {
286+
crd := &apiextensionsv1.CustomResourceDefinition{}
287+
if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: clusterImagePolicyCRDName}, crd); err != nil {
288+
if k8s_errors.IsNotFound(err) || IsNoMatchError(err) {
289+
return "", nil
290+
}
291+
return "", err
292+
}
293+
294+
for _, preferredVersion := range []string{clusterImagePolicyV1, clusterImagePolicyV1Alpha1} {
295+
for _, version := range crd.Spec.Versions {
296+
if version.Name == preferredVersion && version.Served {
297+
return preferredVersion, nil
298+
}
299+
}
300+
}
301+
302+
return "", nil
303+
}
304+
305+
func listClusterImagePolicies(
306+
ctx context.Context,
307+
helper *helper.Helper,
308+
version string,
309+
) (*unstructured.UnstructuredList, error) {
310+
policyList := &unstructured.UnstructuredList{}
311+
policyList.SetGroupVersionKind(schema.GroupVersionKind{
312+
Group: clusterImagePolicyGroup,
313+
Version: version,
314+
Kind: clusterImagePolicyKind + "List",
315+
})
316+
317+
if err := helper.GetClient().List(ctx, policyList); err != nil {
318+
if IsNoMatchError(err) {
319+
return nil, nil
320+
}
321+
return nil, err
322+
}
323+
324+
return policyList, nil
325+
}
326+
327+
// GetSigstoreImagePolicy checks if OCP has a ClusterImagePolicy configured
328+
// with sigstore signature verification for one of the mirror registries in use.
329+
// sourceByMirror maps each mirror scope to its upstream source registry (from IDMS/ICSP).
330+
// Returns policy info if a relevant policy is found, nil if no policy exists.
331+
// Returns nil without error if the ClusterImagePolicy CRD is not installed.
332+
func GetSigstoreImagePolicy(ctx context.Context, helper *helper.Helper, mirrorScopes []string, sourceByMirror map[string]string) (*SigstorePolicyInfo, error) {
333+
if len(mirrorScopes) == 0 {
334+
return nil, nil
335+
}
336+
337+
version, err := getServedClusterImagePolicyVersion(ctx, helper)
338+
if err != nil {
339+
return nil, err
340+
}
341+
if version == "" {
342+
return nil, nil
343+
}
344+
345+
policyList, err := listClusterImagePolicies(ctx, helper, version)
346+
if err != nil {
347+
return nil, err
348+
}
349+
if policyList == nil {
350+
return nil, nil
351+
}
352+
353+
var matches []string
354+
var match *SigstorePolicyInfo
355+
356+
for _, policy := range policyList.Items {
357+
if policy.GetName() == "openshift" {
358+
continue
359+
}
360+
361+
policyType, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "policyType")
362+
if err != nil {
363+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s policyType: %w", policy.GetName(), err)
364+
}
365+
if !found || policyType != publicKeyRootOfTrustPolicyType {
366+
continue
367+
}
368+
369+
keyData, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "publicKey", "keyData")
370+
if err != nil {
371+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s keyData: %w", policy.GetName(), err)
372+
}
373+
if !found || len(keyData) == 0 {
374+
continue
375+
}
376+
377+
scopes, found, err := unstructured.NestedStringSlice(policy.Object, "spec", "scopes")
378+
if err != nil {
379+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s scopes: %w", policy.GetName(), err)
380+
}
381+
if !found || len(scopes) == 0 {
382+
continue
383+
}
384+
385+
signedPrefix := ""
386+
matchPolicy, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "signedIdentity", "matchPolicy")
387+
if err != nil {
388+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s matchPolicy: %w", policy.GetName(), err)
389+
}
390+
if found && matchPolicy == remapIdentityMatchPolicy {
391+
signedPrefix, _, err = unstructured.NestedString(
392+
policy.Object,
393+
"spec", "policy", "signedIdentity", "remapIdentity", "signedPrefix",
394+
)
395+
if err != nil {
396+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s signedPrefix: %w", policy.GetName(), err)
397+
}
398+
}
399+
400+
matchedMirrorScopes := map[string]struct{}{}
401+
for _, scope := range scopes {
402+
policyScope := normalizeImageScope(scope)
403+
if policyScope == "" {
404+
continue
405+
}
406+
407+
for _, mirrorScope := range mirrorScopes {
408+
if clusterImagePolicyScopeMatchesMirror(policyScope, mirrorScope) {
409+
matchedMirrorScopes[normalizeImageScope(mirrorScope)] = struct{}{}
410+
}
411+
}
412+
}
413+
if len(matchedMirrorScopes) == 0 {
414+
continue
415+
}
416+
417+
sortedMirrors := sortedSetKeys(matchedMirrorScopes)
418+
419+
mappings := make([]RegistryMapping, 0, len(sortedMirrors))
420+
for _, m := range sortedMirrors {
421+
mappings = append(mappings, RegistryMapping{
422+
Mirror: m,
423+
Source: sourceByMirror[m],
424+
})
425+
}
426+
427+
matches = append(matches, fmt.Sprintf("%s (%s)", policy.GetName(), strings.Join(sortedMirrors, ", ")))
428+
match = &SigstorePolicyInfo{
429+
RegistryMappings: mappings,
430+
CosignKeyData: keyData,
431+
SignedPrefix: signedPrefix,
432+
}
433+
}
434+
435+
if len(matches) > 1 {
436+
sort.Strings(matches)
437+
return nil, fmt.Errorf(
438+
"expected exactly one ClusterImagePolicy matching mirror registries, found %d: %s",
439+
len(matches), strings.Join(matches, ", "),
440+
)
441+
}
442+
443+
return match, nil
444+
}
445+
154446
// GetMirrorRegistryCACerts retrieves CA certificates from image.config.openshift.io/cluster.
155447
// Returns nil without error if:
156448
// - not on OpenShift (Image CRD doesn't exist)

0 commit comments

Comments
 (0)