From 06284315a357402d572a132df67793e4f8b1017f Mon Sep 17 00:00:00 2001 From: Seth Malaki Date: Mon, 4 Nov 2024 21:32:08 +0000 Subject: [PATCH] wip: applicationlayerpolicy CR + audit log poc --- api/v1/applicationlayer_types.go | 47 ++++ api/v1/testdata/applicationlayerpolicy.yaml | 26 +++ api/v1/zz_generated.deepcopy.go | 157 +++++++++++++ ...or.tigera.io_applicationlayerpolicies.yaml | 217 ++++++++++++++++++ pkg/render/apiserver.go | 30 +-- pkg/render/default-audit-policy.yaml | 35 +++ 6 files changed, 488 insertions(+), 24 deletions(-) create mode 100644 api/v1/testdata/applicationlayerpolicy.yaml create mode 100644 pkg/crds/operator/operator.tigera.io_applicationlayerpolicies.yaml create mode 100644 pkg/render/default-audit-policy.yaml diff --git a/api/v1/applicationlayer_types.go b/api/v1/applicationlayer_types.go index 0c6c48875c..072e07b89e 100644 --- a/api/v1/applicationlayer_types.go +++ b/api/v1/applicationlayer_types.go @@ -149,3 +149,50 @@ type ApplicationLayerList struct { func init() { SchemeBuilder.Register(&ApplicationLayer{}, &ApplicationLayerList{}) } + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced +// ApplicationLayerPolicy is a schema for defining an application-level policy for application traffic on a namespace. +type ApplicationLayerPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ApplicationLayerPolicySpec `json:"spec,omitempty"` + Status ApplicationLayerPolicyStatus `json:"status,omitempty"` +} + +// ApplicationLayerPolicySpec defines the desired state of ApplicationLayerPolicy +type ApplicationLayerPolicySpec struct { + // LabelSelector is used to select the pods to which this policy applies. + LabelSelector metav1.LabelSelector `json:"labelSelector"` + WebApplicationFirewall *ApplicationLayerFirewall `json:"webApplicationFirewall,omitempty"` + LogCollection *LogCollectionSpec `json:"logCollection,omitempty"` + PolicyRego *ApplicationLayerPolicyConfigRego `json:"policyRego,omitempty"` +} + +type ApplicationLayerFirewall struct { + RulesetName *string `json:"rulesetName,omitempty"` + Directives []string `json:"directive,omitempty"` +} + +type ApplicationLayerPolicyConfigRego struct { + // Rego is the OPA policy to enforce. + Rego *string `json:"rego"` +} + +// ApplicationLayerPolicyStatus defines the observed state of ApplicationLayerPolicy +type ApplicationLayerPolicyStatus struct { + // State provides user-readable status. + State string `json:"state,omitempty"` + // Conditions represents the latest observed set of conditions for the component. A component may be one or more of + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// ApplicationLayerPolicyList contains a list of ApplicationLayerPolicy +type ApplicationLayerPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ApplicationLayerPolicy `json:"items"` +} diff --git a/api/v1/testdata/applicationlayerpolicy.yaml b/api/v1/testdata/applicationlayerpolicy.yaml new file mode 100644 index 0000000000..8f8abe405d --- /dev/null +++ b/api/v1/testdata/applicationlayerpolicy.yaml @@ -0,0 +1,26 @@ +kind: ApplicationLayerPolicy +apiVersion: operator.tigera.io/v1 +metadata: + name: app-rules-for-my-web-apps + namespace: default +spec: + labelSelector: app == "web" # optional, if not specified, the policy applies to all pods in this namespace + webApplicationFirewall: + - rulesetName: tigera-secure # name of the configMap containing the ruleset that's in the namespace calico-system + directives: # directive overrides appplied to the base ruleset + - SecRuleRemoveById 981176 + - SecRuleEngine On + logCollection: + logIntervalSeconds: 10 + logRequestsPerInterval: -1 + policyRego: |- + package envoy.http.public + + import rego.v1 + + default allow := false + + allow if { + input.attributes.request.http.method == "GET" + input.attributes.request.http.path == "/" + } \ No newline at end of file diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 7d91c450ec..f9e1fc75fb 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -452,6 +452,31 @@ func (in *ApplicationLayer) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerFirewall) DeepCopyInto(out *ApplicationLayerFirewall) { + *out = *in + if in.RulesetName != nil { + in, out := &in.RulesetName, &out.RulesetName + *out = new(string) + **out = **in + } + if in.Directives != nil { + in, out := &in.Directives, &out.Directives + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerFirewall. +func (in *ApplicationLayerFirewall) DeepCopy() *ApplicationLayerFirewall { + if in == nil { + return nil + } + out := new(ApplicationLayerFirewall) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApplicationLayerList) DeepCopyInto(out *ApplicationLayerList) { *out = *in @@ -484,6 +509,138 @@ func (in *ApplicationLayerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerPolicy) DeepCopyInto(out *ApplicationLayerPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerPolicy. +func (in *ApplicationLayerPolicy) DeepCopy() *ApplicationLayerPolicy { + if in == nil { + return nil + } + out := new(ApplicationLayerPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ApplicationLayerPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerPolicyConfigRego) DeepCopyInto(out *ApplicationLayerPolicyConfigRego) { + *out = *in + if in.Rego != nil { + in, out := &in.Rego, &out.Rego + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerPolicyConfigRego. +func (in *ApplicationLayerPolicyConfigRego) DeepCopy() *ApplicationLayerPolicyConfigRego { + if in == nil { + return nil + } + out := new(ApplicationLayerPolicyConfigRego) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerPolicyList) DeepCopyInto(out *ApplicationLayerPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ApplicationLayerPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerPolicyList. +func (in *ApplicationLayerPolicyList) DeepCopy() *ApplicationLayerPolicyList { + if in == nil { + return nil + } + out := new(ApplicationLayerPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ApplicationLayerPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerPolicySpec) DeepCopyInto(out *ApplicationLayerPolicySpec) { + *out = *in + in.LabelSelector.DeepCopyInto(&out.LabelSelector) + if in.WebApplicationFirewall != nil { + in, out := &in.WebApplicationFirewall, &out.WebApplicationFirewall + *out = new(ApplicationLayerFirewall) + (*in).DeepCopyInto(*out) + } + if in.LogCollection != nil { + in, out := &in.LogCollection, &out.LogCollection + *out = new(LogCollectionSpec) + (*in).DeepCopyInto(*out) + } + if in.PolicyRego != nil { + in, out := &in.PolicyRego, &out.PolicyRego + *out = new(ApplicationLayerPolicyConfigRego) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerPolicySpec. +func (in *ApplicationLayerPolicySpec) DeepCopy() *ApplicationLayerPolicySpec { + if in == nil { + return nil + } + out := new(ApplicationLayerPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApplicationLayerPolicyStatus) DeepCopyInto(out *ApplicationLayerPolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationLayerPolicyStatus. +func (in *ApplicationLayerPolicyStatus) DeepCopy() *ApplicationLayerPolicyStatus { + if in == nil { + return nil + } + out := new(ApplicationLayerPolicyStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApplicationLayerSpec) DeepCopyInto(out *ApplicationLayerSpec) { *out = *in diff --git a/pkg/crds/operator/operator.tigera.io_applicationlayerpolicies.yaml b/pkg/crds/operator/operator.tigera.io_applicationlayerpolicies.yaml new file mode 100644 index 0000000000..6a22fac32e --- /dev/null +++ b/pkg/crds/operator/operator.tigera.io_applicationlayerpolicies.yaml @@ -0,0 +1,217 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: applicationlayerpolicies.operator.tigera.io +spec: + group: operator.tigera.io + names: + kind: ApplicationLayerPolicy + listKind: ApplicationLayerPolicyList + plural: applicationlayerpolicies + singular: applicationlayerpolicy + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: ApplicationLayerPolicy is a schema for defining an application-level + policy for application traffic on a namespace. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ApplicationLayerPolicySpec defines the desired state of ApplicationLayerPolicy + properties: + labelSelector: + description: LabelSelector is used to select the pods to which this + policy applies. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + logCollection: + properties: + collectLogs: + description: |- + This setting enables or disable log collection. + Allowed values are Enabled or Disabled. + enum: + - Enabled + - Disabled + type: string + logIntervalSeconds: + description: |- + Interval in seconds for sending L7 log information for processing. + Default: 5 sec + format: int64 + type: integer + logRequestsPerInterval: + description: |- + Maximum number of unique L7 logs that are sent LogIntervalSeconds. + Adjust this to limit the number of L7 logs sent per LogIntervalSeconds + to felix for further processing, use negative number to ignore limits. + Default: -1 + format: int64 + type: integer + type: object + policyRego: + properties: + rego: + description: Rego is the OPA policy to enforce. + type: string + required: + - rego + type: object + webApplicationFirewall: + properties: + directive: + items: + type: string + type: array + rulesetName: + type: string + type: object + required: + - labelSelector + type: object + status: + description: ApplicationLayerPolicyStatus defines the observed state of + ApplicationLayerPolicy + properties: + conditions: + description: Conditions represents the latest observed set of conditions + for the component. A component may be one or more of + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + state: + description: State provides user-readable status. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/render/apiserver.go b/pkg/render/apiserver.go index 12a1666b44..8baa847782 100644 --- a/pkg/render/apiserver.go +++ b/pkg/render/apiserver.go @@ -15,6 +15,7 @@ package render import ( + _ "embed" "fmt" "strings" @@ -2063,35 +2064,16 @@ func (c *apiServerComponent) uiSettingsPassthruClusterRolebinding() *rbacv1.Clus } } +var ( + //go:embed default-audit-policy.yaml + defaultAuditPolicy string +) + // auditPolicyConfigMap returns a configmap with contents to configure audit logging for // projectcalico.org/v3 APIs. // // Calico Enterprise only func (c *apiServerComponent) auditPolicyConfigMap() *corev1.ConfigMap { - const defaultAuditPolicy = `apiVersion: audit.k8s.io/v1 -kind: Policy -rules: -- level: RequestResponse - omitStages: - - RequestReceived - verbs: - - create - - patch - - update - - delete - resources: - - group: projectcalico.org - resources: - - globalnetworkpolicies - - networkpolicies - - stagedglobalnetworkpolicies - - stagednetworkpolicies - - stagedkubernetesnetworkpolicies - - globalnetworksets - - networksets - - tiers - - hostendpoints` - return &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"}, ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/render/default-audit-policy.yaml b/pkg/render/default-audit-policy.yaml new file mode 100644 index 0000000000..fb2b8ccded --- /dev/null +++ b/pkg/render/default-audit-policy.yaml @@ -0,0 +1,35 @@ +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: +- level: RequestResponse + omitStages: + - RequestReceived + verbs: + - create + - patch + - update + - delete + resources: + - group: operator.tigera.io + resources: + - applicationlayerpolicies +- level: RequestResponse + omitStages: + - RequestReceived + verbs: + - create + - patch + - update + - delete + resources: + - group: projectcalico.org + resources: + - globalnetworkpolicies + - networkpolicies + - stagedglobalnetworkpolicies + - stagednetworkpolicies + - stagedkubernetesnetworkpolicies + - globalnetworksets + - networksets + - tiers + - hostendpoints \ No newline at end of file