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

Add policy-controller annotations #732

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
15 changes: 11 additions & 4 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@
}

func NewMutatingAdmissionController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
store := config.NewStore(logging.FromContext(ctx).Named("config-store"))
store.WatchConfigs(cmw)
policyControllerConfigStore := policycontrollerconfig.NewStore(logging.FromContext(ctx).Named("config-policy-controller"))
hectorj2f marked this conversation as resolved.
Show resolved Hide resolved
policyControllerConfigStore.WatchConfigs(cmw)

Check warning on line 209 in cmd/webhook/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/webhook/main.go#L205-L209

Added lines #L205 - L209 were not covered by tests
kc := kubeclient.Get(ctx)
validator := cwebhook.NewValidator(ctx)

Expand All @@ -218,10 +223,12 @@
// A function that infuses the context passed to Validate/SetDefaults with custom metadata.
func(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, kubeclient.Key{}, kc)
ctx = policyduckv1beta1.WithPodScalableDefaulter(ctx, validator.ResolvePodScalable)
ctx = duckv1.WithPodDefaulter(ctx, validator.ResolvePod)
ctx = duckv1.WithPodSpecDefaulter(ctx, validator.ResolvePodSpecable)
ctx = duckv1.WithCronJobDefaulter(ctx, validator.ResolveCronJob)
ctx = store.ToContext(ctx)
ctx = policyControllerConfigStore.ToContext(ctx)
ctx = policyduckv1beta1.WithPodScalableDefaulter(ctx, validator.PodScalableDefaulter)
ctx = duckv1.WithPodDefaulter(ctx, validator.PodDefaulter)
hectorj2f marked this conversation as resolved.
Show resolved Hide resolved
ctx = duckv1.WithPodSpecDefaulter(ctx, validator.PodSpecableDefaulter)
ctx = duckv1.WithCronJobDefaulter(ctx, validator.CronJobDefaulter)

Check warning on line 231 in cmd/webhook/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/webhook/main.go#L226-L231

Added lines #L226 - L231 were not covered by tests
return ctx
},

Expand Down
344 changes: 344 additions & 0 deletions pkg/webhook/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,30 @@
v.resolvePodSpec(ctx, &wp.Spec.Template.Spec, opt)
}

// PodDefaulter implements duckv1.PodValidator
func (v *Validator) PodDefaulter(ctx context.Context, p *duckv1.Pod) {
v.ResolvePod(ctx, p)
v.AnnotatePod(ctx, p)

Check warning on line 967 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L965-L967

Added lines #L965 - L967 were not covered by tests
}

// PodSpecableDefaulter implements duckv1.PodSpecValidator
func (v *Validator) PodSpecableDefaulter(ctx context.Context, wp *duckv1.WithPod) {
v.ResolvePodSpecable(ctx, wp)
v.AnnotatePodSpecable(ctx, wp)

Check warning on line 973 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L971-L973

Added lines #L971 - L973 were not covered by tests
}

// PodScalableDefaulter implements policyduckv1beta1.PodScalableValidator
func (v *Validator) PodScalableDefaulter(ctx context.Context, ps *policyduckv1beta1.PodScalable) {
v.ResolvePodScalable(ctx, ps)
v.AnnotatePodScalable(ctx, ps)

Check warning on line 979 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L977-L979

Added lines #L977 - L979 were not covered by tests
}

// CronJobDefaulter implements duckv1.CronJobValidator
func (v *Validator) CronJobDefaulter(ctx context.Context, c *duckv1.CronJob) {
v.ResolveCronJob(ctx, c)
v.AnnotateCronJob(ctx, c)

Check warning on line 985 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L983-L985

Added lines #L983 - L985 were not covered by tests
}

// ResolvePod implements duckv1.PodValidator
func (v *Validator) ResolvePod(ctx context.Context, p *duckv1.Pod) {
// Don't mess with things that are being deleted or already deleted or
Expand Down Expand Up @@ -1063,6 +1087,326 @@
resolveEphemeralContainers(ps.EphemeralContainers)
}

const ResultsAnnotationKey = "policy.sigstore.dev/policy-controller-results"

