Skip to content

Commit 860cfb9

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 860cfb9

6 files changed

Lines changed: 659 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, 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)
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_mirror_registries"] = sigstorePolicy.MirrorRegistries
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: 260 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,67 @@ 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, preferring IDMS
93+
// and falling back to ICSP only when no IDMS mirror scopes are present.
94+
// The returned values are normalized and de-duplicated for policy matching.
95+
func GetMirrorRegistryScopes(ctx context.Context, helper *helper.Helper) ([]string, error) {
96+
idmsList := &ocpconfigv1.ImageDigestMirrorSetList{}
97+
if err := helper.GetClient().List(ctx, idmsList); err != nil {
98+
if !IsNoMatchError(err) {
99+
return nil, err
100+
}
101+
} else {
102+
scopes := map[string]struct{}{}
103+
for _, idms := range idmsList.Items {
104+
for _, mirrorSet := range idms.Spec.ImageDigestMirrors {
105+
for _, mirror := range mirrorSet.Mirrors {
106+
addNormalizedScopes(scopes, []string{string(mirror)})
107+
}
108+
}
109+
}
110+
if result := sortedSetKeys(scopes); len(result) > 0 {
111+
return result, nil
112+
}
113+
}
114+
115+
icspList := &ocpicsp.ImageContentSourcePolicyList{}
116+
if err := helper.GetClient().List(ctx, icspList); err != nil {
117+
if !IsNoMatchError(err) {
118+
return nil, err
119+
}
120+
} else {
121+
scopes := map[string]struct{}{}
122+
for _, icsp := range icspList.Items {
123+
for _, mirrorSet := range icsp.Spec.RepositoryDigestMirrors {
124+
addNormalizedScopes(scopes, mirrorSet.Mirrors)
125+
}
126+
}
127+
return sortedSetKeys(scopes), nil
128+
}
129+
130+
return nil, nil
131+
}
132+
68133
// IsNoMatchError checks if the error indicates that a CRD/resource type doesn't exist
69134
func IsNoMatchError(err error) bool {
70135
errStr := err.Error()
@@ -151,6 +216,201 @@ func getMachineConfig(ctx context.Context, helper *helper.Helper) (mc.MachineCon
151216
return masterMachineConfig, nil
152217
}
153218

219+
// SigstorePolicyInfo contains the EDPM-relevant parts of a ClusterImagePolicy.
220+
type SigstorePolicyInfo struct {
221+
MirrorRegistries []string
222+
CosignKeyData string
223+
SignedPrefix string
224+
}
225+
226+
const (
227+
clusterImagePolicyCRDName = "clusterimagepolicies.config.openshift.io"
228+
clusterImagePolicyGroup = "config.openshift.io"
229+
clusterImagePolicyKind = "ClusterImagePolicy"
230+
clusterImagePolicyV1 = "v1"
231+
clusterImagePolicyV1Alpha1 = "v1alpha1"
232+
publicKeyRootOfTrustPolicyType = "PublicKey"
233+
remapIdentityMatchPolicy = "RemapIdentity"
234+
)
235+
236+
func normalizeImageScope(scope string) string {
237+
return strings.TrimSuffix(strings.TrimSpace(scope), "/")
238+
}
239+
240+
func clusterImagePolicyScopeMatchesMirror(policyScope string, mirrorScope string) bool {
241+
policyScope = normalizeImageScope(policyScope)
242+
mirrorScope = normalizeImageScope(mirrorScope)
243+
244+
if policyScope == "" || mirrorScope == "" {
245+
return false
246+
}
247+
248+
if strings.HasPrefix(policyScope, "*.") {
249+
mirrorHostPort := strings.SplitN(mirrorScope, "/", 2)[0]
250+
mirrorHost := strings.SplitN(mirrorHostPort, ":", 2)[0]
251+
suffix := strings.TrimPrefix(policyScope, "*")
252+
return strings.HasSuffix(mirrorHost, suffix)
253+
}
254+
255+
return mirrorScope == policyScope || strings.HasPrefix(mirrorScope, policyScope+"/")
256+
}
257+
258+
func getServedClusterImagePolicyVersion(ctx context.Context, helper *helper.Helper) (string, error) {
259+
crd := &apiextensionsv1.CustomResourceDefinition{}
260+
if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: clusterImagePolicyCRDName}, crd); err != nil {
261+
if k8s_errors.IsNotFound(err) || IsNoMatchError(err) {
262+
return "", nil
263+
}
264+
return "", err
265+
}
266+
267+
for _, preferredVersion := range []string{clusterImagePolicyV1, clusterImagePolicyV1Alpha1} {
268+
for _, version := range crd.Spec.Versions {
269+
if version.Name == preferredVersion && version.Served {
270+
return preferredVersion, nil
271+
}
272+
}
273+
}
274+
275+
return "", nil
276+
}
277+
278+
func listClusterImagePolicies(
279+
ctx context.Context,
280+
helper *helper.Helper,
281+
version string,
282+
) (*unstructured.UnstructuredList, error) {
283+
policyList := &unstructured.UnstructuredList{}
284+
policyList.SetGroupVersionKind(schema.GroupVersionKind{
285+
Group: clusterImagePolicyGroup,
286+
Version: version,
287+
Kind: clusterImagePolicyKind + "List",
288+
})
289+
290+
if err := helper.GetClient().List(ctx, policyList); err != nil {
291+
if IsNoMatchError(err) {
292+
return nil, nil
293+
}
294+
return nil, err
295+
}
296+
297+
return policyList, nil
298+
}
299+
300+
// GetSigstoreImagePolicy checks if OCP has a ClusterImagePolicy configured
301+
// with sigstore signature verification for one of the mirror registries in use.
302+
// Returns policy info if a relevant policy is found, nil if no policy exists.
303+
// Returns nil without error if the ClusterImagePolicy CRD is not installed.
304+
func GetSigstoreImagePolicy(ctx context.Context, helper *helper.Helper, mirrorScopes []string) (*SigstorePolicyInfo, error) {
305+
if len(mirrorScopes) == 0 {
306+
return nil, nil
307+
}
308+
309+
version, err := getServedClusterImagePolicyVersion(ctx, helper)
310+
if err != nil {
311+
return nil, err
312+
}
313+
if version == "" {
314+
return nil, nil
315+
}
316+
317+
policyList, err := listClusterImagePolicies(ctx, helper, version)
318+
if err != nil {
319+
return nil, err
320+
}
321+
if policyList == nil {
322+
return nil, nil
323+
}
324+
325+
var matches []string
326+
var match *SigstorePolicyInfo
327+
328+
for _, policy := range policyList.Items {
329+
if policy.GetName() == "openshift" {
330+
continue
331+
}
332+
333+
policyType, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "policyType")
334+
if err != nil {
335+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s policyType: %w", policy.GetName(), err)
336+
}
337+
if !found || policyType != publicKeyRootOfTrustPolicyType {
338+
continue
339+
}
340+
341+
keyData, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "publicKey", "keyData")
342+
if err != nil {
343+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s keyData: %w", policy.GetName(), err)
344+
}
345+
if !found || len(keyData) == 0 {
346+
continue
347+
}
348+
349+
scopes, found, err := unstructured.NestedStringSlice(policy.Object, "spec", "scopes")
350+
if err != nil {
351+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s scopes: %w", policy.GetName(), err)
352+
}
353+
if !found || len(scopes) == 0 {
354+
continue
355+
}
356+
357+
signedPrefix := ""
358+
matchPolicy, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "signedIdentity", "matchPolicy")
359+
if err != nil {
360+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s matchPolicy: %w", policy.GetName(), err)
361+
}
362+
if found && matchPolicy == remapIdentityMatchPolicy {
363+
signedPrefix, _, err = unstructured.NestedString(
364+
policy.Object,
365+
"spec", "policy", "signedIdentity", "remapIdentity", "signedPrefix",
366+
)
367+
if err != nil {
368+
return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s signedPrefix: %w", policy.GetName(), err)
369+
}
370+
}
371+
372+
matchedMirrorScopes := map[string]struct{}{}
373+
for _, scope := range scopes {
374+
policyScope := normalizeImageScope(scope)
375+
if policyScope == "" {
376+
continue
377+
}
378+
379+
for _, mirrorScope := range mirrorScopes {
380+
if clusterImagePolicyScopeMatchesMirror(policyScope, mirrorScope) {
381+
matchedMirrorScopes[normalizeImageScope(mirrorScope)] = struct{}{}
382+
}
383+
}
384+
}
385+
if len(matchedMirrorScopes) == 0 {
386+
continue
387+
}
388+
389+
mirrorRegistries := make([]string, 0, len(matchedMirrorScopes))
390+
for mirrorScope := range matchedMirrorScopes {
391+
mirrorRegistries = append(mirrorRegistries, mirrorScope)
392+
}
393+
sort.Strings(mirrorRegistries)
394+
395+
matches = append(matches, fmt.Sprintf("%s (%s)", policy.GetName(), strings.Join(mirrorRegistries, ", ")))
396+
match = &SigstorePolicyInfo{
397+
MirrorRegistries: mirrorRegistries,
398+
CosignKeyData: keyData,
399+
SignedPrefix: signedPrefix,
400+
}
401+
}
402+
403+
if len(matches) > 1 {
404+
sort.Strings(matches)
405+
return nil, fmt.Errorf(
406+
"expected exactly one ClusterImagePolicy matching mirror registries, found %d: %s",
407+
len(matches), strings.Join(matches, ", "),
408+
)
409+
}
410+
411+
return match, nil
412+
}
413+
154414
// GetMirrorRegistryCACerts retrieves CA certificates from image.config.openshift.io/cluster.
155415
// Returns nil without error if:
156416
// - not on OpenShift (Image CRD doesn't exist)

0 commit comments

Comments
 (0)