Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/apis/policy/v1alpha1/clusterimagepolicy_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ func (p *Policy) ConvertTo(_ context.Context, sink *v1beta1.Policy) {
if p.IncludeTypeMeta != nil {
sink.IncludeTypeMeta = ptr.Bool(*p.IncludeTypeMeta)
}
if p.NamespaceSelector != "" {
sink.NamespaceSelector = p.NamespaceSelector
}
}

func (p *Policy) ConvertFrom(_ context.Context, source *v1beta1.Policy) {
Expand Down Expand Up @@ -205,6 +208,9 @@ func (p *Policy) ConvertFrom(_ context.Context, source *v1beta1.Policy) {
if source.IncludeTypeMeta != nil {
p.IncludeTypeMeta = ptr.Bool(*source.IncludeTypeMeta)
}
if source.NamespaceSelector != "" {
p.NamespaceSelector = source.NamespaceSelector
}
}

func (key *KeyRef) ConvertTo(_ context.Context, sink *v1beta1.KeyRef) {
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/policy/v1alpha1/clusterimagepolicy_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ func (cs *ClusterImagePolicyStatus) MarkInlinePoliciesFailed(msg string) {
cipCondSet.Manage(cs).MarkFalse(ClusterImagePolicyConditionPoliciesInlined, inlinePoliciesFailedReason, msg)
}

// MarkNamespaceValidationFailed surfaces a failure that we were unable to
// validate the namespace for the CIP.
func (cs *ClusterImagePolicyStatus) MarkNamespaceValidationFailed(msg string) {
cipCondSet.Manage(cs).MarkFalse(ClusterImagePolicyConditionPoliciesInlined, "NamespaceValidationFailed", msg)
}

// MarkInlinePoliciesdOk marks the status saying that the inlining of the
// policies had no errors.
func (cs *ClusterImagePolicyStatus) MarkInlinePoliciesOk() {
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/policy/v1alpha1/clusterimagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ type Policy struct {
// evaluated iff at least one authority matches.
// +optional
IncludeTypeMeta *bool `json:"includeTypeMeta,omitempty"`

// NamespaceSelector is a label selector used to filter namespaces for inclusion.
// For example, it allows enforcing that images can only be verified within specific namespaces,
// such as permitting "a" images only in namespace "a" and "b" images only in namespace "b".
NamespaceSelector string `json:"namespaceSelector,omitempty"`
}

// ConfigMapReference is cut&paste from SecretReference, but for the life of me
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/policy/v1beta1/clusterimagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ type Policy struct {
// evaluated iff at least one authority matches.
// +optional
IncludeTypeMeta *bool `json:"includeTypeMeta,omitempty"`

// NamespaceSelector is a label selector used to filter namespaces for inclusion.
// For example, it allows enforcing that images can only be verified within specific namespaces,
// such as permitting "a" images only in namespace "a" and "b" images only in namespace "b".
NamespaceSelector string `json:"namespaceSelector,omitempty"`
}

// MatchResource allows selecting resources based on its version, group and resource.
Expand Down
37 changes: 37 additions & 0 deletions pkg/reconciler/clusterimagepolicy/clusterimagepolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"fmt"
"io"
"net/http"
"path/filepath"
"strings"

"github.com/sigstore/policy-controller/pkg/apis/config"
Expand Down Expand Up @@ -90,6 +91,13 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, cip *v1alpha1.ClusterIma
}
cip.Status.MarkInlinePoliciesOk()

cipNserr := r.inlineNamespaces(cipCopy)
if cipNserr != nil {
r.handleCIPError(ctx, cip.Name)
cip.Status.MarkNamespaceValidationFailed(cipNserr.Error())
return cipNserr
}

webhookCIP := webhookcip.ConvertClusterImagePolicyV1alpha1ToWebhook(cipCopy)

// See if the CM holding configs exists
Expand Down Expand Up @@ -299,6 +307,35 @@ func (r *Reconciler) inlinePolicies(ctx context.Context, cip *v1alpha1.ClusterIm
return nil
}

func (r *Reconciler) inlineNamespaces(cip *v1alpha1.ClusterImagePolicy) error {
var podList *corev1.PodList
podList, err := r.kubeclient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
return fmt.Errorf("failed to list pods: %w", err)
}

for _, pod := range podList.Items {
for _, container := range pod.Spec.Containers {
image := container.Image
for _, pattern := range cip.Spec.Images {
if pattern.Glob != "" {
matched, err := filepath.Match(pattern.Glob, image)
if err != nil {
return fmt.Errorf("invalid glob pattern: %w", err)
}
if matched {
if cip.Spec.Policy.NamespaceSelector != "" && pod.Namespace != cip.Spec.Policy.NamespaceSelector {
return fmt.Errorf("image %s can only be used in the namespace %s", image, cip.Spec.Policy.NamespaceSelector)
}
return nil // If mage matches, and namespace is correct (or no namespace restriction then dont care about namespace)
}
}
}
}
}
return nil
}

func (r *Reconciler) inlinePolicyURL(ctx context.Context, policyRef *v1alpha1.Policy) error {
logging.FromContext(ctx).Infof("inlining policy url %q", policyRef.Remote.URL.String())
resp, err := http.Get(policyRef.Remote.URL.String())
Expand Down
90 changes: 89 additions & 1 deletion pkg/reconciler/clusterimagepolicy/clusterimagepolicy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strings"
"testing"

k8sfake "k8s.io/client-go/kubernetes/fake"
logtesting "knative.dev/pkg/logging/testing"

"github.com/sigstore/policy-controller/pkg/apis/config"
Expand Down Expand Up @@ -1415,7 +1416,8 @@ malformed KMS format, should be prefixed by any of the supported providers: [aws
WithMarkInlinePoliciesFailed(invalidSHAMsg),
),
}},
}}
},
}