// AnnotatePod implements duckv1.PodValidator
func (v *Validator) AnnotatePod(ctx context.Context, p *duckv1.Pod) {
// Don't mess with things that are being deleted or already deleted or
// status update.
if isDeletedOrStatusUpdate(ctx, p.DeletionTimestamp) {
return
}

// Attach the spec/metadata for down the line to be attached if it's
// required by policy to be included in the PolicyResult.
ctx = IncludeSpec(ctx, p.Spec)
ctx = IncludeObjectMeta(ctx, p.ObjectMeta)

imagePullSecrets := make([]string, 0, len(p.Spec.ImagePullSecrets))
for _, s := range p.Spec.ImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, s.Name)
}

Check warning on line 1108 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1107-L1108

Added lines #L1107 - L1108 were not covered by tests

ns := getNamespace(ctx, p.Namespace)
opt := k8schain.Options{
Namespace: ns,
ServiceAccountName: p.Spec.ServiceAccountName,
ImagePullSecrets: imagePullSecrets,
}

v.annotatePodSpec(ctx, ns, p.Kind, p.APIVersion, &p.ObjectMeta, &p.Spec, opt)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could get from the context whether annotate-results- configuration is enabled or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I am not sure what you are referring to. I make the check from the context in one place in line 1206.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! This comment was related to this other one #732 (comment). I believe we could simply get from the context whether our system had enabled the annotations on resources or not.That'd reduce the amount of code for this feature while keeping the behaviour configurable.

}

func (v *Validator) AnnotatePodSpecable(ctx context.Context, wp *duckv1.WithPod) {
// Don't mess with things that are being deleted or already deleted or
// status update.
if isDeletedOrStatusUpdate(ctx, wp.DeletionTimestamp) {
return
}

// Attach the spec/metadata for down the line to be attached if it's
// required by policy to be included in the PolicyResult.
ctx = IncludeSpec(ctx, wp.Spec)
ctx = IncludeObjectMeta(ctx, wp.ObjectMeta)
ctx = IncludeTypeMeta(ctx, wp.TypeMeta)

imagePullSecrets := make([]string, 0, len(wp.Spec.Template.Spec.ImagePullSecrets))
for _, s := range wp.Spec.Template.Spec.ImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, s.Name)
}

Check warning on line 1136 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1135-L1136

Added lines #L1135 - L1136 were not covered by tests
ns := getNamespace(ctx, wp.Namespace)
opt := k8schain.Options{
Namespace: ns,
ServiceAccountName: wp.Spec.Template.Spec.ServiceAccountName,
ImagePullSecrets: imagePullSecrets,
}

v.annotatePodSpec(ctx, ns, wp.Kind, wp.APIVersion, &wp.ObjectMeta, &wp.Spec.Template.Spec, opt)
}

func (v *Validator) AnnotatePodScalable(ctx context.Context, ps *policyduckv1beta1.PodScalable) {
// If we are deleting (or already deleted) or updating status, don't block.
if isDeletedOrStatusUpdate(ctx, ps.DeletionTimestamp) {
return
}

// If we are being scaled down don't block it.
if ps.IsScalingDown(ctx) {
logging.FromContext(ctx).Debugf("Skipping annotations due to scale down request %s/%s", &ps.ObjectMeta.Name, &ps.ObjectMeta.Namespace)
return
}

// Attach the spec for down the line to be attached if it's required by
// policy to be included in the PolicyResult.
ctx = IncludeSpec(ctx, ps.Spec)
ctx = IncludeObjectMeta(ctx, ps.ObjectMeta)
ctx = IncludeTypeMeta(ctx, ps.TypeMeta)

imagePullSecrets := make([]string, 0, len(ps.Spec.Template.Spec.ImagePullSecrets))
for _, s := range ps.Spec.Template.Spec.ImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, s.Name)
}

Check warning on line 1168 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1167-L1168

Added lines #L1167 - L1168 were not covered by tests
ns := getNamespace(ctx, ps.Namespace)
opt := k8schain.Options{
Namespace: ns,
ServiceAccountName: ps.Spec.Template.Spec.ServiceAccountName,
ImagePullSecrets: imagePullSecrets,
}

v.annotatePodSpec(ctx, ns, ps.Kind, ps.APIVersion, &ps.ObjectMeta, &ps.Spec.Template.Spec, opt)
}

