From c1a1430d1a524ff6a74ebf3a5127d276b5a3b26a Mon Sep 17 00:00:00 2001 From: Oliver Gould Date: Wed, 30 Mar 2022 12:26:45 -0700 Subject: [PATCH] Introduce AuthorizationPolicy CRDs (#8007) Issue #7709 proposes new Custom Resource types to support generalized authorization policies: - `AuthorizationPolicy` - `MeshTLSAuthentication` - `NetworkAuthentication` This change introduces these CRDs to the default linkerd installation (via the `linkerd-crds` chart) and updates the policy controller's to handle these resource types. The policy admission controller validates that these resource reference only suppported types. This new functionality is tested at multiple levels: * `linkerd-policy-controller-k8s-index` includes unit tests for the indexer to test how events update the index; * `linkerd-policy-test` includes integration tests that run in-cluster to validate that the gRPC API updates as resources are manipulated; * `linkerd-policy-test` includes integration tests that exercise the admission controller's resource validation; and * `linkerd-policy-test` includes integration tests that ensure that proxies honor authorization resources. This change does NOT update Linkerd's control plane and extensions to use these new authorization primitives. Furthermore, the `linkerd` CLI does not yet support inspecting these new resource types. These enhancements will be made in followup changes. Signed-off-by: Oliver Gould --- Cargo.lock | 2 + .../templates/destination-rbac.yaml | 6 + .../policy/authorization-policy.yaml | 99 +++ .../policy/meshtls-authentication.yaml | 86 +++ .../policy/network-authentication.yaml | 53 ++ cli/cmd/install.go | 3 + ...install_controlplane_tracing_output.golden | 244 ++++++ cli/cmd/testdata/install_custom_domain.golden | 244 ++++++ .../testdata/install_custom_registry.golden | 244 ++++++ cli/cmd/testdata/install_default.golden | 244 ++++++ ...stall_default_override_dst_get_nets.golden | 244 ++++++ cli/cmd/testdata/install_default_token.golden | 244 ++++++ cli/cmd/testdata/install_ha_output.golden | 244 ++++++ .../install_ha_with_overrides_output.golden | 244 ++++++ .../install_heartbeat_disabled_output.golden | 244 ++++++ .../install_helm_control_plane_output.golden | 6 + ...nstall_helm_control_plane_output_ha.golden | 6 + .../testdata/install_helm_crds_output.golden | 244 ++++++ .../install_helm_crds_output_ha.golden | 244 ++++++ .../install_helm_output_ha_labels.golden | 6 + ...l_helm_output_ha_namespace_selector.golden | 6 + .../testdata/install_no_init_container.golden | 244 ++++++ cli/cmd/testdata/install_output.golden | 244 ++++++ cli/cmd/testdata/install_proxy_ignores.golden | 244 ++++++ cli/cmd/testdata/install_values_file.golden | 244 ++++++ policy-controller/core/src/lib.rs | 20 +- policy-controller/grpc/Cargo.toml | 1 + policy-controller/grpc/src/lib.rs | 38 +- policy-controller/k8s/api/src/lib.rs | 8 +- policy-controller/k8s/api/src/policy.rs | 10 +- .../api/src/policy/authorization_policy.rs | 22 + .../api/src/policy/meshtls_authentication.rs | 23 + .../api/src/policy/network_authentication.rs | 22 + .../k8s/api/src/policy/target_ref.rs | 351 +++++++++ policy-controller/k8s/index/Cargo.toml | 1 + .../k8s/index/src/authorization_policy.rs | 85 +++ policy-controller/k8s/index/src/index.rs | 720 +++++++++++++++--- policy-controller/k8s/index/src/lib.rs | 3 + .../k8s/index/src/meshtls_authentication.rs | 46 ++ .../k8s/index/src/network_authentication.rs | 28 + policy-controller/k8s/index/src/tests.rs | 409 +--------- .../k8s/index/src/tests/annotation.rs | 132 ++++ .../index/src/tests/authorization_policy.rs | 158 ++++ .../k8s/index/src/tests/server.rs | 127 +++ .../index/src/tests/server_authorization.rs | 104 +++ policy-controller/src/admission.rs | 113 ++- policy-controller/src/main.rs | 21 + policy-test/src/curl.rs | 15 +- policy-test/src/grpc.rs | 8 +- .../tests/admit_authorization_policy.rs | 212 ++++++ .../tests/admit_meshtls_authentication.rs | 74 ++ .../tests/admit_network_authentication.rs | 142 ++++ policy-test/tests/api.rs | 142 +++- policy-test/tests/e2e_authorization_policy.rs | 375 +++++++++ 54 files changed, 6833 insertions(+), 510 deletions(-) create mode 100644 charts/linkerd-crds/templates/policy/authorization-policy.yaml create mode 100644 charts/linkerd-crds/templates/policy/meshtls-authentication.yaml create mode 100644 charts/linkerd-crds/templates/policy/network-authentication.yaml create mode 100644 policy-controller/k8s/api/src/policy/authorization_policy.rs create mode 100644 policy-controller/k8s/api/src/policy/meshtls_authentication.rs create mode 100644 policy-controller/k8s/api/src/policy/network_authentication.rs create mode 100644 policy-controller/k8s/api/src/policy/target_ref.rs create mode 100644 policy-controller/k8s/index/src/authorization_policy.rs create mode 100644 policy-controller/k8s/index/src/meshtls_authentication.rs create mode 100644 policy-controller/k8s/index/src/network_authentication.rs create mode 100644 policy-controller/k8s/index/src/tests/annotation.rs create mode 100644 policy-controller/k8s/index/src/tests/authorization_policy.rs create mode 100644 policy-controller/k8s/index/src/tests/server.rs create mode 100644 policy-controller/k8s/index/src/tests/server_authorization.rs create mode 100644 policy-test/tests/admit_authorization_policy.rs create mode 100644 policy-test/tests/admit_meshtls_authentication.rs create mode 100644 policy-test/tests/admit_network_authentication.rs create mode 100644 policy-test/tests/e2e_authorization_policy.rs diff --git a/Cargo.lock b/Cargo.lock index 53c92f4d2d362..1ac9fe5601ab6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,7 @@ dependencies = [ "futures", "linkerd-policy-controller-core", "linkerd2-proxy-api", + "maplit", "tokio", "tonic", "tracing", @@ -989,6 +990,7 @@ dependencies = [ "kubert", "linkerd-policy-controller-core", "linkerd-policy-controller-k8s-api", + "maplit", "parking_lot", "tokio", "tokio-stream", diff --git a/charts/linkerd-control-plane/templates/destination-rbac.yaml b/charts/linkerd-control-plane/templates/destination-rbac.yaml index eba642aafd4ba..a454b71e1b55f 100644 --- a/charts/linkerd-control-plane/templates/destination-rbac.yaml +++ b/charts/linkerd-control-plane/templates/destination-rbac.yaml @@ -166,6 +166,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -190,6 +193,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/charts/linkerd-crds/templates/policy/authorization-policy.yaml b/charts/linkerd-crds/templates/policy/authorization-policy.yaml new file mode 100644 index 0000000000000..fb3f155d51ace --- /dev/null +++ b/charts/linkerd-crds/templates/policy/authorization-policy.yaml @@ -0,0 +1,99 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml b/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml new file mode 100644 index 0000000000000..67c723eb4f367 --- /dev/null +++ b/charts/linkerd-crds/templates/policy/meshtls-authentication.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string diff --git a/charts/linkerd-crds/templates/policy/network-authentication.yaml b/charts/linkerd-crds/templates/policy/network-authentication.yaml new file mode 100644 index 0000000000000..defa9d7ac7433 --- /dev/null +++ b/charts/linkerd-crds/templates/policy/network-authentication.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + {{ include "partials.annotations.created-by" . }} + labels: + helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} + linkerd.io/control-plane-ns: {{.Release.Namespace}} +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string diff --git a/cli/cmd/install.go b/cli/cmd/install.go index ae88fcff273c2..795a61df8f0b7 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -61,6 +61,9 @@ Otherwise, you can use the --ignore-cluster flag to overwrite the existing globa var ( templatesCrdFiles = []string{ + "templates/policy/authorization-policy.yaml", + "templates/policy/meshtls-authentication.yaml", + "templates/policy/network-authentication.yaml", "templates/policy/server.yaml", "templates/policy/server-authorization.yaml", "templates/serviceprofile.yaml", diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden index ee1bcbe73aac3..9858056e861e2 100644 --- a/cli/cmd/testdata/install_controlplane_tracing_output.golden +++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden index 5bbf4ebdae3f3..fd769c704d292 100644 --- a/cli/cmd/testdata/install_custom_domain.golden +++ b/cli/cmd/testdata/install_custom_domain.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden index e551fea639f65..ede50b13fd2ac 100644 --- a/cli/cmd/testdata/install_custom_registry.golden +++ b/cli/cmd/testdata/install_custom_registry.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden index 5bbf4ebdae3f3..fd769c704d292 100644 --- a/cli/cmd/testdata/install_default.golden +++ b/cli/cmd/testdata/install_default.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden index bd5cd1c3fd69f..fae2859c7e102 100644 --- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden +++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_default_token.golden b/cli/cmd/testdata/install_default_token.golden index df8ddbb739ce8..cb2d2c217d13b 100644 --- a/cli/cmd/testdata/install_default_token.golden +++ b/cli/cmd/testdata/install_default_token.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden index da2a91d05e7f0..f987e5e9592fb 100644 --- a/cli/cmd/testdata/install_ha_output.golden +++ b/cli/cmd/testdata/install_ha_output.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden index ae8db9063ab21..e8d1f4943141d 100644 --- a/cli/cmd/testdata/install_ha_with_overrides_output.golden +++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden index e80ff8e457f68..cf5c40ee0701a 100644 --- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden +++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_helm_control_plane_output.golden b/cli/cmd/testdata/install_helm_control_plane_output.golden index 95df215b9b5e6..336a660c5d5b7 100644 --- a/cli/cmd/testdata/install_helm_control_plane_output.golden +++ b/cli/cmd/testdata/install_helm_control_plane_output.golden @@ -161,6 +161,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -185,6 +188,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden index c15f1b0ec46aa..6e4c69b132c9e 100644 --- a/cli/cmd/testdata/install_helm_control_plane_output_ha.golden +++ b/cli/cmd/testdata/install_helm_control_plane_output_ha.golden @@ -161,6 +161,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -185,6 +188,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_helm_crds_output.golden b/cli/cmd/testdata/install_helm_crds_output.golden index 4e970a2bf27ea..eff492d652fc2 100644 --- a/cli/cmd/testdata/install_helm_crds_output.golden +++ b/cli/cmd/testdata/install_helm_crds_output.golden @@ -1,4 +1,248 @@ --- +# Source: linkerd-crds/templates/policy/authorization-policy.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +# Source: linkerd-crds/templates/policy/meshtls-authentication.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +# Source: linkerd-crds/templates/policy/network-authentication.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- # Source: linkerd-crds/templates/policy/server.yaml --- apiVersion: apiextensions.k8s.io/v1 diff --git a/cli/cmd/testdata/install_helm_crds_output_ha.golden b/cli/cmd/testdata/install_helm_crds_output_ha.golden index 4e970a2bf27ea..eff492d652fc2 100644 --- a/cli/cmd/testdata/install_helm_crds_output_ha.golden +++ b/cli/cmd/testdata/install_helm_crds_output_ha.golden @@ -1,4 +1,248 @@ --- +# Source: linkerd-crds/templates/policy/authorization-policy.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +# Source: linkerd-crds/templates/policy/meshtls-authentication.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +# Source: linkerd-crds/templates/policy/network-authentication.yaml +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/helm linkerd-version + labels: + helm.sh/chart: linkerd-crds- + linkerd.io/control-plane-ns: linkerd-dev +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- # Source: linkerd-crds/templates/policy/server.yaml --- apiVersion: apiextensions.k8s.io/v1 diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden index 2381dfc72075a..806fb488142ee 100644 --- a/cli/cmd/testdata/install_helm_output_ha_labels.golden +++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden @@ -161,6 +161,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -185,6 +188,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden index f44840aa0f709..d80cb19c3ebe9 100644 --- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden +++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden @@ -161,6 +161,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -185,6 +188,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden index 8089e2e85ca75..69dd161ef0402 100644 --- a/cli/cmd/testdata/install_no_init_container.golden +++ b/cli/cmd/testdata/install_no_init_container.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden index 730fd5e9fe300..fa4689acdcdcb 100644 --- a/cli/cmd/testdata/install_output.golden +++ b/cli/cmd/testdata/install_output.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: CliVersion + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: CliVersion + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: CliVersion + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -856,6 +1094,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -880,6 +1121,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden index 15eaafab7ea89..0b4eed81ca161 100644 --- a/cli/cmd/testdata/install_proxy_ignores.golden +++ b/cli/cmd/testdata/install_proxy_ignores.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden index 68c6bb242be31..929b403150ee9 100644 --- a/cli/cmd/testdata/install_values_file.golden +++ b/cli/cmd/testdata/install_values_file.golden @@ -1,6 +1,244 @@ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: authorizationpolicies.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: AuthorizationPolicy + plural: authorizationpolicies + singular: authorizationpolicy + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + Authorizes clients to communicate with Linkerd-proxied server + resources. + type: object + required: [targetRef, requiredAuthenticationRefs] + properties: + targetRef: + description: >- + TargetRef references a resource to which the authorization + policy applies. + type: object + required: [kind, name] + # Modified from the gateway API. + # Copyright 2020 The Kubernetes Authors + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + requiredAuthenticationRefs: + description: >- + RequiredAuthenticationRefs enumerates a set of required + authentications. ALL authentications must be satisfied for + the authorization to apply. If any of the referred objects + cannot be found, the authorization will be ignored. + type: array + items: + type: object + required: [kind, name] + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: meshtlsauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: MeshTLSAuthentication + plural: meshtlsauthentications + singular: meshtlsauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + MeshTLSAuthentication defines a list of authenticated client IDs + to be referenced by an `AuthorizationPolicy`. If a client + connection has the mutually-authenticated identity that matches + ANY of the of the provided identities, the connection is + considered authenticated. + type: object + oneOf: + - required: [identities] + - required: [identityRefs] + properties: + identities: + description: >- + Authorizes clients with the provided proxy identity strings + (as provided via MTLS) + + The `*` prefix can be used to match all identities in + a domain. An identity string of `*` indicates that + all authentication clients are authorized. + type: array + items: + type: string + pattern: '^(\*|[a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + identityRefs: + type: array + items: + type: object + required: + - kind + properties: + group: + description: >- + Group is the group of the referent. When empty, the + Kubernetes core API group is inferred." + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: >- + Kind is the kind of the referent. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: >- + Name is the name of the referent. When unspecified, + this refers to all resources of the specified Group + and Kind in the specified namespace. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: >- + Name is the name of the referent. When unspecified, + this authentication refers to the local namespace. + maxLength: 253 + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: networkauthentications.policy.linkerd.io + annotations: + linkerd.io/created-by: linkerd/cli dev-undefined + labels: + helm.sh/chart: linkerd-control-plane-1.1.11-edge + linkerd.io/control-plane-ns: linkerd +spec: + group: policy.linkerd.io + scope: Namespaced + names: + kind: NetworkAuthentication + plural: networkauthentications + singular: networkauthentication + shortNames: [] + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: [spec] + properties: + spec: + description: >- + NetworkAuthentication defines a list of authenticated client + networks to be referenced by an `AuthorizationPolicy`. If a + client connection originates from ANY of the of the provided + networks, the connection is considered authenticated. + type: object + required: [networks] + properties: + networks: + type: array + items: + type: object + required: [cidr] + properties: + cidr: + description: >- + The CIDR of the network to be authorized. + type: string + except: + description: >- + A list of IP networks/addresses not to be included in + the above `cidr`. + type: array + items: + type: string +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: servers.policy.linkerd.io annotations: @@ -859,6 +1097,9 @@ webhooks: apiGroups: ["policy.linkerd.io"] apiVersions: ["v1alpha1", "v1beta1"] resources: + - authorizationpolicies + - networkauthentications + - meshtlsauthentications - serverauthorizations - servers sideEffects: None @@ -883,6 +1124,9 @@ rules: - apiGroups: - policy.linkerd.io resources: + - authorizationpolicies + - meshtlsauthentications + - networkauthentications - servers - serverauthorizations verbs: diff --git a/policy-controller/core/src/lib.rs b/policy-controller/core/src/lib.rs index 1a3e77ebef27c..f26cb4c5c20f5 100644 --- a/policy-controller/core/src/lib.rs +++ b/policy-controller/core/src/lib.rs @@ -24,9 +24,23 @@ pub type InboundServerStream = Pin + Send + /// Inbound server configuration. #[derive(Clone, Debug, PartialEq, Eq)] pub struct InboundServer { - pub name: String, + pub reference: ServerRef, + pub protocol: ProxyProtocol, - pub authorizations: HashMap, + pub authorizations: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ServerRef { + Default(String), + Server(String), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum AuthorizationRef { + Default(String), + ServerAuthorization(String), + AuthorizationPolicy(String), } /// Describes how a proxy should handle inbound connections. @@ -63,7 +77,7 @@ pub enum ClientAuthentication { /// Indicates that clients need not be authenticated. Unauthenticated, - /// Indicates that clients must use TLS bu need not provide a client identity. + /// Indicates that clients must use TLS but need not provide a client identity. TlsUnauthenticated, /// Indicates that clients must use mutually-authenticated TLS. diff --git a/policy-controller/grpc/Cargo.toml b/policy-controller/grpc/Cargo.toml index 151e179be19a7..5579067d9bec3 100644 --- a/policy-controller/grpc/Cargo.toml +++ b/policy-controller/grpc/Cargo.toml @@ -15,6 +15,7 @@ drain = "0.1" futures = { version = "0.3", default-features = false } linkerd2-proxy-api = { version = "0.3", features = ["inbound", "server"] } linkerd-policy-controller-core = { path = "../core" } +maplit = "1" tokio = { version = "1", features = ["macros"] } tonic = { version = "0.6", default-features = false, features = ["transport"] } tracing = "0.1" diff --git a/policy-controller/grpc/src/lib.rs b/policy-controller/grpc/src/lib.rs index 57cb6999b1a10..b186913e43fa1 100644 --- a/policy-controller/grpc/src/lib.rs +++ b/policy-controller/grpc/src/lib.rs @@ -7,9 +7,11 @@ use linkerd2_proxy_api::inbound::{ inbound_server_policies_server::{InboundServerPolicies, InboundServerPoliciesServer}, }; use linkerd_policy_controller_core::{ - ClientAuthentication, ClientAuthorization, DiscoverInboundServer, IdentityMatch, InboundServer, - InboundServerStream, IpNet, NetworkMatch, ProxyProtocol, + AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer, + IdentityMatch, InboundServer, InboundServerStream, IpNet, NetworkMatch, ProxyProtocol, + ServerRef, }; +use maplit::*; use std::sync::Arc; use tracing::trace; @@ -194,9 +196,16 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server { .collect(); trace!(?authorizations); - let labels = vec![("name".to_string(), srv.name.to_string())] - .into_iter() - .collect(); + let labels = match &srv.reference { + ServerRef::Default(name) => convert_args!(hashmap!( + "kind" => "default", + "name" => name, + )), + ServerRef::Server(name) => convert_args!(hashmap!( + "kind" => "server", + "name" => name, + )), + }; trace!(?labels); proto::Server { @@ -208,7 +217,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server { } fn to_authz( - name: impl ToString, + reference: &AuthorizationRef, ClientAuthorization { networks, authentication, @@ -233,9 +242,20 @@ fn to_authz( .collect() }; - let labels = vec![("name".to_string(), name.to_string())] - .into_iter() - .collect(); + let labels = match reference { + AuthorizationRef::Default(name) => convert_args!(hashmap!( + "kind" => "default", + "name" => name, + )), + AuthorizationRef::ServerAuthorization(name) => convert_args!(hashmap!( + "kind" => "serverauthorization", + "name" => name, + )), + AuthorizationRef::AuthorizationPolicy(name) => convert_args!(hashmap!( + "kind" => "authorizationpolicy", + "name" => name, + )), + }; let authn = match authentication { ClientAuthentication::Unauthenticated => proto::Authn { diff --git a/policy-controller/k8s/api/src/lib.rs b/policy-controller/k8s/api/src/lib.rs index 66065be757a69..bb761a8e85f9c 100644 --- a/policy-controller/k8s/api/src/lib.rs +++ b/policy-controller/k8s/api/src/lib.rs @@ -7,7 +7,9 @@ pub mod policy; pub use self::labels::Labels; pub use k8s_openapi::api::{ self, - core::v1::{Namespace, Node, NodeSpec, Pod, PodSpec, PodStatus}, + core::v1::{Namespace, Node, NodeSpec, Pod, PodSpec, PodStatus, ServiceAccount}, +}; +pub use kube::{ + api::{ObjectMeta, Resource, ResourceExt}, + runtime::watcher::Event as WatchEvent, }; -pub use kube::api::{ObjectMeta, Resource, ResourceExt}; -pub use kube::runtime::watcher::Event as WatchEvent; diff --git a/policy-controller/k8s/api/src/policy.rs b/policy-controller/k8s/api/src/policy.rs index a3364281db4cf..dce36084d5ed4 100644 --- a/policy-controller/k8s/api/src/policy.rs +++ b/policy-controller/k8s/api/src/policy.rs @@ -1,9 +1,17 @@ -pub mod network; +pub mod authorization_policy; +pub mod meshtls_authentication; +mod network; +pub mod network_authentication; pub mod server; pub mod server_authorization; +pub mod target_ref; pub use self::{ + authorization_policy::{AuthorizationPolicy, AuthorizationPolicySpec}, + meshtls_authentication::{MeshTLSAuthentication, MeshTLSAuthenticationSpec}, network::Network, + network_authentication::{NetworkAuthentication, NetworkAuthenticationSpec}, server::{Server, ServerSpec}, server_authorization::{ServerAuthorization, ServerAuthorizationSpec}, + target_ref::{ClusterTargetRef, LocalTargetRef, NamespacedTargetRef}, }; diff --git a/policy-controller/k8s/api/src/policy/authorization_policy.rs b/policy-controller/k8s/api/src/policy/authorization_policy.rs new file mode 100644 index 0000000000000..6a5cc189a3fa0 --- /dev/null +++ b/policy-controller/k8s/api/src/policy/authorization_policy.rs @@ -0,0 +1,22 @@ +use super::{LocalTargetRef, NamespacedTargetRef}; + +#[derive( + Clone, + Debug, + Default, + kube::CustomResource, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, +)] +#[kube( + group = "policy.linkerd.io", + version = "v1alpha1", + kind = "AuthorizationPolicy", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct AuthorizationPolicySpec { + pub target_ref: LocalTargetRef, + pub required_authentication_refs: Vec, +} diff --git a/policy-controller/k8s/api/src/policy/meshtls_authentication.rs b/policy-controller/k8s/api/src/policy/meshtls_authentication.rs new file mode 100644 index 0000000000000..42de93a037da3 --- /dev/null +++ b/policy-controller/k8s/api/src/policy/meshtls_authentication.rs @@ -0,0 +1,23 @@ +use super::NamespacedTargetRef; + +#[derive( + Clone, + Debug, + Default, + PartialEq, + kube::CustomResource, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, +)] +#[kube( + group = "policy.linkerd.io", + version = "v1alpha1", + kind = "MeshTLSAuthentication", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct MeshTLSAuthenticationSpec { + pub identities: Option>, + pub identity_refs: Option>, +} diff --git a/policy-controller/k8s/api/src/policy/network_authentication.rs b/policy-controller/k8s/api/src/policy/network_authentication.rs new file mode 100644 index 0000000000000..9f27752b2e771 --- /dev/null +++ b/policy-controller/k8s/api/src/policy/network_authentication.rs @@ -0,0 +1,22 @@ +pub use super::Network; + +#[derive( + Clone, + Debug, + Default, + PartialEq, + kube::CustomResource, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, +)] +#[kube( + group = "policy.linkerd.io", + version = "v1alpha1", + kind = "NetworkAuthentication", + namespaced +)] +#[serde(rename_all = "camelCase")] +pub struct NetworkAuthenticationSpec { + pub networks: Vec, +} diff --git a/policy-controller/k8s/api/src/policy/target_ref.rs b/policy-controller/k8s/api/src/policy/target_ref.rs new file mode 100644 index 0000000000000..fcd74dd140d63 --- /dev/null +++ b/policy-controller/k8s/api/src/policy/target_ref.rs @@ -0,0 +1,351 @@ +#[derive( + Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema, +)] +pub struct ClusterTargetRef { + pub group: Option, + pub kind: String, + pub name: String, +} + +#[derive( + Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema, +)] +pub struct LocalTargetRef { + pub group: Option, + pub kind: String, + pub name: String, +} + +#[derive( + Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema, +)] +pub struct NamespacedTargetRef { + pub group: Option, + pub kind: String, + pub name: String, + pub namespace: Option, +} + +impl ClusterTargetRef { + pub fn from_resource(resource: &T) -> Self + where + T: kube::Resource, + T::DynamicType: Default, + { + let (group, kind, name) = group_kind_name(resource); + Self { group, kind, name } + } + + /// Returns the target ref kind, qualified by its group, if necessary. + pub fn canonical_kind(&self) -> String { + canonical_kind(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given resource type + pub fn targets_kind(&self) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + targets_kind::(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given cluster-level resource + pub fn targets(&self, resource: &T) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + if !self.targets_kind::() { + return false; + } + + if resource.meta().namespace.is_some() { + // If the reference or the resource has a namespace, that's a deal-breaker. + return false; + } + + match resource.meta().name.as_deref() { + None => return false, + Some(rname) => { + if !self.name.eq_ignore_ascii_case(rname) { + return false; + } + } + } + + true + } +} + +impl LocalTargetRef { + pub fn from_resource(resource: &T) -> Self + where + T: kube::Resource, + T::DynamicType: Default, + { + let (group, kind, name) = group_kind_name(resource); + Self { group, kind, name } + } + + /// Returns the target ref kind, qualified by its group, if necessary. + pub fn canonical_kind(&self) -> String { + canonical_kind(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given resource type + pub fn targets_kind(&self) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + targets_kind::(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given namespaced resource + pub fn targets(&self, resource: &T, local_ns: &str) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + if !self.targets_kind::() { + return false; + } + + // If the resource specifies a namespace other than the target or the + // default namespace, that's a deal-breaker. + match resource.meta().namespace.as_deref() { + Some(rns) if rns.eq_ignore_ascii_case(local_ns) => {} + _ => return false, + }; + + match resource.meta().name.as_deref() { + Some(rname) => rname.eq_ignore_ascii_case(&self.name), + _ => false, + } + } +} + +impl NamespacedTargetRef { + pub fn from_resource(resource: &T) -> Self + where + T: kube::Resource, + T::DynamicType: Default, + { + let (group, kind, name) = group_kind_name(resource); + let namespace = resource.meta().namespace.clone(); + Self { + group, + kind, + name, + namespace, + } + } + + /// Returns the target ref kind, qualified by its group, if necessary. + pub fn canonical_kind(&self) -> String { + canonical_kind(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given resource type + pub fn targets_kind(&self) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + targets_kind::(self.group.as_deref(), &self.kind) + } + + /// Checks whether the target references the given namespaced resource + pub fn targets(&self, resource: &T, local_ns: &str) -> bool + where + T: kube::Resource, + T::DynamicType: Default, + { + if !self.targets_kind::() { + return false; + } + + // If the resource specifies a namespace other than the target or the + // default namespace, that's a deal-breaker. + let tns = self.namespace.as_deref().unwrap_or(local_ns); + match resource.meta().namespace.as_deref() { + Some(rns) if rns.eq_ignore_ascii_case(tns) => {} + _ => return false, + }; + + match resource.meta().name.as_deref() { + None => return false, + Some(rname) => { + if !self.name.eq_ignore_ascii_case(rname) { + return false; + } + } + } + + true + } +} + +fn canonical_kind(group: Option<&str>, kind: &str) -> String { + if let Some(group) = group { + format!("{}.{}", kind, group) + } else { + kind.to_string() + } +} + +fn targets_kind(group: Option<&str>, kind: &str) -> bool +where + T: kube::Resource, + T::DynamicType: Default, +{ + let dt = Default::default(); + + let mut t_group = &*T::group(&dt); + if t_group.is_empty() { + t_group = "core"; + } + + group.unwrap_or("core").eq_ignore_ascii_case(t_group) + && kind.eq_ignore_ascii_case(&*T::kind(&dt)) +} + +fn group_kind_name(resource: &T) -> (Option, String, String) +where + T: kube::Resource, + T::DynamicType: Default, +{ + let dt = Default::default(); + + let group = match T::group(&dt) { + g if (*g).is_empty() => None, + g => Some(g.to_string()), + }; + + let kind = T::kind(&dt).to_string(); + + let name = resource + .meta() + .name + .clone() + .expect("resource must have a name"); + + (group, kind, name) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{policy::Server, Namespace, ObjectMeta, ServiceAccount}; + + #[test] + fn cluster_targets_namespace() { + let t = ClusterTargetRef { + kind: "Namespace".to_string(), + name: "appns".to_string(), + ..Default::default() + }; + assert!(t.targets_kind::()); + assert!(t.targets(&Namespace { + metadata: ObjectMeta { + name: Some("appns".to_string()), + ..ObjectMeta::default() + }, + ..Namespace::default() + })); + } + + #[test] + fn namespaced_targets_service_account() { + for tgt in &[ + NamespacedTargetRef { + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + namespace: Some("appns".to_string()), + ..Default::default() + }, + NamespacedTargetRef { + group: Some("core".to_string()), + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + namespace: Some("appns".to_string()), + }, + NamespacedTargetRef { + group: Some("CORE".to_string()), + kind: "SERVICEACCOUNT".to_string(), + name: "DEFAULT".to_string(), + namespace: Some("APPNS".to_string()), + }, + NamespacedTargetRef { + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + ..Default::default() + }, + ] { + assert!(tgt.targets_kind::()); + + assert!(!tgt.targets_kind::()); + + let sa = ServiceAccount { + metadata: ObjectMeta { + namespace: Some("appns".to_string()), + name: Some("default".to_string()), + ..ObjectMeta::default() + }, + ..ServiceAccount::default() + }; + assert!( + tgt.targets(&sa, "appns"), + "ServiceAccounts are targeted by name: {:#?}", + tgt + ); + + let sa = ServiceAccount { + metadata: ObjectMeta { + namespace: Some("otherns".to_string()), + name: Some("default".to_string()), + ..ObjectMeta::default() + }, + ..ServiceAccount::default() + }; + assert!( + !tgt.targets(&sa, "appns"), + "ServiceAccounts in other namespaces should not be targeted: {:#?}", + tgt + ); + } + + let tgt = NamespacedTargetRef { + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + ..Default::default() + }; + assert!( + { + let sa = ServiceAccount { + metadata: ObjectMeta { + namespace: Some("appns".to_string()), + name: Some("special".to_string()), + ..ObjectMeta::default() + }, + ..ServiceAccount::default() + }; + !tgt.targets(&sa, "appns") + }, + "resource comparison uses " + ); + } + + #[test] + fn namespaced_targets_server() { + let tgt = NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "http".to_string(), + namespace: Some("appns".to_string()), + }; + + assert!(tgt.targets_kind::()); + } +} diff --git a/policy-controller/k8s/index/Cargo.toml b/policy-controller/k8s/index/Cargo.toml index 0287555595c81..3f51593c695fd 100644 --- a/policy-controller/k8s/index/Cargo.toml +++ b/policy-controller/k8s/index/Cargo.toml @@ -17,6 +17,7 @@ tokio = { version = "1", features = ["macros", "rt", "sync"] } tracing = "0.1" [dev-dependencies] +maplit = "1" tokio-stream = "0.1" tokio-test = "0.4" tracing-subscriber = "0.3" diff --git a/policy-controller/k8s/index/src/authorization_policy.rs b/policy-controller/k8s/index/src/authorization_policy.rs new file mode 100644 index 0000000000000..0c7f540521702 --- /dev/null +++ b/policy-controller/k8s/index/src/authorization_policy.rs @@ -0,0 +1,85 @@ +use anyhow::{bail, Result}; +use linkerd_policy_controller_k8s_api::{ + self as k8s, + policy::{LocalTargetRef, NamespacedTargetRef}, +}; + +#[derive(Debug, PartialEq)] +pub(crate) struct Spec { + pub target: Target, + pub authentications: Vec, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Target { + Server(String), +} + +#[derive(Debug, PartialEq)] +pub(crate) enum AuthenticationTarget { + MeshTLS { + namespace: Option, + name: String, + }, + Network { + namespace: Option, + name: String, + }, +} + +impl TryFrom for Spec { + type Error = anyhow::Error; + + fn try_from(ap: k8s::policy::AuthorizationPolicySpec) -> Result { + let target = target(ap.target_ref)?; + + let authentications = ap + .required_authentication_refs + .into_iter() + .map(authentication_ref) + .collect::>>()?; + if authentications.is_empty() { + bail!("No authentication targets"); + } + + Ok(Self { + target, + authentications, + }) + } +} + +impl Target { + pub(crate) fn server(&self) -> Option<&str> { + match self { + Self::Server(s) => Some(s), + } + } +} + +fn target(t: LocalTargetRef) -> Result { + if t.targets_kind::() { + return Ok(Target::Server(t.name)); + } + + anyhow::bail!( + "unsupported authorization target type: {}", + t.canonical_kind() + ); +} + +fn authentication_ref(t: NamespacedTargetRef) -> Result { + if t.targets_kind::() { + Ok(AuthenticationTarget::MeshTLS { + namespace: t.namespace.map(Into::into), + name: t.name, + }) + } else if t.targets_kind::() { + Ok(AuthenticationTarget::Network { + namespace: t.namespace.map(Into::into), + name: t.name, + }) + } else { + anyhow::bail!("unsupported authentication target: {}", t.canonical_kind()); + } +} diff --git a/policy-controller/k8s/index/src/index.rs b/policy-controller/k8s/index/src/index.rs index 5f8403b56baa8..0719eb7d2b4bf 100644 --- a/policy-controller/k8s/index/src/index.rs +++ b/policy-controller/k8s/index/src/index.rs @@ -6,12 +6,15 @@ //! implements `kubert::index::IndexNamespacedResource` for the indexed //! kubernetes resources. -use crate::{defaults::DefaultPolicy, pod, server, server_authorization, ClusterInfo}; +use crate::{ + authorization_policy, defaults::DefaultPolicy, meshtls_authentication, network_authentication, + pod, server, server_authorization, ClusterInfo, +}; use ahash::{AHashMap as HashMap, AHashSet as HashSet}; use anyhow::{bail, Result}; use linkerd_policy_controller_core::{ - ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer, Ipv4Net, Ipv6Net, - ProxyProtocol, + AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer, + IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef, }; use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port, ResourceExt}; use parking_lot::RwLock; @@ -28,6 +31,7 @@ pub type SharedIndex = Arc>; pub struct Index { cluster_info: Arc, namespaces: NamespaceIndex, + authentications: AuthenticationNsIndex, } /// Holds all `Pod`, `Server`, and `ServerAuthorization` indices by-namespace. @@ -37,6 +41,15 @@ struct NamespaceIndex { by_ns: HashMap, } +/// Holds all `NetworkAuthentication` and `MeshTLSAuthentication` indices by-namespace. +/// +/// This is separate from `NamespaceIndex` because authorization policies may reference +/// authentication resources across namespaces. +#[derive(Debug, Default)] +struct AuthenticationNsIndex { + by_ns: HashMap, +} + /// Holds `Pod`, `Server`, and `ServerAuthorization` indices for a single namespace. #[derive(Debug)] struct Namespace { @@ -45,7 +58,7 @@ struct Namespace { } /// Holds all pod data for a single namespace. -#[derive(Debug, Default)] +#[derive(Debug)] struct PodIndex { namespace: String, by_name: HashMap, @@ -90,9 +103,24 @@ struct PodPortServer { /// Holds the state of policy resources for a single namespace. #[derive(Debug)] struct PolicyIndex { + namespace: String, cluster_info: Arc, + servers: HashMap, server_authorizations: HashMap, + + authorization_policies: HashMap, +} + +#[derive(Debug, Default)] +struct AuthenticationIndex { + meshtls: HashMap, + network: HashMap, +} + +struct NsUpdate { + added: Vec<(String, T)>, + removed: HashSet, } // === impl Index === @@ -104,8 +132,9 @@ impl Index { cluster_info: cluster_info.clone(), namespaces: NamespaceIndex { cluster_info, - by_ns: HashMap::new(), + by_ns: HashMap::default(), }, + authentications: AuthenticationNsIndex::default(), })) } @@ -134,13 +163,34 @@ impl Index { .rx .clone()) } + + fn ns_with_reindex(&mut self, namespace: String, f: impl FnOnce(&mut Namespace) -> bool) { + self.namespaces + .get_with_reindex(namespace, &self.authentications, f) + } + + fn ns_or_default_with_reindex( + &mut self, + namespace: String, + f: impl FnOnce(&mut Namespace) -> bool, + ) { + self.namespaces + .get_or_default_with_reindex(namespace, &self.authentications, f) + } + + fn reindex_all(&mut self) { + tracing::debug!("Reindexing all namespaces"); + for ns in self.namespaces.by_ns.values_mut() { + ns.reindex(&self.authentications); + } + } } impl kubert::index::IndexNamespacedResource for Index { fn apply(&mut self, pod: k8s::Pod) { let namespace = pod.namespace().unwrap(); let name = pod.name(); - let _span = info_span!("apply", ns = %namespace, pod = %name).entered(); + let _span = info_span!("apply", ns = %namespace, %name).entered(); let port_names = pod::tcp_port_names(pod.spec); let meta = pod::Meta::from_metadata(pod.metadata); @@ -151,21 +201,20 @@ impl kubert::index::IndexNamespacedResource for Index { let ns = self.namespaces.get_or_default(namespace); match ns.pods.update(name, meta, port_names) { Ok(None) => {} - Ok(Some(pod)) => pod.reindex_servers(&ns.policy), + Ok(Some(pod)) => pod.reindex_servers(&ns.policy, &self.authentications), Err(error) => { tracing::error!(%error, "Illegal pod update"); } } } - fn delete(&mut self, ns: String, pod: String) { - let _span = info_span!("delete", %ns, %pod).entered(); - + fn delete(&mut self, ns: String, name: String) { + tracing::debug!(%ns, %name, "delete"); if let Entry::Occupied(mut ns) = self.namespaces.by_ns.entry(ns) { // Once the pod is removed, there's nothing else to update. Any open // watches will complete. No other parts of the index need to be // updated. - if ns.get_mut().pods.by_name.remove(&pod).is_some() && ns.get().is_empty() { + if ns.get_mut().pods.by_name.remove(&name).is_some() && ns.get().is_empty() { ns.remove(); } } @@ -179,30 +228,23 @@ impl kubert::index::IndexNamespacedResource for Index { fn apply(&mut self, srv: k8s::policy::Server) { let ns = srv.namespace().expect("server must be namespaced"); let name = srv.name(); - let _span = info_span!("apply", %ns, srv = %name).entered(); + let _span = info_span!("apply", %ns, %name).entered(); let server = server::Server::from_resource(srv, &self.cluster_info); - self.namespaces - .get_or_default_with_reindex(ns, |ns| ns.policy.update_server(name, server)) + self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_server(name, server)) } - fn delete(&mut self, ns: String, srv: String) { - let _span = info_span!("delete", %ns, %srv).entered(); - self.namespaces - .get_with_reindex(ns, |ns| ns.policy.servers.remove(&srv).is_some()) + fn delete(&mut self, ns: String, name: String) { + let _span = info_span!("delete", %ns, %name).entered(); + self.ns_with_reindex(ns, |ns| ns.policy.servers.remove(&name).is_some()) } fn reset(&mut self, srvs: Vec, deleted: HashMap>) { let _span = info_span!("reset").entered(); - #[derive(Default)] - struct Ns { - added: Vec<(String, server::Server)>, - removed: HashSet, - } - // Aggregate all of the updates by namespace so that we only reindex // once per namespace. + type Ns = NsUpdate; let mut updates_by_ns = HashMap::::default(); for srv in srvs.into_iter() { let namespace = srv.namespace().expect("server must be namespaced"); @@ -224,7 +266,7 @@ impl kubert::index::IndexNamespacedResource for Index { // want to create a default namespace instance, we just want to // clear out all resources for the namespace (and then drop the // whole namespace, if necessary). - self.namespaces.get_with_reindex(namespace, |ns| { + self.ns_with_reindex(namespace, |ns| { ns.policy.servers.clear(); true }); @@ -232,17 +274,16 @@ impl kubert::index::IndexNamespacedResource for Index { // Otherwise, we take greater care to reindex only when the // state actually changed. The vast majority of resets will see // no actual data change. - self.namespaces - .get_or_default_with_reindex(namespace, |ns| { - let mut changed = !removed.is_empty(); - for name in removed.into_iter() { - ns.policy.servers.remove(&name); - } - for (name, server) in added.into_iter() { - changed = ns.policy.update_server(name, server) || changed; - } - changed - }); + self.ns_or_default_with_reindex(namespace, |ns| { + let mut changed = !removed.is_empty(); + for name in removed.into_iter() { + ns.policy.servers.remove(&name); + } + for (name, server) in added.into_iter() { + changed = ns.policy.update_server(name, server) || changed; + } + changed + }); } } } @@ -252,20 +293,20 @@ impl kubert::index::IndexNamespacedResource fo fn apply(&mut self, saz: k8s::policy::ServerAuthorization) { let ns = saz.namespace().unwrap(); let name = saz.name(); - let _span = info_span!("apply", %ns, saz = %name).entered(); + let _span = info_span!("apply", %ns, %name).entered(); match server_authorization::ServerAuthz::from_resource(saz, &self.cluster_info) { - Ok(meta) => self.namespaces.get_or_default_with_reindex(ns, move |ns| { + Ok(meta) => self.ns_or_default_with_reindex(ns, move |ns| { ns.policy.update_server_authz(name, meta) }), Err(error) => tracing::error!(%error, "Illegal server authorization update"), } } - fn delete(&mut self, ns: String, saz: String) { - let _span = info_span!("delete", %ns, %saz).entered(); - self.namespaces.get_with_reindex(ns, |ns| { - ns.policy.server_authorizations.remove(&saz).is_some() + fn delete(&mut self, ns: String, name: String) { + let _span = info_span!("delete", %ns, %name).entered(); + self.ns_with_reindex(ns, |ns| { + ns.policy.server_authorizations.remove(&name).is_some() }) } @@ -276,14 +317,9 @@ impl kubert::index::IndexNamespacedResource fo ) { let _span = info_span!("reset"); - #[derive(Default)] - struct Ns { - added: Vec<(String, server_authorization::ServerAuthz)>, - removed: HashSet, - } - // Aggregate all of the updates by namespace so that we only reindex // once per namespace. + type Ns = NsUpdate; let mut updates_by_ns = HashMap::::default(); for saz in sazs.into_iter() { let namespace = saz @@ -297,7 +333,7 @@ impl kubert::index::IndexNamespacedResource fo .added .push((name, saz)), Err(error) => { - tracing::error!(ns = %namespace, saz = %name, %error, "Illegal server authorization update") + tracing::error!(ns = %namespace, %name, %error, "Illegal server authorization update") } } } @@ -311,7 +347,7 @@ impl kubert::index::IndexNamespacedResource fo // want to create a default namespace instance, we just want to // clear out all resources for the namespace (and then drop the // whole namespace, if necessary). - self.namespaces.get_with_reindex(namespace, |ns| { + self.ns_with_reindex(namespace, |ns| { ns.policy.server_authorizations.clear(); true }); @@ -319,23 +355,261 @@ impl kubert::index::IndexNamespacedResource fo // Otherwise, we take greater care to reindex only when the // state actually changed. The vast majority of resets will see // no actual data change. - self.namespaces - .get_or_default_with_reindex(namespace, |ns| { - let mut changed = !removed.is_empty(); - for name in removed.into_iter() { - ns.policy.server_authorizations.remove(&name); - } - for (name, saz) in added.into_iter() { - changed = ns.policy.update_server_authz(name, saz) || changed; - } - changed - }); + self.ns_or_default_with_reindex(namespace, |ns| { + let mut changed = !removed.is_empty(); + for name in removed.into_iter() { + ns.policy.server_authorizations.remove(&name); + } + for (name, saz) in added.into_iter() { + changed = ns.policy.update_server_authz(name, saz) || changed; + } + changed + }); } } } } -// === impl NamespaceIndex === +impl kubert::index::IndexNamespacedResource for Index { + fn apply(&mut self, policy: k8s::policy::AuthorizationPolicy) { + let ns = policy.namespace().unwrap(); + let name = policy.name(); + let _span = info_span!("apply", %ns, saz = %name).entered(); + + let spec = match authorization_policy::Spec::try_from(policy.spec) { + Ok(spec) => spec, + Err(error) => { + tracing::warn!(%error, "Invalid authorization policy"); + return; + } + }; + + self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_authz_policy(name, spec)) + } + + fn delete(&mut self, ns: String, ap: String) { + let _span = info_span!("delete", %ns, %ap).entered(); + self.ns_with_reindex(ns, |ns| { + ns.policy.authorization_policies.remove(&ap).is_some() + }) + } + + fn reset( + &mut self, + policies: Vec, + deleted: HashMap>, + ) { + let _span = info_span!("reset"); + + // Aggregate all of the updates by namespace so that we only reindex + // once per namespace. + type Ns = NsUpdate; + let mut updates_by_ns = HashMap::::default(); + for policy in policies.into_iter() { + let namespace = policy + .namespace() + .expect("authorizationpolicy must be namespaced"); + let name = policy.name(); + match authorization_policy::Spec::try_from(policy.spec) { + Ok(spec) => updates_by_ns + .entry(namespace) + .or_default() + .added + .push((name, spec)), + Err(error) => { + tracing::error!(ns = %namespace, %name, %error, "Illegal server authorization update") + } + } + } + for (ns, names) in deleted.into_iter() { + updates_by_ns.entry(ns).or_default().removed = names; + } + + for (namespace, Ns { added, removed }) in updates_by_ns.into_iter() { + if added.is_empty() { + // If there are no live resources in the namespace, we do not + // want to create a default namespace instance, we just want to + // clear out all resources for the namespace (and then drop the + // whole namespace, if necessary). + self.ns_with_reindex(namespace, |ns| { + ns.policy.authorization_policies.clear(); + true + }); + } else { + // Otherwise, we take greater care to reindex only when the + // state actually changed. The vast majority of resets will see + // no actual data change. + self.ns_or_default_with_reindex(namespace, |ns| { + let mut changed = !removed.is_empty(); + for name in removed.into_iter() { + ns.policy.authorization_policies.remove(&name); + } + for (name, spec) in added.into_iter() { + changed = ns.policy.update_authz_policy(name, spec) || changed; + } + changed + }); + } + } + } +} + +impl kubert::index::IndexNamespacedResource for Index { + fn apply(&mut self, authn: k8s::policy::MeshTLSAuthentication) { + let ns = authn + .namespace() + .expect("MeshTLSAuthentication must have a namespace"); + let name = authn.name(); + let _span = info_span!("apply", %ns, %name).entered(); + + let spec = match meshtls_authentication::Spec::try_from_resource(authn, &self.cluster_info) + { + Ok(spec) => spec, + Err(error) => { + tracing::warn!(%error, "Invalid MeshTLSAuthentication"); + return; + } + }; + + if self.authentications.update_meshtls(ns, name, spec) { + self.reindex_all(); + } + } + + fn delete(&mut self, ns: String, name: String) { + let _span = info_span!("delete", %ns, %name).entered(); + + if let Entry::Occupied(mut ns) = self.authentications.by_ns.entry(ns) { + tracing::debug!("Deleting MeshTLSAuthentication"); + ns.get_mut().network.remove(&name); + if ns.get().is_empty() { + ns.remove(); + } + self.reindex_all(); + } else { + tracing::warn!("Namespace already deleted!"); + } + } + + fn reset( + &mut self, + authns: Vec, + deleted: HashMap>, + ) { + let _span = info_span!("reset"); + + let mut changed = false; + + for authn in authns.into_iter() { + let namespace = authn + .namespace() + .expect("meshtlsauthentication must be namespaced"); + let name = authn.name(); + let spec = match meshtls_authentication::Spec::try_from_resource( + authn, + &self.cluster_info, + ) { + Ok(spec) => spec, + Err(error) => { + tracing::warn!(ns = %namespace, %name, %error, "Invalid MeshTLSAuthentication"); + return; + } + }; + changed = self.authentications.update_meshtls(namespace, name, spec) || changed; + } + for (namespace, names) in deleted.into_iter() { + if let Entry::Occupied(mut ns) = self.authentications.by_ns.entry(namespace) { + for name in names.into_iter() { + ns.get_mut().meshtls.remove(&name); + } + if ns.get().is_empty() { + ns.remove(); + } + } + } + + if changed { + self.reindex_all(); + } + } +} + +impl kubert::index::IndexNamespacedResource for Index { + fn apply(&mut self, authn: k8s::policy::NetworkAuthentication) { + let ns = authn.namespace().unwrap(); + let name = authn.name(); + let _span = info_span!("apply", %ns, %name).entered(); + + let spec = match network_authentication::Spec::try_from(authn.spec) { + Ok(spec) => spec, + Err(error) => { + tracing::warn!(%error, "Invalid NetworkAuthentication"); + return; + } + }; + + if self.authentications.update_network(ns, name, spec) { + self.reindex_all(); + } + } + + fn delete(&mut self, ns: String, name: String) { + let _span = info_span!("delete", %ns, %name).entered(); + + if let Entry::Occupied(mut ns) = self.authentications.by_ns.entry(ns) { + tracing::debug!("Deleting MeshTLSAuthentication"); + + ns.get_mut().network.remove(&name); + if ns.get().is_empty() { + ns.remove(); + } + self.reindex_all(); + } else { + tracing::warn!("Namespace already deleted!"); + } + } + + fn reset( + &mut self, + authns: Vec, + deleted: HashMap>, + ) { + let _span = info_span!("reset"); + + let mut changed = false; + + for authn in authns.into_iter() { + let namespace = authn + .namespace() + .expect("meshtlsauthentication must be namespaced"); + let name = authn.name(); + let spec = match network_authentication::Spec::try_from(authn.spec) { + Ok(spec) => spec, + Err(error) => { + tracing::warn!(ns = %namespace, %name, %error, "Invalid NetworkAuthentication"); + return; + } + }; + changed = self.authentications.update_network(namespace, name, spec) || changed; + } + for (namespace, names) in deleted.into_iter() { + if let Entry::Occupied(mut ns) = self.authentications.by_ns.entry(namespace) { + for name in names.into_iter() { + ns.get_mut().meshtls.remove(&name); + } + if ns.get().is_empty() { + ns.remove(); + } + } + } + + if changed { + self.reindex_all(); + } + } +} + +// === impl NemspaceIndex === impl NamespaceIndex { fn get_or_default(&mut self, ns: String) -> &mut Namespace { @@ -345,16 +619,21 @@ impl NamespaceIndex { } /// Gets the given namespace and, if it exists, passes it to the given - /// function. When the function returns `true`, all pods in the namespace are - /// reindexed or, if the namespace is empty, the namespace is removed - /// entirely. - fn get_with_reindex(&mut self, namespace: String, f: impl FnOnce(&mut Namespace) -> bool) { + /// function. If the function returns true, all pods in the namespace are + /// reindexed; or, if the function returns false and the namespace is empty, + /// it is removed from the index. + fn get_with_reindex( + &mut self, + namespace: String, + authns: &AuthenticationNsIndex, + f: impl FnOnce(&mut Namespace) -> bool, + ) { if let Entry::Occupied(mut ns) = self.by_ns.entry(namespace) { if f(ns.get_mut()) { if ns.get().is_empty() { ns.remove(); } else { - ns.get_mut().reindex(); + ns.get_mut().reindex(authns); } } } @@ -366,11 +645,12 @@ impl NamespaceIndex { fn get_or_default_with_reindex( &mut self, namespace: String, + authns: &AuthenticationNsIndex, f: impl FnOnce(&mut Namespace) -> bool, ) { let ns = self.get_or_default(namespace); if f(ns) { - ns.reindex(); + ns.reindex(authns); } } } @@ -381,13 +661,15 @@ impl Namespace { fn new(namespace: String, cluster_info: Arc) -> Self { Namespace { pods: PodIndex { - namespace, + namespace: namespace.clone(), by_name: HashMap::default(), }, policy: PolicyIndex { + namespace, cluster_info, servers: HashMap::default(), server_authorizations: HashMap::default(), + authorization_policies: HashMap::default(), }, } } @@ -399,8 +681,8 @@ impl Namespace { } #[inline] - fn reindex(&mut self) { - self.pods.reindex(&self.policy); + fn reindex(&mut self, authns: &AuthenticationNsIndex) { + self.pods.reindex(&self.policy, authns); } } @@ -418,16 +700,12 @@ impl PodIndex { meta: pod::Meta, port_names: HashMap, ) -> Result> { - let pod = match self.by_name.entry(name) { - Entry::Vacant(entry) => { - tracing::debug!(?meta, ?port_names, "Creating"); - let pod = Pod { - meta, - port_names, - port_servers: pod::PortMap::default(), - }; - entry.insert(pod) - } + let pod = match self.by_name.entry(name.clone()) { + Entry::Vacant(entry) => entry.insert(Pod { + meta, + port_names, + port_servers: pod::PortMap::default(), + }), Entry::Occupied(entry) => { let pod = entry.into_mut(); @@ -435,16 +713,16 @@ impl PodIndex { // Pod labels and annotations may change at runtime, but the // port list may not if pod.port_names != port_names { - bail!("pod port names must not change"); + bail!("pod {} port names must not change", name); } // If there aren't meaningful changes, then don't bother doing // any more work. if pod.meta == meta { - tracing::trace!("No changes"); + tracing::debug!(pod = %name, "No changes"); return Ok(None); } - tracing::debug!(?meta, "Updating"); + tracing::debug!(pod = %name, "Updating"); pod.meta = meta; pod } @@ -452,11 +730,11 @@ impl PodIndex { Ok(Some(pod)) } - fn reindex(&mut self, policy: &PolicyIndex) { + fn reindex(&mut self, policy: &PolicyIndex, authns: &AuthenticationNsIndex) { let _span = info_span!("reindex", ns = %self.namespace).entered(); for (name, pod) in self.by_name.iter_mut() { let _span = info_span!("pod", pod = %name).entered(); - pod.reindex_servers(policy); + pod.reindex_servers(policy, authns); } } } @@ -465,9 +743,7 @@ impl PodIndex { impl Pod { /// Determines the policies for ports on this pod. - fn reindex_servers(&mut self, policy: &PolicyIndex) { - tracing::debug!("Indexing servers for pod"); - + fn reindex_servers(&mut self, policy: &PolicyIndex, authentications: &AuthenticationNsIndex) { // Keep track of the ports that are already known in the pod so that, after applying server // matches, we can ensure remaining ports are set to the default policy. let mut unmatched_ports = self.port_servers.keys().copied().collect::(); @@ -497,7 +773,7 @@ impl Pod { continue; } - let s = policy.inbound_server(srvname.clone(), server); + let s = policy.inbound_server(srvname.clone(), server, authentications); self.update_server(port, srvname, s); matched_ports.insert(port, srvname.clone()); @@ -519,6 +795,7 @@ impl Pod { fn update_server(&mut self, port: u16, name: &str, server: InboundServer) { match self.port_servers.entry(port) { Entry::Vacant(entry) => { + tracing::trace!(port = %port, server = %name, "Creating server"); let (tx, rx) = watch::channel(server); entry.insert(PodPortServer { name: Some(name.to_string()), @@ -533,6 +810,7 @@ impl Pod { // Avoid sending redundant updates. if ps.name.as_deref() == Some(name) && *ps.rx.borrow() == server { tracing::trace!(port = %port, server = %name, "Skipped redundant server update"); + tracing::trace!(?server); return; } @@ -542,6 +820,7 @@ impl Pod { // make the opportunistic choice to assume the cluster is // configured coherently so we take the update. The admission // controller should prevent conflicts. + tracing::trace!(port = %port, server = %name, "Updating server"); ps.name = Some(name.to_string()); ps.tx.send(server).expect("a receiver is held by the index"); } @@ -553,9 +832,9 @@ impl Pod { /// Updates a pod-port to use the given named server. fn set_default_server(&mut self, port: u16, config: &ClusterInfo) { let server = Self::default_inbound_server(port, &self.meta.settings, config); - tracing::debug!(%port, server = %config.default_policy, "Setting default server"); match self.port_servers.entry(port) { Entry::Vacant(entry) => { + tracing::debug!(%port, server = %config.default_policy, "Creating default server"); let (tx, rx) = watch::channel(server); entry.insert(PodPortServer { name: None, tx, rx }); } @@ -565,9 +844,11 @@ impl Pod { // Avoid sending redundant updates. if *ps.rx.borrow() == server { + tracing::trace!(%port, server = %config.default_policy, "Default server already set"); return; } + tracing::debug!(%port, server = %config.default_policy, "Setting default server"); ps.name = None; ps.tx.send(server).expect("a receiver is held by the index"); } @@ -642,10 +923,13 @@ impl Pod { let networks = if cluster_only { config.networks.iter().copied().map(Into::into).collect() } else { - vec![Ipv4Net::default().into(), Ipv6Net::default().into()] + vec![ + "0.0.0.0/0".parse::().unwrap().into(), + "::/0".parse::().unwrap().into(), + ] }; authorizations.insert( - format!("default:{}", policy), + AuthorizationRef::Default(policy.to_string()), ClientAuthorization { authentication, networks, @@ -653,9 +937,8 @@ impl Pod { ); }; - tracing::trace!(port, ?settings, %policy, ?protocol, ?authorizations, "default server"); InboundServer { - name: format!("default:{}", policy), + reference: ServerRef::Default(policy.to_string()), protocol, authorizations, } @@ -708,10 +991,32 @@ impl PolicyIndex { true } - fn inbound_server(&self, name: String, server: &server::Server) -> InboundServer { - let authorizations = self.client_authzs(&name, server); + fn update_authz_policy(&mut self, name: String, spec: authorization_policy::Spec) -> bool { + match self.authorization_policies.entry(name) { + Entry::Vacant(entry) => { + entry.insert(spec); + } + Entry::Occupied(entry) => { + let ap = entry.into_mut(); + if *ap == spec { + return false; + } + *ap = spec; + } + } + true + } + + fn inbound_server( + &self, + name: String, + server: &server::Server, + authentications: &AuthenticationNsIndex, + ) -> InboundServer { + tracing::trace!(%name, ?server, "Creating inbound server"); + let authorizations = self.client_authzs(&name, server, authentications); InboundServer { - name, + reference: ServerRef::Server(name), authorizations, protocol: server.protocol.clone(), } @@ -721,16 +1026,215 @@ impl PolicyIndex { &self, server_name: &str, server: &server::Server, - ) -> HashMap { - self.server_authorizations - .iter() - .filter_map(|(name, saz)| { - if saz.server_selector.selects(server_name, &server.labels) { - Some((name.to_string(), saz.authz.clone())) - } else { - None + authentications: &AuthenticationNsIndex, + ) -> HashMap { + let mut authzs = HashMap::default(); + for (name, saz) in self.server_authorizations.iter() { + if saz.server_selector.selects(server_name, &server.labels) { + authzs.insert( + AuthorizationRef::ServerAuthorization(name.to_string()), + saz.authz.clone(), + ); + } + } + + for (name, spec) in self.authorization_policies.iter() { + match spec.target.server() { + Some(target) if target != server_name => { + tracing::trace!( + ns = %self.namespace, + authorizationpolicy = %name, + server = %server_name, + %target, + "AuthorizationPolicy does not target server", + ); + continue; + } + None => continue, + Some(_) => {} + } + + tracing::trace!( + ns = %self.namespace, + authorizationpolicy = %name, + server = %server_name, + "AuthorizationPolicy targets server", + ); + tracing::trace!(authns = ?spec.authentications); + + let authz = match self.policy_client_authz(spec, authentications) { + Ok(authz) => authz, + Err(error) => { + tracing::info!( + server = %server_name, + authorizationpolicy = %name, + %error, + "Illegal AuthorizationPolicy; ignoring", + ); + continue; + } + }; + + let reference = AuthorizationRef::AuthorizationPolicy(name.to_string()); + authzs.insert(reference, authz); + } + + authzs + } + + fn policy_client_authz( + &self, + spec: &authorization_policy::Spec, + all_authentications: &AuthenticationNsIndex, + ) -> Result { + use authorization_policy::AuthenticationTarget; + + let mut identities = None; + for tgt in spec.authentications.iter() { + if let AuthenticationTarget::MeshTLS { + ref namespace, + ref name, + } = tgt + { + let namespace = namespace.as_deref().unwrap_or(&self.namespace); + tracing::trace!(ns = %namespace, %name, "Finding MeshTLSAuthentication"); + if let Some(ns) = all_authentications.by_ns.get(namespace) { + if let Some(authn) = ns.meshtls.get(name) { + tracing::trace!(ns = %namespace, %name, ids = ?authn.matches, "Found MeshTLSAuthentication"); + // There can only be a single required MeshTLSAuthentication. This is + // enforced by the admission controller. + if identities.is_some() { + bail!("policy must not include multiple MeshTLSAuthentications"); + } + let ids = authn.matches.clone(); + identities = Some(ids); + continue; + } + } + bail!( + "could not find MeshTLSAuthentication {} in namespace {}", + name, + namespace + ); + } + } + + let mut networks = None; + for tgt in spec.authentications.iter() { + if let AuthenticationTarget::Network { + ref namespace, + ref name, + } = tgt + { + let namespace = namespace.as_deref().unwrap_or(&self.namespace); + tracing::trace!(ns = %namespace, %name, "Finding NetworkAuthentication"); + if let Some(ns) = all_authentications.by_ns.get(namespace) { + if let Some(authn) = ns.network.get(name).as_ref() { + tracing::trace!(ns = %namespace, %name, nets = ?authn.matches, "Found NetworkAuthentication"); + // There can only be a single required NetworkAuthentication. This is + // enforced by the admission controller. + if networks.is_some() { + bail!("policy must not include multiple NetworkAuthentications"); + } + let nets = authn.matches.clone(); + networks = Some(nets); + continue; + } + } + bail!( + "could not find NetworkAuthentication {} in namespace {}", + name, + namespace + ); + } + } + + Ok(ClientAuthorization { + // If MTLS identities are configured, use them. Otherwise, do not require + // authentication. + authentication: identities + .map(ClientAuthentication::TlsAuthenticated) + .unwrap_or(ClientAuthentication::Unauthenticated), + + // If networks are configured, use them. Otherwise, this applies to all networks. + networks: networks.unwrap_or_else(|| { + vec![ + NetworkMatch { + net: Ipv4Net::default().into(), + except: vec![], + }, + NetworkMatch { + net: Ipv6Net::default().into(), + except: vec![], + }, + ] + }), + }) + } +} + +// === impl AuthenticationNsIndex === + +impl AuthenticationNsIndex { + fn update_meshtls( + &mut self, + namespace: String, + name: String, + spec: meshtls_authentication::Spec, + ) -> bool { + match self.by_ns.entry(namespace).or_default().meshtls.entry(name) { + Entry::Vacant(entry) => { + entry.insert(spec); + } + Entry::Occupied(mut entry) => { + if *entry.get() == spec { + return false; } - }) - .collect() + entry.insert(spec); + } + } + + true + } + + fn update_network( + &mut self, + namespace: String, + name: String, + spec: network_authentication::Spec, + ) -> bool { + match self.by_ns.entry(namespace).or_default().network.entry(name) { + Entry::Vacant(entry) => { + entry.insert(spec); + } + Entry::Occupied(mut entry) => { + if *entry.get() == spec { + return false; + } + entry.insert(spec); + } + } + + true + } +} + +// === impl AuthenticationIndex === + +impl AuthenticationIndex { + #[inline] + fn is_empty(&self) -> bool { + self.meshtls.is_empty() && self.network.is_empty() + } +} + +// === imp NsUpdate === + +impl Default for NsUpdate { + fn default() -> Self { + Self { + added: vec![], + removed: Default::default(), + } } } diff --git a/policy-controller/k8s/index/src/lib.rs b/policy-controller/k8s/index/src/lib.rs index 9fd5417d65168..b3d1f4520c87d 100644 --- a/policy-controller/k8s/index/src/lib.rs +++ b/policy-controller/k8s/index/src/lib.rs @@ -23,8 +23,11 @@ #![deny(warnings, rust_2018_idioms)] #![forbid(unsafe_code)] +mod authorization_policy; mod defaults; mod index; +mod meshtls_authentication; +mod network_authentication; mod pod; mod server; mod server_authorization; diff --git a/policy-controller/k8s/index/src/meshtls_authentication.rs b/policy-controller/k8s/index/src/meshtls_authentication.rs new file mode 100644 index 0000000000000..6dd26a38d06db --- /dev/null +++ b/policy-controller/k8s/index/src/meshtls_authentication.rs @@ -0,0 +1,46 @@ +use crate::ClusterInfo; +use anyhow::Result; +use linkerd_policy_controller_core::IdentityMatch; +use linkerd_policy_controller_k8s_api::{ + policy::MeshTLSAuthentication, ResourceExt, ServiceAccount, +}; + +#[derive(Debug, PartialEq)] +pub(crate) struct Spec { + pub matches: Vec, +} + +impl Spec { + pub(crate) fn try_from_resource( + ma: MeshTLSAuthentication, + cluster: &ClusterInfo, + ) -> anyhow::Result { + let namespace = ma + .namespace() + .expect("MeshTLSAuthentication must have a namespace"); + + let identities = ma.spec.identities.into_iter().flatten().map(|s| { + Ok(s.parse::() + .expect("identity match parsing is infallible")) + }); + + let identity_refs = ma.spec.identity_refs.into_iter().flatten().map(|tgt| { + if tgt.targets_kind::() { + let ns = tgt.namespace.as_deref().unwrap_or(&namespace); + let id = cluster.service_account_identity(ns, &tgt.name); + Ok(IdentityMatch::Exact(id)) + } else { + anyhow::bail!("unsupported target type: {:?}", tgt.canonical_kind()) + } + }); + + let matches = identities + .chain(identity_refs) + .collect::>>()?; + if matches.is_empty() { + anyhow::bail!("No identities configured"); + } + + Ok(Spec { matches }) + } +} diff --git a/policy-controller/k8s/index/src/network_authentication.rs b/policy-controller/k8s/index/src/network_authentication.rs new file mode 100644 index 0000000000000..c73a4d5921548 --- /dev/null +++ b/policy-controller/k8s/index/src/network_authentication.rs @@ -0,0 +1,28 @@ +use linkerd_policy_controller_core::NetworkMatch; +use linkerd_policy_controller_k8s_api::policy::NetworkAuthenticationSpec; + +#[derive(Debug, PartialEq)] +pub(crate) struct Spec { + pub matches: Vec, +} + +impl TryFrom for Spec { + type Error = anyhow::Error; + + fn try_from(spec: NetworkAuthenticationSpec) -> anyhow::Result { + let matches = spec + .networks + .into_iter() + .map(|n| NetworkMatch { + net: n.cidr.into(), + except: n.except.into_iter().flatten().map(Into::into).collect(), + }) + .collect::>(); + + if matches.is_empty() { + anyhow::bail!("No networks configured"); + } + + Ok(Spec { matches }) + } +} diff --git a/policy-controller/k8s/index/src/tests.rs b/policy-controller/k8s/index/src/tests.rs index b4e3a27da7eee..0003993856f59 100644 --- a/policy-controller/k8s/index/src/tests.rs +++ b/policy-controller/k8s/index/src/tests.rs @@ -1,13 +1,21 @@ +mod annotation; +mod authorization_policy; +mod server_authorization; + use crate::{defaults::DefaultPolicy, index::*, server_authorization::ServerSelector, ClusterInfo}; use ahash::AHashMap as HashMap; use kubert::index::IndexNamespacedResource; use linkerd_policy_controller_core::{ - ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer, IpNet, Ipv4Net, - Ipv6Net, NetworkMatch, ProxyProtocol, + AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer, + IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef, }; use linkerd_policy_controller_k8s_api::{ - self as k8s, api::core::v1::ContainerPort, policy::server::Port, ResourceExt, + self as k8s, + api::core::v1::ContainerPort, + policy::{server::Port, LocalTargetRef, NamespacedTargetRef}, + ResourceExt, }; +use maplit::*; use tokio::time; #[test] @@ -19,329 +27,14 @@ fn pod_must_exist_for_lookup() { .expect_err("pod-0.ns-0 must not exist"); } -#[test] -fn links_named_server_port() { - let test = TestConfig::default(); - - let mut pod = mk_pod( - "ns-0", - "pod-0", - Some(( - "container-0", - Some(ContainerPort { - name: Some("admin-http".to_string()), - container_port: 8080, - protocol: Some("TCP".to_string()), - ..ContainerPort::default() - }), - )), - ); - pod.labels_mut() - .insert("app".to_string(), "app-0".to_string()); - test.index.write().apply(pod); - - let mut rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 8080) - .expect("pod-0.ns-0 should exist"); - assert_eq!(*rx.borrow_and_update(), test.default_server()); - - test.index.write().apply(mk_server( - "ns-0", - "srv-admin-http", - Port::Name("admin-http".to_string()), - None, - Some(("app", "app-0")), - Some(k8s::policy::server::ProxyProtocol::Http1), - )); - assert!(rx.has_changed().unwrap()); - assert_eq!( - *rx.borrow_and_update(), - InboundServer { - name: "srv-admin-http".to_string(), - authorizations: Default::default(), - protocol: ProxyProtocol::Http1, - }, - ); -} - -#[test] -fn links_unnamed_server_port() { - let test = TestConfig::default(); - - let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - pod.labels_mut() - .insert("app".to_string(), "app-0".to_string()); - test.index.write().apply(pod); - - let mut rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 8080) - .expect("pod-0.ns-0 should exist"); - assert_eq!(*rx.borrow_and_update(), test.default_server()); - - test.index.write().apply(mk_server( - "ns-0", - "srv-8080", - Port::Number(8080), - None, - Some(("app", "app-0")), - Some(k8s::policy::server::ProxyProtocol::Http1), - )); - assert!(rx.has_changed().unwrap()); - assert_eq!( - *rx.borrow_and_update(), - InboundServer { - name: "srv-8080".to_string(), - authorizations: Default::default(), - protocol: ProxyProtocol::Http1, - }, - ); -} - -#[test] -fn links_server_authz_by_name() { - link_server_authz(ServerSelector::Name("srv-8080".to_string())) -} - -#[test] -fn links_server_authz_by_label() { - link_server_authz(ServerSelector::Selector( - Some(("app", "app-0")).into_iter().collect(), - )); -} - -#[inline] -fn link_server_authz(selector: ServerSelector) { - let test = TestConfig::default(); - - let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - pod.labels_mut() - .insert("app".to_string(), "app-0".to_string()); - test.index.write().apply(pod); - - let mut rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 8080) - .expect("pod-0.ns-0 should exist"); - assert_eq!(*rx.borrow_and_update(), test.default_server()); - - test.index.write().apply(mk_server( - "ns-0", - "srv-8080", - Port::Number(8080), - Some(("app", "app-0")), - Some(("app", "app-0")), - Some(k8s::policy::server::ProxyProtocol::Http1), - )); - assert!(rx.has_changed().unwrap()); - assert_eq!( - *rx.borrow_and_update(), - InboundServer { - name: "srv-8080".to_string(), - authorizations: Default::default(), - protocol: ProxyProtocol::Http1, - }, - ); - test.index.write().apply(mk_server_authz( - "ns-0", - "authz-foo", - selector, - k8s::policy::server_authorization::Client { - networks: Some(vec![k8s::policy::server_authorization::Network { - cidr: "10.0.0.0/8".parse().unwrap(), - except: None, - }]), - unauthenticated: false, - mesh_tls: Some(k8s::policy::server_authorization::MeshTls { - identities: Some(vec!["foo.bar".to_string()]), - ..Default::default() - }), - }, - )); - assert!(rx.has_changed().unwrap()); - assert_eq!(rx.borrow().name, "srv-8080"); - assert_eq!(rx.borrow().protocol, ProxyProtocol::Http1,); - assert!(rx.borrow().authorizations.contains_key("authz-foo")); -} - -#[test] -fn server_update_deselects_pod() { - let test = TestConfig::default(); - - test.index.write().reset( - vec![mk_pod("ns-0", "pod-0", Some(("container-0", None)))], - Default::default(), - ); - - let mut srv = mk_server( - "ns-0", - "srv-0", - Port::Number(2222), - None, - None, - Some(k8s::policy::server::ProxyProtocol::Http2), - ); - test.index - .write() - .reset(vec![srv.clone()], Default::default()); - - // The default policy applies for all ports. - let mut rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 2222) - .unwrap(); - assert_eq!( - *rx.borrow_and_update(), - InboundServer { - name: "srv-0".into(), - protocol: ProxyProtocol::Http2, - authorizations: Default::default(), - } - ); - - test.index.write().apply({ - srv.spec.pod_selector = Some(("label", "value")).into_iter().collect(); - srv - }); - assert!(rx.has_changed().unwrap()); - assert_eq!(*rx.borrow(), test.default_server()); -} - -/// Tests that pod servers are configured with defaults based on the -/// workload-defined `DefaultPolicy` policy. -/// -/// Iterates through each default policy and validates that it produces expected -/// configurations. -#[test] -fn default_policy_annotated() { - for default in &DEFAULTS { - let test = TestConfig::from_default_policy(match *default { - // Invert default to ensure override applies. - DefaultPolicy::Deny => DefaultPolicy::Allow { - authenticated_only: false, - cluster_only: false, - }, - _ => DefaultPolicy::Deny, - }); - - // Initially create the pod without an annotation and check that it gets - // the global default. - let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - test.index - .write() - .reset(vec![pod.clone()], Default::default()); - - let mut rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 2222) - .expect("pod-0.ns-0 should exist"); - assert_eq!( - rx.borrow_and_update().name, - format!("default:{}", test.default_policy) - ); - - // Update the annotation on the pod and check that the watch is updated - // with the new default. - pod.annotations_mut().insert( - "config.linkerd.io/default-inbound-policy".into(), - default.to_string(), - ); - test.index.write().apply(pod); - assert!(rx.has_changed().unwrap()); - assert_eq!(rx.borrow().name, format!("default:{}", default)); - } -} - -/// Tests that an invalid workload annotation is ignored in favor of the global -/// default. -#[tokio::test] -async fn default_policy_annotated_invalid() { - let test = TestConfig::default(); - - let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - p.annotations_mut().insert( - "config.linkerd.io/default-inbound-policy".into(), - "bogus".into(), - ); - test.index.write().reset(vec![p], Default::default()); - - // Lookup port 2222 -> default config. - let rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 2222) - .expect("pod must exist in lookups"); - assert_eq!(*rx.borrow(), test.default_server()); -} - -#[test] -fn opaque_annotated() { - for default in &DEFAULTS { - let test = TestConfig::from_default_policy(*default); - - let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - p.annotations_mut() - .insert("config.linkerd.io/opaque-ports".into(), "2222".into()); - test.index.write().reset(vec![p], Default::default()); - - let mut server = test.default_server(); - server.protocol = ProxyProtocol::Opaque; - - let rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 2222) - .expect("pod-0.ns-0 should exist"); - assert_eq!(*rx.borrow(), server); - } -} - -#[test] -fn authenticated_annotated() { - for default in &DEFAULTS { - let test = TestConfig::from_default_policy(*default); - - let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); - p.annotations_mut().insert( - "config.linkerd.io/proxy-require-identity-inbound-ports".into(), - "2222".into(), - ); - test.index.write().reset(vec![p], Default::default()); - - let config = { - let policy = match *default { - DefaultPolicy::Allow { cluster_only, .. } => DefaultPolicy::Allow { - cluster_only, - authenticated_only: true, - }, - DefaultPolicy::Deny => DefaultPolicy::Deny, - }; - InboundServer { - name: format!("default:{}", policy), - authorizations: mk_default_policy(policy, test.cluster.networks), - protocol: ProxyProtocol::Detect { - timeout: test.detect_timeout, - }, - } - }; - - let rx = test - .index - .write() - .pod_server_rx("ns-0", "pod-0", 2222) - .expect("pod-0.ns-0 should exist"); - assert_eq!(*rx.borrow(), config); - } +struct TestConfig { + index: SharedIndex, + detect_timeout: time::Duration, + default_policy: DefaultPolicy, + cluster: ClusterInfo, + _tracing: tracing::subscriber::DefaultGuard, } -// === Helpers === - const DEFAULTS: [DefaultPolicy; 5] = [ DefaultPolicy::Deny, DefaultPolicy::Allow { @@ -363,21 +56,21 @@ const DEFAULTS: [DefaultPolicy; 5] = [ ]; fn mk_pod( - ns: impl Into, - name: impl Into, - containers: impl IntoIterator, impl IntoIterator)>, + ns: impl ToString, + name: impl ToString, + containers: impl IntoIterator)>, ) -> k8s::Pod { k8s::Pod { metadata: k8s::ObjectMeta { - namespace: Some(ns.into()), - name: Some(name.into()), + namespace: Some(ns.to_string()), + name: Some(name.to_string()), ..Default::default() }, spec: Some(k8s::api::core::v1::PodSpec { containers: containers .into_iter() .map(|(name, ports)| k8s::api::core::v1::Container { - name: name.into(), + name: name.to_string(), ports: Some(ports.into_iter().collect()), ..Default::default() }) @@ -389,8 +82,8 @@ fn mk_pod( } fn mk_server( - ns: impl Into, - name: impl Into, + ns: impl ToString, + name: impl ToString, port: Port, srv_labels: impl IntoIterator, pod_labels: impl IntoIterator, @@ -398,8 +91,8 @@ fn mk_server( ) -> k8s::policy::Server { k8s::policy::Server { metadata: k8s::ObjectMeta { - namespace: Some(ns.into()), - name: Some(name.into()), + namespace: Some(ns.to_string()), + name: Some(name.to_string()), labels: Some( srv_labels .into_iter() @@ -416,38 +109,10 @@ fn mk_server( } } -fn mk_server_authz( - ns: impl Into, - name: impl Into, - selector: ServerSelector, - client: k8s::policy::server_authorization::Client, -) -> k8s::policy::ServerAuthorization { - k8s::policy::ServerAuthorization { - metadata: k8s::ObjectMeta { - namespace: Some(ns.into()), - name: Some(name.into()), - ..Default::default() - }, - spec: k8s::policy::ServerAuthorizationSpec { - server: match selector { - ServerSelector::Name(n) => k8s::policy::server_authorization::Server { - name: Some(n), - selector: None, - }, - ServerSelector::Selector(s) => k8s::policy::server_authorization::Server { - selector: Some(s), - name: None, - }, - }, - client, - }, - } -} - fn mk_default_policy( da: DefaultPolicy, cluster_nets: Vec, -) -> HashMap { +) -> HashMap { let all_nets = vec![Ipv4Net::default().into(), Ipv6Net::default().into()]; let cluster_nets = cluster_nets.into_iter().map(NetworkMatch::from).collect(); @@ -460,7 +125,7 @@ fn mk_default_policy( authenticated_only: true, cluster_only: false, } => Some(( - "default:all-authenticated".into(), + AuthorizationRef::Default("all-authenticated".to_string()), ClientAuthorization { authentication: authed, networks: all_nets, @@ -470,7 +135,7 @@ fn mk_default_policy( authenticated_only: false, cluster_only: false, } => Some(( - "default:all-unauthenticated".into(), + AuthorizationRef::Default("all-unauthenticated".to_string()), ClientAuthorization { authentication: ClientAuthentication::Unauthenticated, networks: all_nets, @@ -480,7 +145,7 @@ fn mk_default_policy( authenticated_only: true, cluster_only: true, } => Some(( - "default:cluster-authenticated".into(), + AuthorizationRef::Default("cluster-authenticated".to_string()), ClientAuthorization { authentication: authed, networks: cluster_nets, @@ -490,7 +155,7 @@ fn mk_default_policy( authenticated_only: false, cluster_only: true, } => Some(( - "default:cluster-unauthenticated".into(), + AuthorizationRef::Default("cluster-unauthenticated".to_string()), ClientAuthorization { authentication: ClientAuthentication::Unauthenticated, networks: cluster_nets, @@ -501,14 +166,6 @@ fn mk_default_policy( .collect() } -struct TestConfig { - index: SharedIndex, - detect_timeout: time::Duration, - default_policy: DefaultPolicy, - cluster: ClusterInfo, - _tracing: tracing::subscriber::DefaultGuard, -} - impl TestConfig { fn from_default_policy(default_policy: DefaultPolicy) -> Self { let _tracing = Self::init_tracing(); @@ -533,7 +190,7 @@ impl TestConfig { fn default_server(&self) -> InboundServer { InboundServer { - name: format!("default:{}", self.default_policy), + reference: ServerRef::Default(self.default_policy.to_string()), authorizations: mk_default_policy(self.default_policy, self.cluster.networks.clone()), protocol: ProxyProtocol::Detect { timeout: self.detect_timeout, diff --git a/policy-controller/k8s/index/src/tests/annotation.rs b/policy-controller/k8s/index/src/tests/annotation.rs new file mode 100644 index 0000000000000..9864ac519e8c4 --- /dev/null +++ b/policy-controller/k8s/index/src/tests/annotation.rs @@ -0,0 +1,132 @@ +use super::*; + +/// Tests that pod servers are configured with defaults based on the +/// workload-defined `DefaultPolicy` policy. +/// +/// Iterates through each default policy and validates that it produces expected +/// configurations. +#[test] +fn default_policy_annotated() { + for default in &DEFAULTS { + let test = TestConfig::from_default_policy(match *default { + // Invert default to ensure override applies. + DefaultPolicy::Deny => DefaultPolicy::Allow { + authenticated_only: false, + cluster_only: false, + }, + _ => DefaultPolicy::Deny, + }); + + // Initially create the pod without an annotation and check that it gets + // the global default. + let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + test.index + .write() + .reset(vec![pod.clone()], Default::default()); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 2222) + .expect("pod-0.ns-0 should exist"); + assert_eq!( + rx.borrow_and_update().reference, + ServerRef::Default(test.default_policy.to_string()), + ); + + // Update the annotation on the pod and check that the watch is updated + // with the new default. + pod.annotations_mut().insert( + "config.linkerd.io/default-inbound-policy".into(), + default.to_string(), + ); + test.index.write().apply(pod); + assert!(rx.has_changed().unwrap()); + assert_eq!( + rx.borrow().reference, + ServerRef::Default(default.to_string()) + ); + } +} + +/// Tests that an invalid workload annotation is ignored in favor of the global +/// default. +#[tokio::test] +async fn default_policy_annotated_invalid() { + let test = TestConfig::default(); + + let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + p.annotations_mut().insert( + "config.linkerd.io/default-inbound-policy".into(), + "bogus".into(), + ); + test.index.write().reset(vec![p], Default::default()); + + // Lookup port 2222 -> default config. + let rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 2222) + .expect("pod must exist in lookups"); + assert_eq!(*rx.borrow(), test.default_server()); +} + +#[test] +fn opaque_annotated() { + for default in &DEFAULTS { + let test = TestConfig::from_default_policy(*default); + + let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + p.annotations_mut() + .insert("config.linkerd.io/opaque-ports".into(), "2222".into()); + test.index.write().reset(vec![p], Default::default()); + + let mut server = test.default_server(); + server.protocol = ProxyProtocol::Opaque; + + let rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 2222) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow(), server); + } +} + +#[test] +fn authenticated_annotated() { + for default in &DEFAULTS { + let test = TestConfig::from_default_policy(*default); + + let mut p = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + p.annotations_mut().insert( + "config.linkerd.io/proxy-require-identity-inbound-ports".into(), + "2222".into(), + ); + test.index.write().reset(vec![p], Default::default()); + + let config = { + let policy = match *default { + DefaultPolicy::Allow { cluster_only, .. } => DefaultPolicy::Allow { + cluster_only, + authenticated_only: true, + }, + DefaultPolicy::Deny => DefaultPolicy::Deny, + }; + InboundServer { + reference: ServerRef::Default(policy.to_string()), + authorizations: mk_default_policy(policy, test.cluster.networks), + protocol: ProxyProtocol::Detect { + timeout: test.detect_timeout, + }, + } + }; + + let rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 2222) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow(), config); + } +} diff --git a/policy-controller/k8s/index/src/tests/authorization_policy.rs b/policy-controller/k8s/index/src/tests/authorization_policy.rs new file mode 100644 index 0000000000000..031dbeeee0f3f --- /dev/null +++ b/policy-controller/k8s/index/src/tests/authorization_policy.rs @@ -0,0 +1,158 @@ +use super::*; + +#[test] +fn links_authorization_policy_with_mtls_name() { + let test = TestConfig::default(); + + let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + pod.labels_mut() + .insert("app".to_string(), "app-0".to_string()); + test.index.write().apply(pod); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 8080) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow_and_update(), test.default_server()); + + test.index.write().apply(mk_server( + "ns-0", + "srv-8080", + Port::Number(8080), + None, + Some(("app", "app-0")), + Some(k8s::policy::server::ProxyProtocol::Http1), + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-8080".to_string()), + authorizations: Default::default(), + protocol: ProxyProtocol::Http1, + }, + ); + + let authz = ClientAuthorization { + networks: vec!["10.0.0.0/8".parse::().unwrap().into()], + authentication: ClientAuthentication::TlsAuthenticated(vec![IdentityMatch::Exact( + "foo.bar".to_string(), + )]), + }; + test.index.write().apply(mk_authorization_policy( + "ns-0", + "authz-foo", + "srv-8080", + vec![ + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + name: "net-foo".to_string(), + ..Default::default() + }, + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "MeshTLSAuthentication".to_string(), + namespace: Some("ns-1".to_string()), + name: "mtls-bar".to_string(), + }, + ], + )); + test.index.write().apply(mk_network_authentication( + "ns-0".to_string(), + "net-foo".to_string(), + vec![k8s::policy::network_authentication::Network { + cidr: "10.0.0.0/8".parse().unwrap(), + except: None, + }], + )); + test.index.write().apply(mk_meshtls_authentication( + "ns-1", + "mtls-bar", + Some("foo.bar".to_string()), + None, + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow(), + InboundServer { + reference: ServerRef::Server("srv-8080".to_string()), + authorizations: hashmap!( + AuthorizationRef::AuthorizationPolicy("authz-foo".to_string()) => authz + ) + .into_iter() + .collect(), + protocol: ProxyProtocol::Http1, + }, + ); +} + +fn mk_authorization_policy( + ns: impl ToString, + name: impl ToString, + server: impl ToString, + authns: impl IntoIterator, +) -> k8s::policy::AuthorizationPolicy { + k8s::policy::AuthorizationPolicy { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some(name.to_string()), + ..Default::default() + }, + spec: k8s::policy::AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: server.to_string(), + }, + required_authentication_refs: authns.into_iter().collect(), + }, + } +} + +fn mk_meshtls_authentication( + ns: impl ToString, + name: impl ToString, + identities: impl IntoIterator, + refs: impl IntoIterator, +) -> k8s::policy::MeshTLSAuthentication { + let identities = identities.into_iter().collect::>(); + let identity_refs = refs.into_iter().collect::>(); + k8s::policy::MeshTLSAuthentication { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some(name.to_string()), + ..Default::default() + }, + spec: k8s::policy::MeshTLSAuthenticationSpec { + identities: if identities.is_empty() { + None + } else { + Some(identities) + }, + identity_refs: if identity_refs.is_empty() { + None + } else { + Some(identity_refs) + }, + }, + } +} + +fn mk_network_authentication( + ns: impl ToString, + name: impl ToString, + networks: impl IntoIterator, +) -> k8s::policy::NetworkAuthentication { + k8s::policy::NetworkAuthentication { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some(name.to_string()), + ..Default::default() + }, + spec: k8s::policy::NetworkAuthenticationSpec { + networks: networks.into_iter().collect(), + }, + } +} diff --git a/policy-controller/k8s/index/src/tests/server.rs b/policy-controller/k8s/index/src/tests/server.rs new file mode 100644 index 0000000000000..3d56294f856bd --- /dev/null +++ b/policy-controller/k8s/index/src/tests/server.rs @@ -0,0 +1,127 @@ +use super::*; + +#[test] +fn links_named_server_port() { + let test = TestConfig::default(); + + let mut pod = mk_pod( + "ns-0", + "pod-0", + Some(( + "container-0", + Some(ContainerPort { + name: Some("admin-http".to_string()), + container_port: 8080, + protocol: Some("TCP".to_string()), + ..ContainerPort::default() + }), + )), + ); + pod.labels_mut() + .insert("app".to_string(), "app-0".to_string()); + test.index.write().apply(pod); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 8080) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow_and_update(), test.default_server()); + + test.index.write().apply(mk_server( + "ns-0", + "srv-admin-http", + Port::Name("admin-http".to_string()), + None, + Some(("app", "app-0")), + Some(k8s::policy::server::ProxyProtocol::Http1), + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-admin-http".to_string()), + authorizations: Default::default(), + protocol: ProxyProtocol::Http1, + }, + ); +} + +#[test] +fn links_unnamed_server_port() { + let test = TestConfig::default(); + + let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + pod.labels_mut() + .insert("app".to_string(), "app-0".to_string()); + test.index.write().apply(pod); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 8080) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow_and_update(), test.default_server()); + + test.index.write().apply(mk_server( + "ns-0", + "srv-8080", + Port::Number(8080), + None, + Some(("app", "app-0")), + Some(k8s::policy::server::ProxyProtocol::Http1), + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-8080".to_string()), + authorizations: Default::default(), + protocol: ProxyProtocol::Http1, + }, + ); +} + +#[test] +fn server_update_deselects_pod() { + let test = TestConfig::default(); + + test.index.write().reset( + vec![mk_pod("ns-0", "pod-0", Some(("container-0", None)))], + Default::default(), + ); + + let mut srv = mk_server( + "ns-0", + "srv-0", + Port::Number(2222), + None, + None, + Some(k8s::policy::server::ProxyProtocol::Http2), + ); + test.index + .write() + .reset(vec![srv.clone()], Default::default()); + + // The default policy applies for all ports. + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 2222) + .unwrap(); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-0".to_string()), + protocol: ProxyProtocol::Http2, + authorizations: Default::default(), + } + ); + + test.index.write().apply({ + srv.spec.pod_selector = Some(("label", "value")).into_iter().collect(); + srv + }); + assert!(rx.has_changed().unwrap()); + assert_eq!(*rx.borrow(), test.default_server()); +} diff --git a/policy-controller/k8s/index/src/tests/server_authorization.rs b/policy-controller/k8s/index/src/tests/server_authorization.rs new file mode 100644 index 0000000000000..c5df2f814190a --- /dev/null +++ b/policy-controller/k8s/index/src/tests/server_authorization.rs @@ -0,0 +1,104 @@ +use super::*; + +#[test] +fn links_server_authz_by_name() { + link_server_authz(ServerSelector::Name("srv-8080".to_string())) +} + +#[test] +fn links_server_authz_by_label() { + link_server_authz(ServerSelector::Selector( + Some(("app", "app-0")).into_iter().collect(), + )); +} + +#[inline] +fn link_server_authz(selector: ServerSelector) { + let test = TestConfig::default(); + + let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + pod.labels_mut() + .insert("app".to_string(), "app-0".to_string()); + test.index.write().apply(pod); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 8080) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow_and_update(), test.default_server()); + + test.index.write().apply(mk_server( + "ns-0", + "srv-8080", + Port::Number(8080), + Some(("app", "app-0")), + Some(("app", "app-0")), + Some(k8s::policy::server::ProxyProtocol::Http1), + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-8080".to_string()), + authorizations: Default::default(), + protocol: ProxyProtocol::Http1, + }, + ); + test.index.write().apply(mk_server_authz( + "ns-0", + "authz-foo", + selector, + k8s::policy::server_authorization::Client { + networks: Some(vec![k8s::policy::server_authorization::Network { + cidr: "10.0.0.0/8".parse().unwrap(), + except: None, + }]), + unauthenticated: false, + mesh_tls: Some(k8s::policy::server_authorization::MeshTls { + identities: Some(vec!["foo.bar".to_string()]), + ..Default::default() + }), + }, + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + rx.borrow().reference, + ServerRef::Server("srv-8080".to_string()) + ); + assert_eq!(rx.borrow().protocol, ProxyProtocol::Http1,); + assert!(rx + .borrow() + .authorizations + .contains_key(&AuthorizationRef::ServerAuthorization( + "authz-foo".to_string() + ))); +} + +fn mk_server_authz( + ns: impl ToString, + name: impl ToString, + selector: ServerSelector, + client: k8s::policy::server_authorization::Client, +) -> k8s::policy::ServerAuthorization { + k8s::policy::ServerAuthorization { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some(name.to_string()), + ..Default::default() + }, + spec: k8s::policy::ServerAuthorizationSpec { + server: match selector { + ServerSelector::Name(n) => k8s::policy::server_authorization::Server { + name: Some(n), + selector: None, + }, + ServerSelector::Selector(s) => k8s::policy::server_authorization::Server { + selector: Some(s), + name: None, + }, + }, + client, + }, + } +} diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs index a44c92345738c..b80da6c7cac8d 100644 --- a/policy-controller/src/admission.rs +++ b/policy-controller/src/admission.rs @@ -1,10 +1,15 @@ use crate::k8s::{ labels, - policy::{Server, ServerAuthorization, ServerAuthorizationSpec, ServerSpec}, + policy::{ + AuthorizationPolicy, AuthorizationPolicySpec, MeshTLSAuthentication, + MeshTLSAuthenticationSpec, NetworkAuthentication, NetworkAuthenticationSpec, Server, + ServerAuthorization, ServerAuthorizationSpec, ServerSpec, + }, }; use anyhow::{anyhow, bail, Result}; use futures::future; use hyper::{body::Buf, http, Body, Request, Response}; +use k8s_openapi::api::core::v1::ServiceAccount; use kube::{core::DynamicObject, Resource, ResourceExt}; use serde::de::DeserializeOwned; use std::task; @@ -91,6 +96,18 @@ impl Admission { } async fn admit(self, req: AdmissionRequest) -> AdmissionResponse { + if is_kind::(&req) { + return self.admit_spec::(req).await; + } + + if is_kind::(&req) { + return self.admit_spec::(req).await; + } + + if is_kind::(&req) { + return self.admit_spec::(req).await; + } + if is_kind::(&req) { return self.admit_spec::(req).await; }; @@ -170,6 +187,71 @@ fn parse_spec(req: AdmissionRequest) -> Result<(String, Str Ok((ns, name, spec)) } +#[async_trait::async_trait] +impl Validate for Admission { + async fn validate(self, _ns: &str, _name: &str, spec: AuthorizationPolicySpec) -> Result<()> { + // TODO support namespace references? + if !spec.target_ref.targets_kind::() { + bail!( + "invalid targetRef kind: {}", + spec.target_ref.canonical_kind() + ); + } + + let mtls_authns_count = spec + .required_authentication_refs + .iter() + .filter(|authn| authn.targets_kind::()) + .count(); + if mtls_authns_count > 1 { + bail!("only a single MeshTLSAuthentication may be set"); + } + + let net_authns_count = spec + .required_authentication_refs + .iter() + .filter(|authn| authn.targets_kind::()) + .count(); + if net_authns_count > 1 { + bail!("only a single NetworkAuthentication may be set"); + } + + if mtls_authns_count + net_authns_count < spec.required_authentication_refs.len() { + let kinds = spec + .required_authentication_refs + .iter() + .filter(|authn| { + !authn.targets_kind::() + && !authn.targets_kind::() + }) + .map(|authn| authn.canonical_kind()) + .collect::>(); + bail!("unsupported authentication kind(s): {}", kinds.join(", ")); + } + + if mtls_authns_count + net_authns_count == 0 { + bail!("at least one authentication reference must be set"); + } + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Validate for Admission { + async fn validate(self, _ns: &str, _name: &str, spec: MeshTLSAuthenticationSpec) -> Result<()> { + // The CRD validates identity strings, but does not validate identity references. + + for id in spec.identity_refs.iter().flatten() { + if !id.targets_kind::() { + bail!("invalid identity target kind: {}", id.canonical_kind()); + } + } + + Ok(()) + } +} + #[async_trait::async_trait] impl Validate for Admission { /// Checks that `spec` doesn't select the same pod/ports as other existing Servers @@ -212,6 +294,35 @@ impl Admission { } } +#[async_trait::async_trait] +impl Validate for Admission { + async fn validate(self, _ns: &str, _name: &str, spec: NetworkAuthenticationSpec) -> Result<()> { + if spec.networks.is_empty() { + bail!("at least one network must be specified"); + } + for net in spec.networks.into_iter() { + for except in net.except.into_iter().flatten() { + if except.contains(&net.cidr) { + bail!( + "cidr '{}' is completely negated by exception '{}'", + net.cidr, + except + ); + } + if !net.cidr.contains(&except) { + bail!( + "cidr '{}' does not include exception '{}'", + net.cidr, + except + ); + } + } + } + + Ok(()) + } +} + #[async_trait::async_trait] impl Validate for Admission { async fn validate(self, _ns: &str, _name: &str, spec: ServerAuthorizationSpec) -> Result<()> { diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs index a95d0ea097f2d..444621c64dd09 100644 --- a/policy-controller/src/main.rs +++ b/policy-controller/src/main.rs @@ -125,6 +125,27 @@ async fn main() -> Result<()> { .instrument(info_span!("serverauthorizations")), ); + let authz_policies = + runtime.watch_all::(ListParams::default()); + tokio::spawn( + kubert::index::namespaced(index.clone(), authz_policies) + .instrument(info_span!("authorizationpolicies")), + ); + + let mtls_authns = + runtime.watch_all::(ListParams::default()); + tokio::spawn( + kubert::index::namespaced(index.clone(), mtls_authns) + .instrument(info_span!("meshtlsauthentications")), + ); + + let network_authns = + runtime.watch_all::(ListParams::default()); + tokio::spawn( + kubert::index::namespaced(index.clone(), network_authns) + .instrument(info_span!("networkauthentications")), + ); + // Run the gRPC server, serving results by looking up against the index handle. tokio::spawn(grpc( grpc_addr, diff --git a/policy-test/src/curl.rs b/policy-test/src/curl.rs index bf8a67238ceb9..5a935dcd374d6 100644 --- a/policy-test/src/curl.rs +++ b/policy-test/src/curl.rs @@ -155,7 +155,7 @@ impl Runner { args: Some( vec![ "wait", - "--timeout=60s", + "--timeout=120s", "--for=delete", "--namespace", ns, @@ -219,13 +219,22 @@ impl Running { &self.name, |obj: Option<&k8s::Pod>| -> bool { obj.and_then(get_exit_code).is_some() }, ); - match time::timeout(time::Duration::from_secs(60), finished).await { + match time::timeout(time::Duration::from_secs(120), finished).await { Ok(Ok(())) => {} Ok(Err(error)) => panic!("Failed to wait for exit code: {}: {}", self.name, error), Err(_timeout) => panic!("Timeout waiting for exit code: {}", self.name), }; let curl_pod = api.get(&self.name).await.expect("pod must exist"); - get_exit_code(&curl_pod).expect("curl pod must have an exit code") + let ex = get_exit_code(&curl_pod).expect("curl pod must have an exit code"); + + if let Err(error) = api + .delete(&self.name, &kube::api::DeleteParams::background()) + .await + { + tracing::trace!(%error, name = %self.name, "Failed to delete pod"); + } + + ex } } diff --git a/policy-test/src/grpc.rs b/policy-test/src/grpc.rs index 2ca114c7fc0fc..9f9fe8c3e06f8 100644 --- a/policy-test/src/grpc.rs +++ b/policy-test/src/grpc.rs @@ -14,10 +14,10 @@ macro_rules! assert_is_default_all_unauthenticated { ($config:expr) => { assert_eq!( $config.labels, - Some(( - "name".to_string(), - "default:all-unauthenticated".to_string() - )) + vec![ + ("kind".to_string(), "default".to_string()), + ("name".to_string(), "all-unauthenticated".to_string()), + ] .into_iter() .collect() ); diff --git a/policy-test/tests/admit_authorization_policy.rs b/policy-test/tests/admit_authorization_policy.rs new file mode 100644 index 0000000000000..b6008053e8405 --- /dev/null +++ b/policy-test/tests/admit_authorization_policy.rs @@ -0,0 +1,212 @@ +use linkerd_policy_controller_k8s_api::{ + self as api, + policy::{AuthorizationPolicy, AuthorizationPolicySpec, LocalTargetRef, NamespacedTargetRef}, +}; +use linkerd_policy_test::admission; + +#[tokio::test(flavor = "current_thread")] +async fn accepts_valid() { + admission::accepts(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "api".to_string(), + }, + required_authentication_refs: vec![ + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "MeshTLSAuthentication".to_string(), + name: "mtls-clients".to_string(), + ..Default::default() + }, + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + name: "cluster-nets".to_string(), + namespace: Some("linkerd".to_string()), + }, + ], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn accepts_valid_with_only_meshtls() { + admission::accepts(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "api".to_string(), + }, + required_authentication_refs: vec![NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "MeshTLSAuthentication".to_string(), + name: "mtls-clients".to_string(), + ..Default::default() + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn accepts_valid_with_only_network() { + admission::accepts(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "api".to_string(), + }, + required_authentication_refs: vec![NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + name: "cluster-nets".to_string(), + namespace: Some("linkerd".to_string()), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_empty() { + admission::rejects(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec::default(), + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_empty_required_authentications() { + admission::rejects(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "deny".to_string(), + }, + required_authentication_refs: vec![], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_target_ref_deployment() { + admission::rejects(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("apps".to_string()), + kind: "Deployment".to_string(), + name: "someapp".to_string(), + }, + required_authentication_refs: vec![NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + namespace: Some("linkerd".to_string()), + name: "cluster-nets".to_string(), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_duplicate_mtls_authns() { + admission::rejects(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "some-srv".to_string(), + }, + required_authentication_refs: vec![ + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "MeshTLSAuthentication".to_string(), + namespace: Some("some-ns".to_string()), + name: "some-ids".to_string(), + }, + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "MeshTLSAuthentication".to_string(), + namespace: Some("other-ns".to_string()), + name: "other-ids".to_string(), + }, + ], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_duplicate_network_authns() { + admission::rejects(|ns| AuthorizationPolicy { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: AuthorizationPolicySpec { + target_ref: LocalTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "Server".to_string(), + name: "some-srv".to_string(), + }, + required_authentication_refs: vec![ + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + namespace: Some("some-ns".to_string()), + name: "some-nets".to_string(), + }, + NamespacedTargetRef { + group: Some("policy.linkerd.io".to_string()), + kind: "NetworkAuthentication".to_string(), + namespace: Some("other-ns".to_string()), + name: "other-nets".to_string(), + }, + ], + }, + }) + .await; +} diff --git a/policy-test/tests/admit_meshtls_authentication.rs b/policy-test/tests/admit_meshtls_authentication.rs new file mode 100644 index 0000000000000..bcf7e200b7501 --- /dev/null +++ b/policy-test/tests/admit_meshtls_authentication.rs @@ -0,0 +1,74 @@ +use linkerd_policy_controller_k8s_api::{ + self as api, + policy::{MeshTLSAuthentication, MeshTLSAuthenticationSpec, NamespacedTargetRef}, +}; +use linkerd_policy_test::admission; + +#[tokio::test(flavor = "current_thread")] +async fn accepts_valid_ref() { + admission::accepts(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec { + identity_refs: Some(vec![NamespacedTargetRef { + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + ..Default::default() + }]), + ..Default::default() + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn accepts_strings() { + admission::accepts(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec { + identities: Some(vec!["example.id".to_string()]), + ..Default::default() + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_empty() { + admission::rejects(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec::default(), + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_both_refs_and_strings() { + admission::rejects(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec { + identities: Some(vec!["example.id".to_string()]), + identity_refs: Some(vec![NamespacedTargetRef { + kind: "ServiceAccount".to_string(), + name: "default".to_string(), + ..Default::default() + }]), + }, + }) + .await; +} diff --git a/policy-test/tests/admit_network_authentication.rs b/policy-test/tests/admit_network_authentication.rs new file mode 100644 index 0000000000000..beb5011a48a26 --- /dev/null +++ b/policy-test/tests/admit_network_authentication.rs @@ -0,0 +1,142 @@ +use linkerd_policy_controller_k8s_api::{ + self as api, + policy::network_authentication::{Network, NetworkAuthentication, NetworkAuthenticationSpec}, +}; +use linkerd_policy_test::admission; + +#[tokio::test(flavor = "current_thread")] +async fn accepts_valid() { + admission::accepts(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { + networks: vec![ + Network { + cidr: "10.1.0.0/24".parse().unwrap(), + except: None, + }, + Network { + cidr: "10.1.1.0/24".parse().unwrap(), + except: Some(vec!["10.1.1.0/28".parse().unwrap()]), + }, + ], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn accepts_ip_except() { + admission::accepts(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { + networks: vec![Network { + cidr: "10.1.0.0/16".parse().unwrap(), + except: Some(vec!["10.1.1.1".parse().unwrap()]), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_except_whole_cidr() { + admission::rejects(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { + networks: vec![Network { + cidr: "10.1.1.0/24".parse().unwrap(), + except: Some(vec!["10.1.0.0/16".parse().unwrap()]), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_except_not_in_cidr() { + admission::rejects(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { + networks: vec![Network { + cidr: "10.1.1.0/24".parse().unwrap(), + except: Some(vec!["10.1.2.0/24".parse().unwrap()]), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_invalid_cidr() { + // Duplicate the CRD with relaxed validation so we can send an invalid CIDR value. + #[derive( + Clone, + Debug, + Default, + kube::CustomResource, + serde::Deserialize, + serde::Serialize, + schemars::JsonSchema, + )] + #[kube( + group = "policy.linkerd.io", + version = "v1alpha1", + kind = "NetworkAuthentication", + namespaced + )] + #[serde(rename_all = "camelCase")] + pub struct NetworkAuthenticationSpec { + pub networks: Vec, + } + + #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)] + #[serde(rename_all = "camelCase")] + pub struct Network { + pub cidr: String, + pub except: Option>, + } + + admission::rejects(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { + networks: vec![Network { + cidr: "10.1.0.0/16".to_string(), + except: Some(vec!["bogus".to_string()]), + }], + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_empty() { + admission::rejects(|ns| NetworkAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: NetworkAuthenticationSpec { networks: vec![] }, + }) + .await; +} diff --git a/policy-test/tests/api.rs b/policy-test/tests/api.rs index acc766a5b07ae..2560ce6057f8e 100644 --- a/policy-test/tests/api.rs +++ b/policy-test/tests/api.rs @@ -1,5 +1,6 @@ use futures::prelude::*; use kube::ResourceExt; +use linkerd_policy_controller_core::{Ipv4Net, Ipv6Net}; use linkerd_policy_controller_k8s_api as k8s; use linkerd_policy_test::{ assert_is_default_all_unauthenticated, assert_protocol_detect, create, create_ready_pod, grpc, @@ -55,7 +56,7 @@ async fn server_with_server_authorization() { assert_eq!(config.authorizations, vec![]); assert_eq!( config.labels, - convert_args!(hashmap!("name" => "linkerd-admin")), + convert_args!(hashmap!("kind" => "server", "name" => "linkerd-admin")), ); // Create a server authorizaation that refers to the `linkerd-admin` @@ -99,6 +100,7 @@ async fn server_with_server_authorization() { assert_eq!( config.authorizations.first().unwrap().labels, convert_args!(hashmap!( + "kind" => "serverauthorization", "name" => "all-admin", )), ); @@ -118,7 +120,7 @@ async fn server_with_server_authorization() { ); assert_eq!( config.labels, - convert_args!(hashmap!("name" => server.name())) + convert_args!(hashmap!("kind" => "server", "name" => server.name())) ); // Delete the `Server` and ensure that the update reverts to the @@ -139,6 +141,142 @@ async fn server_with_server_authorization() { .await; } +/// Creates a pod, watches its policy, and updates policy resources that impact +/// the watch. +#[tokio::test(flavor = "current_thread")] +async fn server_with_authorization_policy() { + with_temp_ns(|client, ns| async move { + // Create a pod that does nothing. It's injected with a proxy, so we can + // attach policies to its admin server. + let pod = create_ready_pod(&client, mk_pause(&ns, "pause")).await; + tracing::trace!(?pod); + + // Port-forward to the control plane and start watching the pod's admin + // server's policy and ensure that the first update uses the default + // policy. + let mut policy_api = grpc::PolicyClient::port_forwarded(&client).await; + let mut rx = policy_api + .watch_port(&ns, &pod.name(), 4191) + .await + .expect("failed to establish watch"); + let config = rx + .next() + .await + .expect("watch must not fail") + .expect("watch must return an initial config"); + tracing::trace!(?config); + assert_is_default_all_unauthenticated!(config); + assert_protocol_detect!(config); + + // Create a server that selects the pod's proxy admin server and ensure + // that the update now uses this server, which has no authorizations + let server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await; + let config = rx + .next() + .await + .expect("watch must not fail") + .expect("watch must return an updated config"); + tracing::trace!(?config); + assert_eq!( + config.protocol, + Some(grpc::inbound::ProxyProtocol { + kind: Some(grpc::inbound::proxy_protocol::Kind::Http1( + grpc::inbound::proxy_protocol::Http1::default() + )), + }), + ); + assert_eq!(config.authorizations, vec![]); + assert_eq!( + config.labels, + convert_args!(hashmap!("kind" => "server", "name" => server.name())) + ); + + let all_nets = create( + &client, + k8s::policy::NetworkAuthentication { + metadata: kube::api::ObjectMeta { + namespace: Some(ns.clone()), + name: Some("all-admin".to_string()), + ..Default::default() + }, + spec: k8s::policy::NetworkAuthenticationSpec { + networks: vec![ + k8s::policy::network_authentication::Network { + cidr: Ipv4Net::default().into(), + except: None, + }, + k8s::policy::network_authentication::Network { + cidr: Ipv6Net::default().into(), + except: None, + }, + ], + }, + }, + ) + .await; + + let authz_policy = create( + &client, + k8s::policy::AuthorizationPolicy { + metadata: kube::api::ObjectMeta { + namespace: Some(ns.clone()), + name: Some("all-admin".to_string()), + ..Default::default() + }, + spec: k8s::policy::AuthorizationPolicySpec { + target_ref: k8s::policy::LocalTargetRef::from_resource(&server), + required_authentication_refs: vec![ + k8s::policy::NamespacedTargetRef::from_resource(&all_nets), + ], + }, + }, + ) + .await; + + let config = time::timeout(time::Duration::from_secs(10), rx.next()) + .await + .expect("watch must update within 10s") + .expect("watch must not fail") + .expect("watch must return an updated config"); + tracing::trace!(?config); + assert_eq!( + config.protocol, + Some(grpc::inbound::ProxyProtocol { + kind: Some(grpc::inbound::proxy_protocol::Kind::Http1( + grpc::inbound::proxy_protocol::Http1::default() + )), + }), + ); + assert_eq!(config.authorizations.len(), 1); + assert_eq!( + config.authorizations.first().unwrap().labels, + convert_args!(hashmap!( + "kind" => "authorizationpolicy", + "name" => authz_policy.name(), + )) + ); + assert_eq!( + *config + .authorizations + .first() + .unwrap() + .authentication + .as_ref() + .unwrap(), + grpc::inbound::Authn { + permit: Some(grpc::inbound::authn::Permit::Unauthenticated( + grpc::inbound::authn::PermitUnauthenticated {} + )), + } + ); + assert_eq!( + config.labels, + convert_args!(hashmap!("kind" => "server", "name" => server.name())) + ); + }) + .await; +} + fn mk_pause(ns: &str, name: &str) -> k8s::Pod { k8s::Pod { metadata: k8s::ObjectMeta { diff --git a/policy-test/tests/e2e_authorization_policy.rs b/policy-test/tests/e2e_authorization_policy.rs new file mode 100644 index 0000000000000..ed5e0318585a8 --- /dev/null +++ b/policy-test/tests/e2e_authorization_policy.rs @@ -0,0 +1,375 @@ +use linkerd_policy_controller_k8s_api::{ + self as k8s, + policy::{LocalTargetRef, NamespacedTargetRef}, +}; +use linkerd_policy_test::{create, create_ready_pod, curl, nginx, with_temp_ns, LinkerdInject}; + +#[tokio::test(flavor = "current_thread")] +async fn meshtls() { + with_temp_ns(|client, ns| async move { + // First create all of the policies we'll need so that the nginx pod + // starts up with the correct policy (to prevent races). + // + // The policy requires that all connections are authenticated with MeshTLS. + let (srv, all_mtls) = tokio::join!( + create(&client, nginx::server(&ns)), + create(&client, all_authenticated(&ns)) + ); + create( + &client, + authz_policy( + &ns, + "nginx", + &srv, + Some(NamespacedTargetRef::from_resource(&all_mtls)), + ), + ) + .await; + + // Create the nginx pod and wait for it to be ready. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)) + ); + + let curl = curl::Runner::init(&client, &ns).await; + let (injected, uninjected) = tokio::join!( + curl.run("curl-injected", "http://nginx", LinkerdInject::Enabled), + curl.run("curl-uninjected", "http://nginx", LinkerdInject::Disabled), + ); + let (injected_status, uninjected_status) = + tokio::join!(injected.exit_code(), uninjected.exit_code()); + assert_eq!( + injected_status, 0, + "uninjected curl must fail to contact nginx" + ); + assert_ne!(uninjected_status, 0, "injected curl must contact nginx"); + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn network() { + // In order to test the network policy, we need to create the client pod + // before creating the authorization policy. To avoid races, we do this by + // creating a `curl-lock` configmap that prevents curl from actually being + // executed. Once nginx is running with the correct policy, the configmap is + // deleted to unblock the curl pods. + with_temp_ns(|client, ns| async move { + let curl = curl::Runner::init(&client, &ns).await; + curl.create_lock().await; + + // Create a curl pod and wait for it to get an IP. + let blessed = curl + .run("curl-blessed", "http://nginx", LinkerdInject::Disabled) + .await; + let blessed_ip = blessed.ip().await; + tracing::debug!(curl.blessed.ip = %blessed_ip); + + // Once we know the IP of the (blocked) pod, create an nginx + // authorization policy that permits connections from this pod. + let (srv, allow_ips) = tokio::join!( + create(&client, nginx::server(&ns)), + create(&client, allow_ips(&ns, Some(blessed_ip))) + ); + create( + &client, + authz_policy( + &ns, + "nginx", + &srv, + Some(NamespacedTargetRef::from_resource(&allow_ips)), + ), + ) + .await; + + // Start nginx with the policy. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)) + ); + + // Once the nginx pod is ready, delete the `curl-lock` configmap to + // unblock curl from running. + curl.delete_lock().await; + + // The blessed pod should be able to connect to the nginx pod. + let status = blessed.exit_code().await; + assert_eq!(status, 0, "blessed curl pod must succeed"); + + // Create another curl pod that is not included in the authorization. It + // should fail to connect to the nginx pod. + let status = curl + .run("curl-cursed", "http://nginx", LinkerdInject::Disabled) + .await + .exit_code() + .await; + assert_ne!(status, 0, "cursed curl pod must fail"); + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn both() { + // In order to test the network policy, we need to create the client pod + // before creating the authorization policy. To avoid races, we do this by + // creating a `curl-lock` configmap that prevents curl from actually being + // executed. Once nginx is running with the correct policy, the configmap is + // deleted to unblock the curl pods. + with_temp_ns(|client, ns| async move { + let curl = curl::Runner::init(&client, &ns).await; + curl.create_lock().await; + + let (blessed_injected, blessed_uninjected) = tokio::join!( + curl.run( + "curl-blessed-injected", + "http://nginx", + LinkerdInject::Enabled, + ), + curl.run( + "curl-blessed-uninjected", + "http://nginx", + LinkerdInject::Disabled, + ) + ); + let (blessed_injected_ip, blessed_uninjected_ip) = + tokio::join!(blessed_injected.ip(), blessed_uninjected.ip(),); + tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip); + tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip); + + // Once we know the IP of the (blocked) pod, create an nginx + // authorization policy that permits connections from this pod. + let (srv, allow_ips, all_mtls) = tokio::join!( + create(&client, nginx::server(&ns)), + create( + &client, + allow_ips(&ns, vec![blessed_injected_ip, blessed_uninjected_ip]), + ), + create(&client, all_authenticated(&ns)) + ); + create( + &client, + authz_policy( + &ns, + "nginx", + &srv, + vec![ + NamespacedTargetRef::from_resource(&allow_ips), + NamespacedTargetRef::from_resource(&all_mtls), + ], + ), + ) + .await; + + // Start nginx with the policy. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)) + ); + + // Once the nginx pod is ready, delete the `curl-lock` configmap to + // unblock curl from running. + curl.delete_lock().await; + tracing::info!("unblocked curl"); + + let (blessed_injected_status, blessed_uninjected_status) = + tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code()); + // The blessed and injected pod should be able to connect to the nginx pod. + assert_eq!( + blessed_injected_status, 0, + "blessed injected curl pod must succeed" + ); + // The blessed and uninjected pod should NOT be able to connect to the nginx pod. + assert_ne!( + blessed_uninjected_status, 0, + "blessed uninjected curl pod must NOT succeed" + ); + + let (cursed_injected, cursed_uninjected) = tokio::join!( + curl.run( + "curl-cursed-injected", + "http://nginx", + LinkerdInject::Enabled, + ), + curl.run( + "curl-cursed-uninjected", + "http://nginx", + LinkerdInject::Disabled, + ) + ); + let (cursed_injected_status, cursed_uninjected_status) = + tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code(),); + assert_ne!( + cursed_injected_status, 0, + "cursed injected curl pod must fail" + ); + assert_ne!( + cursed_uninjected_status, 0, + "cursed uninjected curl pod must fail" + ); + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn either() { + // In order to test the network policy, we need to create the client pod + // before creating the authorization policy. To avoid races, we do this by + // creating a `curl-lock` configmap that prevents curl from actually being + // executed. Once nginx is running with the correct policy, the configmap is + // deleted to unblock the curl pods. + with_temp_ns(|client, ns| async move { + let curl = curl::Runner::init(&client, &ns).await; + curl.create_lock().await; + + let (blessed_injected, blessed_uninjected) = tokio::join!( + curl.run( + "curl-blessed-injected", + "http://nginx", + LinkerdInject::Enabled, + ), + curl.run( + "curl-blessed-uninjected", + "http://nginx", + LinkerdInject::Disabled, + ) + ); + let (blessed_injected_ip, blessed_uninjected_ip) = + tokio::join!(blessed_injected.ip(), blessed_uninjected.ip()); + tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip); + tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip); + + // Once we know the IP of the (blocked) pod, create an nginx + // authorization policy that permits connections from this pod. + let (srv, allow_ips, all_mtls) = tokio::join!( + create(&client, nginx::server(&ns)), + create(&client, allow_ips(&ns, vec![blessed_uninjected_ip])), + create(&client, all_authenticated(&ns)) + ); + tokio::join!( + create( + &client, + authz_policy( + &ns, + "nginx-from-ip", + &srv, + vec![NamespacedTargetRef::from_resource(&allow_ips)], + ), + ), + create( + &client, + authz_policy( + &ns, + "nginx-from-id", + &srv, + vec![NamespacedTargetRef::from_resource(&all_mtls)], + ), + ) + ); + + // Start nginx with the policy. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)), + ); + + // Once the nginx pod is ready, delete the `curl-lock` configmap to + // unblock curl from running. + curl.delete_lock().await; + tracing::info!("unblocking curl"); + + let (blessed_injected_status, blessed_uninjected_status) = + tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code()); + // The blessed and injected pod should be able to connect to the nginx pod. + assert_eq!( + blessed_injected_status, 0, + "blessed injected curl pod must succeed" + ); + // The blessed and uninjected pod should NOT be able to connect to the nginx pod. + assert_eq!( + blessed_uninjected_status, 0, + "blessed uninjected curl pod must succeed" + ); + + let (cursed_injected, cursed_uninjected) = tokio::join!( + curl.run( + "curl-cursed-injected", + "http://nginx", + LinkerdInject::Enabled, + ), + curl.run( + "curl-cursed-uninjected", + "http://nginx", + LinkerdInject::Disabled, + ), + ); + let (cursed_injected_status, cursed_uninjected_status) = + tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code()); + assert_eq!( + cursed_injected_status, 0, + "cursed injected curl pod must succeed" + ); + assert_ne!( + cursed_uninjected_status, 0, + "cursed uninjected curl pod must fail" + ); + }) + .await; +} + +// === helpers === + +fn authz_policy( + ns: &str, + name: &str, + target: &k8s::policy::Server, + authns: impl IntoIterator, +) -> k8s::policy::AuthorizationPolicy { + k8s::policy::AuthorizationPolicy { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some(name.to_string()), + ..Default::default() + }, + spec: k8s::policy::AuthorizationPolicySpec { + target_ref: LocalTargetRef::from_resource(target), + required_authentication_refs: authns.into_iter().collect(), + }, + } +} + +fn all_authenticated(ns: &str) -> k8s::policy::MeshTLSAuthentication { + k8s::policy::MeshTLSAuthentication { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some("all-authenticated".to_string()), + ..Default::default() + }, + spec: k8s::policy::MeshTLSAuthenticationSpec { + identity_refs: None, + identities: Some(vec!["*".to_string()]), + }, + } +} + +fn allow_ips( + ns: &str, + ips: impl IntoIterator, +) -> k8s::policy::NetworkAuthentication { + k8s::policy::NetworkAuthentication { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some("allow-pod".to_string()), + ..Default::default() + }, + spec: k8s::policy::NetworkAuthenticationSpec { + networks: ips + .into_iter() + .map(|ip| k8s::policy::Network { + cidr: ip.into(), + except: None, + }) + .collect(), + }, + } +}