Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Add ACL support for allowing cross-namespace access to image repository #162

Merged
merged 6 commits into from
Aug 9, 2021
Merged
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
2 changes: 1 addition & 1 deletion api/v1beta1/imagepolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type ImagePolicySpec struct {
// ImageRepositoryRef points at the object specifying the image
// being scanned
// +required
ImageRepositoryRef meta.LocalObjectReference `json:"imageRepositoryRef"`
ImageRepositoryRef meta.NamespacedObjectReference `json:"imageRepositoryRef"`
// Policy gives the particulars of the policy to be followed in
// selecting the most recent image
// +required
Expand Down
13 changes: 13 additions & 0 deletions api/v1beta1/imagerepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ type ImageRepositorySpec struct {
// It does not apply to already started scans. Defaults to false.
// +optional
Suspend bool `json:"suspend,omitempty"`

// AccessFrom defines an ACL for allowing cross-namespace references
// to the ImageRepository object based on the caller's namespace labels.
// +optional
AccessFrom *AccessFrom `json:"accessFrom,omitempty"`
}

type AccessFrom struct {
NamespaceSelectors []NamespaceSelector `json:"namespaceSelectors,omitempty"`
}

type NamespaceSelector struct {
MatchLabels map[string]string `json:"matchLabels,omitempty"`
}

type ScanResult struct {
Expand Down
49 changes: 49 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions config/crd/bases/image.toolkit.fluxcd.io_imagepolicies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ spec:
name:
description: Name of the referent
type: string
namespace:
description: Namespace of the referent, when not specified it acts as LocalObjectReference
type: string
required:
- name
type: object
Expand Down
13 changes: 13 additions & 0 deletions config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,19 @@ spec:
spec:
description: ImageRepositorySpec defines the parameters for scanning an image repository, e.g., `fluxcd/flux`.
properties:
accessFrom:
description: AccessFrom defines an ACL for allowing cross-namespace references to the ImageRepository object based on the caller's namespace labels.
properties:
namespaceSelectors:
items:
properties:
matchLabels:
additionalProperties:
type: string
type: object
type: object
type: array
type: object
certSecretRef:
description: "CertSecretRef can be given the name of a secret containing either or both of \n - a PEM-encoded client certificate (`certFile`) and private key (`keyFile`); - a PEM-encoded CA certificate (`caFile`) \n and whichever are supplied, will be used for connecting to the registry. The client cert and key are useful if you are authenticating with a certificate; the CA cert is useful if you are using a self-signed server certificate."
properties:
Expand Down
16 changes: 16 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ metadata:
creationTimestamp: null
name: manager-role
rules:
- apiGroups:
- ""
resources:
- namespaces
- secrets
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- image.toolkit.fluxcd.io
resources:
Expand Down
4 changes: 4 additions & 0 deletions config/samples/image_v1beta1_imagerepository.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ metadata:
spec:
image: ghcr.io/stefanprodan/podinfo
interval: 1m0s
accessFrom:
namespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: flux-system
61 changes: 59 additions & 2 deletions controllers/imagepolicy_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import (
"time"

"github.com/go-logr/logr"
v1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record"
Expand Down Expand Up @@ -65,6 +67,8 @@ type ImagePolicyReconcilerOptions struct {
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagepolicies,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagepolicies/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagerepositories,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reconcileStart := time.Now()
Expand All @@ -87,10 +91,14 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
defer r.recordReadinessMetric(ctx, &pol)

var repo imagev1.ImageRepository
if err := r.Get(ctx, types.NamespacedName{
repoNamespacedName := types.NamespacedName{
Namespace: pol.Namespace,
Name: pol.Spec.ImageRepositoryRef.Name,
}, &repo); err != nil {
}
if pol.Spec.ImageRepositoryRef.Namespace != "" {
repoNamespacedName.Namespace = pol.Spec.ImageRepositoryRef.Namespace
}
if err := r.Get(ctx, repoNamespacedName, &repo); err != nil {
if client.IgnoreNotFound(err) == nil {
imagev1.SetImagePolicyReadiness(
&pol,
Expand All @@ -107,6 +115,21 @@ func (r *ImagePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

// check if we are allowed to use the referenced ImageRepository
if _, err := r.hasAccessToRepository(ctx, req, pol.Spec.ImageRepositoryRef, repo.Spec.AccessFrom); err != nil {
imagev1.SetImagePolicyReadiness(
&pol,
metav1.ConditionFalse,
"AccessDenied",
squaremo marked this conversation as resolved.
Show resolved Hide resolved
err.Error(),
)
if err := r.patchStatus(ctx, req, pol.Status); err != nil {
return ctrl.Result{Requeue: true}, err
}
log.Error(err, "access denied")
return ctrl.Result{}, nil
}

// if the image repo hasn't been scanned, don't bother
if repo.Status.CanonicalImageName == "" {
msg := "referenced ImageRepository has not been scanned yet"
Expand Down Expand Up @@ -284,3 +307,37 @@ func (r *ImagePolicyReconciler) patchStatus(ctx context.Context, req ctrl.Reques

return r.Status().Patch(ctx, &res, patch)
}

func (r *ImagePolicyReconciler) hasAccessToRepository(ctx context.Context, policy ctrl.Request, repo meta.NamespacedObjectReference, acl *imagev1.AccessFrom) (bool, error) {
// grant access if the policy is in the same namespace as the repository
if repo.Namespace == "" || policy.Namespace == repo.Namespace {
return true, nil
}

// deny access if the repository has no ACL defined
if acl == nil {
return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to missing access list",
repo.Namespace, repo.Name)
}

// get the policy namespace labels
var policyNamespace v1.Namespace
if err := r.Get(ctx, types.NamespacedName{Name: policy.Namespace}, &policyNamespace); err != nil {
return false, err
}
policyLabels := policyNamespace.GetLabels()

stefanprodan marked this conversation as resolved.
Show resolved Hide resolved
// check if the policy namespace labels match any ACL
for _, selector := range acl.NamespaceSelectors {
sel, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: selector.MatchLabels})
if err != nil {
return false, err
}
if sel.Matches(labels.Set(policyLabels)) {
return true, nil
}
}

return false, fmt.Errorf("ImageRepository '%s/%s' can't be accessed due to labels mismatch on namespace '%s'",
repo.Namespace, repo.Name, policy.Namespace)
}
2 changes: 2 additions & 0 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ type dockerConfig struct {

// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagerepositories,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=image.toolkit.fluxcd.io,resources=imagerepositories/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch

func (r *ImageRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reconcileStart := time.Now()
Expand Down
Loading