func (v *Validator) AnnotateCronJob(ctx context.Context, c *duckv1.CronJob) {
// If we are deleting (or already deleted) or updating status, don't block.
if isDeletedOrStatusUpdate(ctx, c.DeletionTimestamp) {
return
}

// Attach the spec/metadata for down the line to be attached if it's
// required by policy to be included in the PolicyResult.
ctx = IncludeSpec(ctx, c.Spec)
ctx = IncludeObjectMeta(ctx, c.ObjectMeta)
ctx = IncludeTypeMeta(ctx, c.TypeMeta)

imagePullSecrets := make([]string, 0, len(c.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets))
for _, s := range c.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets {
imagePullSecrets = append(imagePullSecrets, s.Name)
}
ns := getNamespace(ctx, c.Namespace)
opt := k8schain.Options{
Namespace: ns,
ServiceAccountName: c.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName,
ImagePullSecrets: imagePullSecrets,
}

v.annotatePodSpec(ctx, ns, c.Kind, c.APIVersion, &c.ObjectMeta, &c.Spec.JobTemplate.Spec.Template.Spec, opt)
}

func (v *Validator) annotatePodSpec(ctx context.Context, namespace, kind, apiVersion string, objectMeta *metav1.ObjectMeta, ps *corev1.PodSpec, opt k8schain.Options) {
kc, err := k8schain.New(ctx, kubeclient.Get(ctx), opt)
if err != nil {
logging.FromContext(ctx).Warnf("Unable to build k8schain: %v", err)
return
}

Check warning on line 1210 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1208-L1210

Added lines #L1208 - L1210 were not covered by tests

labels := objectMeta.Labels
annotations := make([]*ContainerAnnotation, 0)

checkContainers := func(cs []corev1.Container, field string) {
results := make(chan *ContainerAnnotation, len(cs))
wg := new(sync.WaitGroup)
for i, c := range cs {
i := i
c := c
wg.Add(1)
go func() {
defer wg.Done()

// Require digests, otherwise the validation is meaningless
// since the tag can move.
fe := refOrFieldError(c.Image, field, i)
if fe != nil {
results <- &ContainerAnnotation{
Index: i,
Name: c.Name,
Image: c.Image,
Field: field,
Result: fe.Message,
}
return
}

containerAnnotation := v.generateContainerImageAnnotation(ctx, c.Image, namespace, c.Name, field, i, kind, apiVersion, labels, kc, ociremote.WithRemoteOptions(
remote.WithContext(ctx),
remote.WithAuthFromKeychain(kc),
))
results <- containerAnnotation
}()
}
for i := 0; i < len(cs); i++ {
select {
case <-ctx.Done():
logging.FromContext(ctx).Warnf("context was canceled before annotations completed")

Check warning on line 1249 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1248-L1249

Added lines #L1248 - L1249 were not covered by tests
case result, ok := <-results:
if !ok {
logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result")

Check warning on line 1252 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1252

Added line #L1252 was not covered by tests
} else if result != nil {
annotations = append(annotations, result)
}
}
}
wg.Wait()
}

checkEphemeralContainers := func(cs []corev1.EphemeralContainer, field string) {
results := make(chan *ContainerAnnotation, len(cs))
wg := new(sync.WaitGroup)
for i, c := range cs {
i := i
c := c
wg.Add(1)
go func() {
defer wg.Done()

// Require digests, otherwise the validation is meaningless
// since the tag can move.
fe := refOrFieldError(c.Image, field, i)
if fe != nil {
results <- &ContainerAnnotation{
Index: i,
Name: c.Name,
Image: c.Image,
Field: field,
Result: fe.Message,
}
return
}

Check warning on line 1283 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1265-L1283

Added lines #L1265 - L1283 were not covered by tests

containerAnnotation := v.generateContainerImageAnnotation(ctx, c.Image, namespace, c.Name, field, i, kind, apiVersion, labels, kc, ociremote.WithRemoteOptions(
remote.WithContext(ctx),
remote.WithAuthFromKeychain(kc),
))
results <- containerAnnotation

Check warning on line 1289 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1285-L1289

Added lines #L1285 - L1289 were not covered by tests
}()
}
for i := 0; i < len(cs); i++ {
select {
case <-ctx.Done():
logging.FromContext(ctx).Warnf("context was canceled before annotations completed")
case result, ok := <-results:
if !ok {
logging.FromContext(ctx).Warnf("Annotation results channel failed to produce a result")
} else if result != nil {
annotations = append(annotations, result)
}

Check warning on line 1301 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1293-L1301

Added lines #L1293 - L1301 were not covered by tests
}
}
wg.Wait()
}

checkContainers(ps.InitContainers, "initContainers")
checkContainers(ps.Containers, "containers")
checkEphemeralContainers(ps.EphemeralContainers, "ephemeralContainers")
resultAnnotations := ResultAnnotations{
ContainerResults: annotations,
}

annotationBytes, err := json.Marshal(resultAnnotations)
if err != nil {
logging.FromContext(ctx).Warnf("Unable to marshal annotatios: %v", err)
return
}

Check warning on line 1318 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1316-L1318

Added lines #L1316 - L1318 were not covered by tests
if objectMeta.Annotations == nil {
objectMeta.Annotations = make(map[string]string)
}
objectMeta.Annotations[ResultsAnnotationKey] = string(annotationBytes)
}