logger := logtesting.TestLogger(t)
table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, _ configmap.Watcher) controller.Reconciler {
Expand Down Expand Up @@ -1563,3 +1565,89 @@ func patchRemoveFinalizers(namespace, name string) clientgotesting.PatchActionIm
action.Patch = []byte(patch)
return action
}

func TestInlineNamespaces(t *testing.T) {
tests := []struct {
name string
cip *v1alpha1.ClusterImagePolicy
pods []corev1.Pod
expectedErr bool
expectedErrMsg string
}{
{
name: "Image matches pattern and pod is in correct namespace",
cip: &v1alpha1.ClusterImagePolicy{
Spec: v1alpha1.ClusterImagePolicySpec{
Images: []v1alpha1.ImagePattern{{
Glob: "ghcr.io/sigstore/timestamp-server**",
}},
Policy: &v1alpha1.Policy{
NamespaceSelector: "namespace-a",
},
},
},
pods: []corev1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-a",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "ghcr.io/sigstore/timestamp-server:v1",
}},
},
},
},
expectedErr: false,
expectedErrMsg: "",
},
{
name: "Image matches pattern but pod is in wrong namespace",
cip: &v1alpha1.ClusterImagePolicy{
Spec: v1alpha1.ClusterImagePolicySpec{
Images: []v1alpha1.ImagePattern{{
Glob: "ghcr.io/sigstore/timestamp-server**",
}},
Policy: &v1alpha1.Policy{
NamespaceSelector: "namespace-a",
},
},
},
pods: []corev1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-b",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "ghcr.io/sigstore/timestamp-server:v1",
}},
},
},
},
expectedErr: true,
expectedErrMsg: "image ghcr.io/sigstore/timestamp-server:v1 can only be used in the namespace namespace-a",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := k8sfake.NewSimpleClientset(&corev1.PodList{Items: tt.pods})
r := &Reconciler{kubeclient: fakeClient}

err := r.inlineNamespaces(tt.cip)

if tt.expectedErr {
if err == nil {
t.Errorf("expected error, got nil")
} else if err.Error() != tt.expectedErrMsg {
t.Errorf("expected error message %q, got %q", tt.expectedErrMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
})
}
}