// ResultAnnotations is a list of ContainerAnnotations that will be added to
// the resource during the mutation phase
type ResultAnnotations struct {
ContainerResults []*ContainerAnnotation `json:"containerResults"`
}

// ContainerAnnotation stores the results of the validations so the
// users can see which policies were evaluated for each container
type ContainerAnnotation struct {
Index int `json:"index"`
Name string `json:"name"`
Image string `json:"image"`
Field string `json:"field"`
Result string `json:"result"`
ResultMsg string `json:"resultMsg"`
PolicyResults map[string]*PolicyResult `json:"policyResults,omitempty"`
PolicyErrors map[string][]string `json:"policyErrors,omitempty"`
}

func (v *Validator) generateContainerImageAnnotation(ctx context.Context, containerImage string, namespace, containerName string, field string, index int, kind, apiVersion string, labels map[string]string, kc authn.Keychain, ociRemoteOpts ...ociremote.Option) *ContainerAnnotation {
annotation := &ContainerAnnotation{
Index: index,
Name: containerName,
Image: containerImage,
Field: field,
Result: "deny",
PolicyResults: make(map[string]*PolicyResult),
PolicyErrors: make(map[string][]string),
}
ref, err := name.ParseReference(containerImage)
if err != nil {
annotation.ResultMsg = err.Error()
return annotation
}

Check warning on line 1358 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1356-L1358

Added lines #L1356 - L1358 were not covered by tests
config := config.FromContext(ctx)

if config != nil {
policies, err := config.ImagePolicyConfig.GetMatchingPolicies(ref.Name(), kind, apiVersion, labels)
if err != nil {
annotation.ResultMsg = err.Error()
return annotation
}

Check warning on line 1366 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1364-L1366

Added lines #L1364 - L1366 were not covered by tests

// If there is at least one policy that matches, that means it
// has to be satisfied.
if len(policies) > 0 {
signatures, fieldErrors := validatePolicies(ctx, namespace, ref, policies, kc, ociRemoteOpts...)
annotation.PolicyResults = signatures
for failingPolicy, policyErrs := range fieldErrors {
for _, policyErr := range policyErrs {
var fe *apis.FieldError
if errors.As(policyErr, &fe) {
if fe.Filter(apis.WarningLevel) != nil {
annotation.Result = "warn"
}
annotation.PolicyErrors[failingPolicy] = append(annotation.PolicyErrors[failingPolicy], strings.Trim(fe.Message, "\n"))
} else {
annotation.PolicyErrors[failingPolicy] = append(annotation.PolicyErrors[failingPolicy], strings.Trim(policyErr.Error(), "\n"))
}

Check warning on line 1383 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1381-L1383

Added lines #L1381 - L1383 were not covered by tests
}
}
if len(signatures) != len(policies) {
annotation.ResultMsg = fmt.Sprintf("Failed to validate at least one policy for %s wanted %d policies, only validated %d", ref.Name(), len(policies), len(signatures))
} else {
annotation.ResultMsg = fmt.Sprintf("Validated %d policies for image %s", len(signatures), containerImage)
annotation.Result = "allow"
}
return annotation
}

// Container matched no policies
noMatchingError := setNoMatchingPoliciesError(ctx, containerImage, field, index)
if noMatchingError != nil {
annotation.ResultMsg = noMatchingError.Message
} else {
annotation.ResultMsg = fmt.Sprintf("No matching policies for %s", containerImage)
annotation.Result = "allow"
}

Check warning on line 1402 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1396-L1402

Added lines #L1396 - L1402 were not covered by tests

return annotation

Check warning on line 1404 in pkg/webhook/validator.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/validator.go#L1404

Added line #L1404 was not covered by tests
}

return nil
}

// getNamespace tries to extract the namespace from the HTTPRequest
// if the namespace passed as argument is empty. This is a workaround
// for a bug in k8s <= 1.24.
Expand Down