| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #!/bin/bash | ||
|
|
||
| # Copyright 2019 Istio Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| set -e | ||
|
|
||
| fail() { | ||
| echo "$@" 1>&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| API_TMP="$(mktemp -d -u)" | ||
|
|
||
| trap 'rm -rf "${API_TMP}"' EXIT | ||
|
|
||
| SCRIPTPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||
| ROOTDIR=$(dirname "${SCRIPTPATH}") | ||
| cd "${ROOTDIR}" | ||
|
|
||
| # using the pseudo version we have in go.mod file. e.g. v.0.0.0-<timestamp>-<SHA> | ||
| SHA=$(grep "istio.io/api" go.mod | sed 's/[[:blank:]]istio\.io\/api v0\.0\.0-[[:digit:]]*-//g') | ||
|
|
||
| if [ -z "${SHA}" ]; then | ||
| fail "Unable to retrieve the commit SHA of istio/api from go.mod file. Not updating the CRD file. Please make sure istio/api exists in the Go module."; | ||
| fi | ||
|
|
||
| mkdir -p "${API_TMP}" | ||
| cd "${API_TMP}" | ||
| git init -q && git fetch "https://github.com/istio/api" -q && git merge "${SHA}" -q | ||
| if [ ! -f "${API_TMP}/kubernetes/customresourcedefinitions.gen.yaml" ]; then | ||
| echo "Generated Custom Resource Definitions file does not exist in the commit SHA. Not updating the CRD file." | ||
| exit | ||
| fi | ||
| rm -f "${ROOTDIR}/install/kubernetes/helm/istio-init/files/crd-all.gen.yaml" | ||
| cp "${API_TMP}/kubernetes/customresourcedefinitions.gen.yaml" "${ROOTDIR}/install/kubernetes/helm/istio-init/files/crd-all.gen.yaml" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #!/bin/bash | ||
|
|
||
| # Copyright 2019 Istio Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| set -e | ||
|
|
||
| UPDATE_BRANCH=${UPDATE_BRANCH:-"master"} | ||
|
|
||
| SCRIPTPATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | ||
| ROOTDIR=$(dirname "${SCRIPTPATH}") | ||
| cd "${ROOTDIR}" | ||
|
|
||
| # Get the sha of top commit | ||
| # $1 = repo | ||
| function getSha() { | ||
| local dir result | ||
| dir=$(mktemp -d) | ||
| git clone --depth=1 "https://github.com/istio/${1}.git" -b "${UPDATE_BRANCH}" "${dir}" | ||
|
|
||
| result="$(cd "${dir}" && git rev-parse HEAD)" | ||
| rm -rf "${dir}" | ||
|
|
||
| echo "${result}" | ||
| } | ||
|
|
||
| make update-common | ||
|
|
||
| export GO111MODULE=on | ||
| go get -u "istio.io/operator@${UPDATE_BRANCH}" | ||
| go get -u "istio.io/api@${UPDATE_BRANCH}" | ||
| go get -u "istio.io/gogo-genproto@${UPDATE_BRANCH}" | ||
| go get -u "istio.io/pkg@${UPDATE_BRANCH}" | ||
| go mod tidy | ||
|
|
||
| sed -i "s/^BUILDER_SHA=.*\$/BUILDER_SHA=$(getSha release-builder)/" prow/release-commit.sh | ||
| sed -i '/PROXY_REPO_SHA/,/lastStableSHA/ { s/"lastStableSHA":.*/"lastStableSHA": "'"$(getSha proxy)"'"/ }' istio.deps | ||
| sed -i '/CNI_REPO_SHA/,/lastStableSHA/ { s/"lastStableSHA":.*/"lastStableSHA": "'"$(getSha cni)"'"/ }' istio.deps |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| ca3ba53a54beac2f6830831b9477c199671bc1b6 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # BASE_DISTRIBUTION is used to switch between the old base distribution and distroless base images | ||
| ARG BASE_DISTRIBUTION=default | ||
|
|
||
| # Version is the base image version from the TLD Makefile | ||
| ARG BASE_VERSION=latest | ||
|
|
||
| # The following section is used as base image if BASE_DISTRIBUTION=default | ||
| FROM docker.io/istio/base:${BASE_VERSION} as default | ||
|
|
||
| USER 1337:1337 | ||
|
|
||
| # The following section is used as base image if BASE_DISTRIBUTION=distroless | ||
| # hadolint ignore=DL3007 | ||
| FROM gcr.io/distroless/static:nonroot as distroless | ||
|
|
||
| # This will build the final image based on either default or distroless from above | ||
| # hadolint ignore=DL3006 | ||
| FROM ${BASE_DISTRIBUTION} | ||
|
|
||
| COPY istiod /usr/local/bin/ | ||
| ENTRYPOINT ["/usr/local/bin/istiod"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,369 @@ | ||
| // Copyright 2019 Istio Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package auth | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "istio.io/api/authentication/v1alpha1" | ||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/util" | ||
|
|
||
| v1 "k8s.io/api/core/v1" | ||
| k8s_labels "k8s.io/apimachinery/pkg/labels" | ||
|
|
||
| "istio.io/istio/galley/pkg/config/analysis/msg" | ||
|
|
||
| "istio.io/api/networking/v1alpha3" | ||
| "istio.io/istio/galley/pkg/config/analysis" | ||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/auth/mtls" | ||
| "istio.io/istio/galley/pkg/config/meta/metadata" | ||
| "istio.io/istio/galley/pkg/config/meta/schema/collection" | ||
| "istio.io/istio/galley/pkg/config/resource" | ||
| ) | ||
|
|
||
| const missingResourceName = "(none)" | ||
|
|
||
| // MTLSAnalyzer checks for misconfigurations of MTLS policy when autoMtls is | ||
| // disabled. More specifically, it detects situations where a DestinationRule's | ||
| // MTLS usage is in conflict with mTLS specified by a policy. | ||
| // | ||
| // The most common situations that this detects are: 1. A MeshPolicy exists that | ||
| // requires mTLS, but no global destination rule | ||
| // says to use mTLS. | ||
| // 2. mTLS is used throughout the mesh, but a DestinationRule is added that | ||
| // doesn't specify mTLS (usually because it was forgotten). | ||
| // | ||
| // The analyzer tries to act more generally by imagining service-to-service | ||
| // traffic and detecting whether or not there's a conflict with regards to mTLS | ||
| // policy. This means it will also detect explicit misconfigurations as well. | ||
| // | ||
| // Note this is very similar to `istioctl authn tls-check`; however this | ||
| // inspection is all done via analyzing configuration rather than requiring a | ||
| // connection to pilot. | ||
| type MTLSAnalyzer struct{} | ||
|
|
||
| // Compile-time check that this Analyzer correctly implements the interface | ||
| var _ analysis.Analyzer = &MTLSAnalyzer{} | ||
|
|
||
| // Metadata implements Analyzer | ||
| func (s *MTLSAnalyzer) Metadata() analysis.Metadata { | ||
| return analysis.Metadata{ | ||
| Name: "auth.MTLSAnalyzer", | ||
| Description: "Checks for misconfigurations of MTLS policy when autoMtls is disabled", | ||
| // Each analyzer should register the collections that it needs to use as input. | ||
| Inputs: collection.Names{ | ||
| metadata.K8SCoreV1Pods, | ||
| metadata.K8SCoreV1Namespaces, | ||
| metadata.K8SCoreV1Services, | ||
| metadata.IstioAuthenticationV1Alpha1Meshpolicies, | ||
| metadata.IstioAuthenticationV1Alpha1Policies, | ||
| metadata.IstioMeshV1Alpha1MeshConfig, | ||
| metadata.IstioNetworkingV1Alpha3Destinationrules, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // Analyze implements Analyzer | ||
| func (s *MTLSAnalyzer) Analyze(c analysis.Context) { | ||
| // TODO Reuse pilot logic as a library rather than reproducing its logic | ||
| // here. | ||
|
|
||
| mc := util.MeshConfig(c) | ||
|
|
||
| // If autoMTLS is turned on, bail out early as the logic used below does not | ||
| // reason about its usage. | ||
| if mc.GetEnableAutoMtls().GetValue() { | ||
| return | ||
| } | ||
|
|
||
| // The mesh config object includes a default value for this already, so it should be set | ||
| rootNamespace := mc.GetRootNamespace() | ||
|
|
||
| // Loop over all services, building up a list of selectors for each. This is | ||
| // used to determine which pods are in which services, and determine whether | ||
| // or not the sidecar is fully enmeshed. If a service doesn't have a | ||
| // sidecar, then we always treat it as having an explicit "plaintext" policy | ||
| // regardless of the service/namespace/mesh policy. | ||
|
|
||
| var targetServices []mtls.TargetService | ||
| fqdnsWithoutSidecars := make(map[string]struct{}) | ||
| // Keep track of each fqdn -> port name -> port number. This is because | ||
| // the Policy object lets you target a port name, but DR requires port | ||
| // number. Tracking this means we can normalize to port number later. | ||
| fqdnToNameToPort := make(map[string]map[string]uint32) | ||
|
|
||
| c.ForEach(metadata.K8SCoreV1Services, func(r *resource.Entry) bool { | ||
| svcNs, svcName := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
|
|
||
| // Skip system namespaces entirely | ||
| if util.IsSystemNamespace(svcNs) { | ||
| return true | ||
| } | ||
|
|
||
| // Skip the istio control plane, which doesn't obey Policy/MeshPolicy MTLS | ||
| // rules in general and instead is controlled by the mesh option | ||
| // 'controlPlaneSecurityEnabled'. | ||
| if _, ok := r.Metadata.Labels["istio"]; ok { | ||
| return true | ||
| } | ||
|
|
||
| svc := r.Item.(*v1.ServiceSpec) | ||
|
|
||
| svcSelector := k8s_labels.SelectorFromSet(svc.Selector) | ||
| fqdn := util.ConvertHostToFQDN(svcNs, svcName) | ||
| for _, port := range svc.Ports { | ||
| // Ignore non-TCP protocols (UDP and others). Can be revisited once | ||
| // https://github.com/istio/istio/issues/1430 is closed. | ||
| if port.Protocol != "TCP" && port.Protocol != "" { | ||
| continue | ||
| } | ||
| portNumber := uint32(port.Port) | ||
| // portName is optional, but we note it so we can translate later. | ||
| if port.Name != "" { | ||
| // allocate a new map if necessary | ||
| if _, ok := fqdnToNameToPort[fqdn]; !ok { | ||
| fqdnToNameToPort[fqdn] = make(map[string]uint32) | ||
| } | ||
| fqdnToNameToPort[fqdn][port.Name] = portNumber | ||
| } | ||
|
|
||
| targetServices = append(targetServices, mtls.NewTargetServiceWithPortNumber(fqdn, portNumber)) | ||
| } | ||
|
|
||
| // Now we loop over all pods looking for sidecars that match our | ||
| // service. If we find a single pod without a sidecar, we label the | ||
| // service as not having a sidecar (which means we bypass policy | ||
| // checking). If we find no pods at all that match, also assume there's | ||
| // no sidecar. | ||
| var foundMatchingPods bool | ||
| c.ForEach(metadata.K8SCoreV1Pods, func(pr *resource.Entry) bool { | ||
| // If it's not in our namespace, we're not interested | ||
| podNs, _ := pr.Metadata.Name.InterpretAsNamespaceAndName() | ||
| if podNs != svcNs { | ||
| return true | ||
| } | ||
| pod := pr.Item.(*v1.Pod) | ||
| podLabels := k8s_labels.Set(pod.ObjectMeta.Labels) | ||
|
|
||
| if svcSelector.Empty() || !svcSelector.Matches(podLabels) { | ||
| return true | ||
| } | ||
|
|
||
| // This pod is selected for this service - ensure there's a sidecar. | ||
| foundMatchingPods = true | ||
| sidecarFound := false | ||
| for _, container := range pod.Spec.Containers { | ||
| if container.Name == "istio-proxy" { | ||
| sidecarFound = true | ||
| } | ||
| } | ||
|
|
||
| if !sidecarFound { | ||
| fqdnsWithoutSidecars[fqdn] = struct{}{} | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| if !foundMatchingPods { | ||
| fqdnsWithoutSidecars[fqdn] = struct{}{} | ||
| } | ||
|
|
||
| return true | ||
| }) | ||
|
|
||
| // While we visit every item, collect the set of namespaces that exist. Note | ||
| // that we will collect the namespace name for all resource types - this | ||
| // ensures our analyzer still behaves correctly even if namespaces are | ||
| // implicitly defined. | ||
| namespaces := make(map[string]struct{}) | ||
|
|
||
| c.ForEach(metadata.K8SCoreV1Namespaces, func(r *resource.Entry) bool { | ||
| _, name := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
| namespaces[name] = struct{}{} | ||
| return true | ||
| }) | ||
|
|
||
| pc := mtls.NewPolicyChecker(fqdnToNameToPort) | ||
| meshPolicyResource := c.Find(metadata.IstioAuthenticationV1Alpha1Meshpolicies, resource.NewName("", "default")) | ||
| if meshPolicyResource != nil { | ||
| err := pc.AddMeshPolicy(meshPolicyResource, meshPolicyResource.Item.(*v1alpha1.Policy)) | ||
| if err != nil { | ||
| c.Report(metadata.IstioAuthenticationV1Alpha1Meshpolicies, msg.NewInternalError(meshPolicyResource, err.Error())) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| c.ForEach(metadata.IstioAuthenticationV1Alpha1Policies, func(r *resource.Entry) bool { | ||
| ns, _ := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
| namespaces[ns] = struct{}{} | ||
|
|
||
| err := pc.AddPolicy(r, r.Item.(*v1alpha1.Policy)) | ||
| if err != nil { | ||
| // AddPolicy can return a NamedPortInPolicyNotFoundError - if it | ||
| // does we can print a useful message. | ||
| // TODO this should be in its own analyzer, and ignored here. | ||
| if missingPortNameErr, ok := err.(mtls.NamedPortInPolicyNotFoundError); ok { | ||
| c.Report(metadata.IstioAuthenticationV1Alpha1Meshpolicies, | ||
| msg.NewPolicySpecifiesPortNameThatDoesntExist(r, missingPortNameErr.PortName, missingPortNameErr.FQDN)) | ||
| return true | ||
| } | ||
| c.Report(metadata.IstioAuthenticationV1Alpha1Meshpolicies, msg.NewInternalError(r, err.Error())) | ||
| return false | ||
| } | ||
| return true | ||
| }) | ||
|
|
||
| drc := mtls.NewDestinationRuleChecker(rootNamespace) | ||
| c.ForEach(metadata.IstioNetworkingV1Alpha3Destinationrules, func(r *resource.Entry) bool { | ||
| ns, _ := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
| namespaces[ns] = struct{}{} | ||
|
|
||
| drc.AddDestinationRule(r, r.Item.(*v1alpha3.DestinationRule)) | ||
| return true | ||
| }) | ||
|
|
||
| // Here we explicitly handle the common case where a user specifies a | ||
| // MeshPolicy with no global DestinationRule. We also track if we report a | ||
| // problem with the global configuration. This is used later to suppress | ||
| // reporting a message for every service/namespace combination due to the | ||
| // same misconfiguration. | ||
| anyK8sServiceHost := fmt.Sprintf("%s.%s", util.Wildcard, util.DefaultKubernetesDomain) | ||
| globalMTLSMisconfigured := false | ||
| mpr := pc.MeshPolicy() | ||
| globalMtls, globalDR := drc.DoesNamespaceUseMTLSToService(rootNamespace, rootNamespace, mtls.NewTargetService(anyK8sServiceHost)) | ||
| if mpr.MTLSMode == mtls.ModeStrict && !globalMtls { | ||
| // We may or may not have a matching DR. If we don't, use the special | ||
| // missing resource string | ||
| globalDRName := missingResourceName | ||
| if globalDR != nil { | ||
| globalDRName = globalDR.Metadata.Name.String() | ||
| } | ||
| c.Report( | ||
| metadata.IstioAuthenticationV1Alpha1Meshpolicies, | ||
| msg.NewMTLSPolicyConflict( | ||
| mpr.Resource, | ||
| anyK8sServiceHost, | ||
| globalDRName, | ||
| globalMtls, | ||
| mpr.Resource.Metadata.Name.String(), | ||
| mpr.MTLSMode.String())) | ||
| globalMTLSMisconfigured = true | ||
| } | ||
|
|
||
| // Also handle the less-common case where a global DR exists that specifies | ||
| // mtls, but MTLS is off | ||
| if mpr.MTLSMode == mtls.ModePlaintext && globalMtls { | ||
| // We may or may not have a matching policy. If we don't, use the | ||
| // special missing resource string | ||
| globalPolicyName := missingResourceName | ||
| if mpr.Resource != nil { | ||
| globalPolicyName = mpr.Resource.Metadata.Name.String() | ||
| } | ||
| c.Report( | ||
| metadata.IstioNetworkingV1Alpha3Destinationrules, | ||
| msg.NewMTLSPolicyConflict( | ||
| globalDR, | ||
| anyK8sServiceHost, | ||
| globalDR.Metadata.Name.String(), | ||
| globalMtls, | ||
| globalPolicyName, | ||
| mpr.MTLSMode.String())) | ||
| globalMTLSMisconfigured = true | ||
| } | ||
|
|
||
| // Iterate over all fqdns and namespaces, and check that the mtls mode | ||
| // specified by the destination rule and the policy are not in conflict. | ||
| for _, ts := range targetServices { | ||
| var tsPolicy mtls.ModeAndResource | ||
| // If we don't have a sidecar, don't check policy and treat as plaintext | ||
| if _, ok := fqdnsWithoutSidecars[ts.FQDN()]; ok { | ||
| tsPolicy = mtls.ModeAndResource{ | ||
| MTLSMode: mtls.ModePlaintext, | ||
| Resource: nil, | ||
| } | ||
| } else { | ||
| var err error | ||
| tsPolicy, err = pc.IsServiceMTLSEnforced(ts) | ||
| if err != nil { | ||
| c.Report(metadata.IstioAuthenticationV1Alpha1Policies, msg.NewInternalError(nil, err.Error())) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| // Extract out the namespace for the target service | ||
| tsNamespace, _ := util.GetNamespaceAndNameFromFQDN(ts.FQDN()) | ||
|
|
||
| for ns := range namespaces { | ||
| mtlsUsed, matchingDR := drc.DoesNamespaceUseMTLSToService(ns, tsNamespace, ts) | ||
| if (tsPolicy.MTLSMode == mtls.ModeStrict && !mtlsUsed) || | ||
| (tsPolicy.MTLSMode == mtls.ModePlaintext && mtlsUsed) { | ||
|
|
||
| // If global mTLS is misconfigured, and one of the resources we | ||
| // are about to complain about is missing, it's almost certainly | ||
| // due to the same underlying problem (a missing global | ||
| // DR/MeshPolicy). In that case, don't emit since it's redundant. | ||
| if globalMTLSMisconfigured && (tsPolicy.Resource == nil || matchingDR == nil) { | ||
| continue | ||
| } | ||
|
|
||
| // Check to see if our mismatch is due to a missing sidecar. If | ||
| // so, use a different analyzer message. | ||
| if _, ok := fqdnsWithoutSidecars[ts.FQDN()]; ok { | ||
| c.Report(metadata.IstioNetworkingV1Alpha3Destinationrules, | ||
| msg.NewDestinationRuleUsesMTLSForWorkloadWithoutSidecar( | ||
| matchingDR, | ||
| matchingDR.Metadata.Name.String(), | ||
| ts.String())) | ||
| continue | ||
| } | ||
|
|
||
| if tsPolicy.Resource != nil { | ||
| // We may or may not have a matching DR. If we don't, use | ||
| // the special missing resource string | ||
| matchingDRName := missingResourceName | ||
| if matchingDR != nil { | ||
| matchingDRName = matchingDR.Metadata.Name.String() | ||
| } | ||
| c.Report( | ||
| metadata.IstioAuthenticationV1Alpha1Policies, | ||
| msg.NewMTLSPolicyConflict( | ||
| tsPolicy.Resource, | ||
| ts.String(), | ||
| matchingDRName, | ||
| mtlsUsed, | ||
| tsPolicy.Resource.Metadata.Name.String(), | ||
| tsPolicy.MTLSMode.String())) | ||
| } | ||
| if matchingDR != nil { | ||
| // We may or may not have a matching policy. If we don't, use | ||
| // the special missing resource string | ||
| policyName := missingResourceName | ||
| if tsPolicy.Resource != nil { | ||
| policyName = tsPolicy.Resource.Metadata.Name.String() | ||
| } | ||
| c.Report( | ||
| metadata.IstioNetworkingV1Alpha3Destinationrules, | ||
| msg.NewMTLSPolicyConflict( | ||
| matchingDR, | ||
| ts.String(), | ||
| matchingDR.Metadata.Name.String(), | ||
| mtlsUsed, | ||
| policyName, | ||
| tsPolicy.MTLSMode.String())) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| // Copyright 2019 Istio Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package mtls | ||
|
|
||
| import ( | ||
| "sort" | ||
|
|
||
| "istio.io/istio/galley/pkg/config/resource" | ||
|
|
||
| "istio.io/api/networking/v1alpha3" | ||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/util" | ||
| "istio.io/istio/pkg/config/host" | ||
| ) | ||
|
|
||
| // DestinationRuleChecker computes whether or not MTLS is used according to | ||
| // DestinationRules added to the instance. It handles the complicated logic of | ||
| // looking up which DestinationRule takes effect for a given source namespace, | ||
| // destination namespace and host name. | ||
| // | ||
| // This logic matches the logic Pilot uses: | ||
| // https://github.com/istio/istio/blob/4a442e9f4cbdedb0accdb33bd8b96a3e59691b0b/pilot/pkg/model/push_context.go#L605 | ||
| // and what's documented on the istio.io site: | ||
| // https://istio.io/docs/ops/traffic-management/deploy-guidelines/#cross-namespace-configuration-sharing | ||
| type DestinationRuleChecker struct { | ||
| namespaceToDestinations map[string]destinations | ||
| rootNamespace string | ||
| } | ||
|
|
||
| // destination represents a destination specified in a DestinationRule. | ||
| type destination struct { | ||
| targetService TargetService | ||
| usesMTLS bool | ||
| isPrivate bool | ||
| resource *resource.Entry | ||
| } | ||
|
|
||
| // destinations is a list of destinations that supports being sorted by | ||
| // hostname. This means that, once sorted, you can iterate over the list going | ||
| // from most-specific rules to least-specific. This matches how the API behaves. | ||
| type destinations []destination | ||
|
|
||
| // destinations implements sort.Interface | ||
| var _ sort.Interface = destinations{} | ||
|
|
||
| func (d destinations) Len() int { | ||
| return len(d) | ||
| } | ||
|
|
||
| func (d destinations) Less(i, j int) bool { | ||
| // First, check to see if we have a tie on FQDN. If we do, we want to break | ||
| // ties so that target services with a port specified come before those that | ||
| // don't. | ||
| ts1 := d[i].targetService | ||
| ts2 := d[j].targetService | ||
| if ts1.FQDN() == ts2.FQDN() { | ||
| return ts1.PortNumber() != 0 | ||
| } | ||
|
|
||
| // Defer to the sort order for target service hostname | ||
| hosts := []string{d[i].targetService.FQDN(), d[j].targetService.FQDN()} | ||
| return host.NewNames(hosts).Less(0, 1) | ||
| } | ||
|
|
||
| func (d destinations) Swap(i, j int) { | ||
| d[i], d[j] = d[j], d[i] | ||
| } | ||
|
|
||
| // NewDestinationRuleChecker creates a new instance with the given config root | ||
| // namespace. | ||
| func NewDestinationRuleChecker(rootNamespace string) *DestinationRuleChecker { | ||
| return &DestinationRuleChecker{ | ||
| namespaceToDestinations: make(map[string]destinations), | ||
| rootNamespace: rootNamespace, | ||
| } | ||
| } | ||
|
|
||
| // TargetServices returns the list of TargetServices known to the checker. These | ||
| // services are generated from DestinationRules previously added to the checker. | ||
| func (dc *DestinationRuleChecker) TargetServices() []TargetService { | ||
| var targetServices []TargetService | ||
| for _, destinations := range dc.namespaceToDestinations { | ||
| for _, destination := range destinations { | ||
| targetServices = append(targetServices, destination.targetService) | ||
| } | ||
| } | ||
|
|
||
| return targetServices | ||
| } | ||
|
|
||
| // AddDestinationRule adds a DestinationRule to the checker. | ||
| func (dc *DestinationRuleChecker) AddDestinationRule(resource *resource.Entry, rule *v1alpha3.DestinationRule) { | ||
| // By default Destination rules are exported publicly. | ||
| isPrivate := false | ||
| for _, export := range rule.ExportTo { | ||
| if export == "." { | ||
| isPrivate = true | ||
| } | ||
| } | ||
|
|
||
| namespace, _ := resource.Metadata.Name.InterpretAsNamespaceAndName() | ||
| fqdn := util.ConvertHostToFQDN(namespace, rule.GetHost()) | ||
| // By default, we are not using MTLS | ||
| usesMTLS := false | ||
| if rule.TrafficPolicy != nil && rule.TrafficPolicy.Tls != nil && rule.TrafficPolicy.Tls.Mode == v1alpha3.TLSSettings_ISTIO_MUTUAL { | ||
| usesMTLS = true | ||
| } | ||
|
|
||
| dc.namespaceToDestinations[namespace] = append(dc.namespaceToDestinations[namespace], destination{ | ||
| targetService: NewTargetService(fqdn), | ||
| usesMTLS: usesMTLS, | ||
| isPrivate: isPrivate, | ||
| resource: resource, | ||
| }) | ||
|
|
||
| if rule.TrafficPolicy == nil { | ||
| // No overrides to check | ||
| return | ||
| } | ||
| // TODO Support checking subsets. | ||
| // Now check if we have any overrides | ||
| for _, pls := range rule.TrafficPolicy.PortLevelSettings { | ||
| portUsesMTLS := false | ||
| if pls.Tls != nil && pls.Tls.Mode == v1alpha3.TLSSettings_ISTIO_MUTUAL { | ||
| portUsesMTLS = true | ||
| } | ||
|
|
||
| dc.namespaceToDestinations[namespace] = append(dc.namespaceToDestinations[namespace], destination{ | ||
| targetService: NewTargetServiceWithPortNumber(fqdn, pls.Port.GetNumber()), | ||
| usesMTLS: portUsesMTLS, | ||
| isPrivate: isPrivate, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // DoesNamespaceUseMTLSToService returns true if, according to DestinationRules | ||
| // added to the checker, mTLS will be used when communicating to the specified | ||
| // TargetService from the source namespace to the destination namespace. | ||
| // | ||
| // If the TargetService's FQDN has a wildcard, then the set of DestinationRules | ||
| // considered for routing are only rules that match a superset of the hosts | ||
| // specified by the TargetService FQDN. This means you can check, for example, | ||
| // the hostname '*.svc.cluster.local' to see if strict MTLS is enforced globally. | ||
| func (dc *DestinationRuleChecker) DoesNamespaceUseMTLSToService(srcNamespace, dstNamespace string, ts TargetService) (bool, *resource.Entry) { | ||
| var matchingDestination *destination | ||
| // First, check for a destination rule for src namespace only if the | ||
| // namespace isn't the root namespace. Pilot has this behavior to ensure that the | ||
| // rules in the root namespace don't override other rules. | ||
| if srcNamespace != dc.rootNamespace { | ||
| matchingDestination = dc.findMatchingRuleInNamespace(srcNamespace, ts, true) | ||
| if matchingDestination != nil { | ||
| return matchingDestination.usesMTLS, matchingDestination.resource | ||
| } | ||
| } | ||
|
|
||
| // Now check destination namespace | ||
| matchingDestination = dc.findMatchingRuleInNamespace(dstNamespace, ts, false) | ||
| if matchingDestination != nil { | ||
| return matchingDestination.usesMTLS, matchingDestination.resource | ||
| } | ||
|
|
||
| // Finally, try the root namespace | ||
| matchingDestination = dc.findMatchingRuleInNamespace(dc.rootNamespace, ts, false) | ||
| if matchingDestination != nil { | ||
| return matchingDestination.usesMTLS, matchingDestination.resource | ||
| } | ||
|
|
||
| // no matches found - just return false | ||
| return false, nil | ||
| } | ||
|
|
||
| // findMatchingRuleInNamespace looks up a DestinationRule in a namespace for a | ||
| // specified target service, optionally including privately-exported rules. Note | ||
| // that if target service has a wildcard in it, then matched rules must be a | ||
| // strict superset of the target service hostnames. | ||
| func (dc *DestinationRuleChecker) findMatchingRuleInNamespace(namespace string, ts TargetService, includePrivate bool) *destination { | ||
| // TODO Port name should be handled at some point. | ||
| // TODO We should really presort these ahead of time. | ||
| var ds destinations | ||
| for _, d := range dc.namespaceToDestinations[namespace] { | ||
| if !includePrivate && d.isPrivate { | ||
| continue | ||
| } | ||
| ds = append(ds, d) | ||
| } | ||
| // Sort our destinations, which allows us to find the first match by iterating. | ||
| sort.Sort(ds) | ||
|
|
||
| for _, d := range ds { | ||
| if !host.Name(ts.FQDN()).SubsetOf(host.Name(d.targetService.FQDN())) { | ||
| continue | ||
| } | ||
|
|
||
| // If a port is specified for ts, then skip if destination also has a | ||
| // port number and it doesn't match. | ||
| if ts.PortNumber() != 0 && d.targetService.PortNumber() != 0 && ts.PortNumber() != d.targetService.PortNumber() { | ||
| continue | ||
| } | ||
| // If target service doesn't specify a port, then skip if destination | ||
| // does specify a port. | ||
| if ts.PortNumber() == 0 && d.targetService.PortNumber() != 0 { | ||
| continue | ||
| } | ||
| return &d | ||
| } | ||
|
|
||
| // No match | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| // Copyright 2019 Istio Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package mtls | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "istio.io/api/authentication/v1alpha1" | ||
|
|
||
| "istio.io/istio/galley/pkg/config/resource" | ||
|
|
||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/util" | ||
| ) | ||
|
|
||
| // TargetService is a simple struct type for representing a service | ||
| // targeted by an Authentication policy. | ||
| type TargetService struct { | ||
| fqdn string | ||
| portNumber uint32 | ||
| } | ||
|
|
||
| // NewTargetServiceWithPortNumber creates a new TargetService using the specified | ||
| // fqdn and portNumber. | ||
| func NewTargetServiceWithPortNumber(fqdn string, portNumber uint32) TargetService { | ||
| return TargetService{fqdn: fqdn, portNumber: portNumber} | ||
| } | ||
|
|
||
| // NewTargetService creates a new TargetService using the specified fqdn. Because no | ||
| // port is specified, this implicitly represents the service bound to any port. | ||
| func NewTargetService(fqdn string) TargetService { | ||
| return TargetService{fqdn: fqdn} | ||
| } | ||
|
|
||
| // FQDN is the fully-qualified domain name for the service (e.g. | ||
| // foobar.my-namespace.svc.cluster.local). | ||
| func (w TargetService) FQDN() string { | ||
| return w.fqdn | ||
| } | ||
|
|
||
| // PortNumber is the port used by the service. | ||
| func (w TargetService) PortNumber() uint32 { | ||
| return w.portNumber | ||
| } | ||
|
|
||
| func (w TargetService) String() string { | ||
| if w.PortNumber() != 0 { | ||
| return fmt.Sprintf("%s:%d", w.fqdn, w.portNumber) | ||
| } | ||
| return w.fqdn | ||
| } | ||
|
|
||
| // PolicyChecker allows callers to add a set of v1alpha1.Policy objects in the | ||
| // mesh. Once these are loaded, you can query whether or not a specific | ||
| // TargetService will require MTLS when an incoming connection occurs using the | ||
| // IsServiceMTLSEnforced() call. | ||
| type PolicyChecker struct { | ||
| meshMTLSModeAndResource ModeAndResource | ||
|
|
||
| namespaceToMTLSMode map[string]ModeAndResource | ||
| serviceToMTLSMode map[TargetService]ModeAndResource | ||
| fqdnToPortNameToPortNumber map[string]map[string]uint32 | ||
| } | ||
|
|
||
| // Mode is a special type used to distinguish between MTLS being off | ||
| // (unsupported), permissive (supported but not required), and strict (required). | ||
| type Mode int | ||
|
|
||
| const ( | ||
| // ModePlaintext means MTLS is off (unsupported). | ||
| ModePlaintext Mode = iota | ||
| // ModePermissive means MTLS is permissive (supported but not required) | ||
| ModePermissive | ||
| // ModeStrict means MTLS is strict (required) | ||
| ModeStrict | ||
| ) | ||
|
|
||
| func (m Mode) String() string { | ||
| switch m { | ||
| case ModePlaintext: | ||
| return "Plaintext" | ||
| case ModePermissive: | ||
| return "Permissive" | ||
| case ModeStrict: | ||
| return "Strict" | ||
| default: | ||
| return "UNKNOWN" | ||
| } | ||
| } | ||
|
|
||
| // ModeAndResource is a simple tuple type of mode and the resource that | ||
| // specified the mode. | ||
| type ModeAndResource struct { | ||
| MTLSMode Mode | ||
| Resource *resource.Entry | ||
| } | ||
|
|
||
| // NewPolicyChecker creates a new PolicyChecker instance. | ||
| func NewPolicyChecker(fqdnToPortNameToPortNumber map[string]map[string]uint32) *PolicyChecker { | ||
| return &PolicyChecker{ | ||
| namespaceToMTLSMode: make(map[string]ModeAndResource), | ||
| serviceToMTLSMode: make(map[TargetService]ModeAndResource), | ||
| fqdnToPortNameToPortNumber: fqdnToPortNameToPortNumber, | ||
| } | ||
| } | ||
|
|
||
| // AddMeshPolicy adds a mesh-level policy to the checker. Note that there can | ||
| // only be at most one mesh level policy in effect. | ||
| func (pc *PolicyChecker) AddMeshPolicy(r *resource.Entry, p *v1alpha1.Policy) error { | ||
| mode, err := parsePolicyMTLSMode(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| pc.meshMTLSModeAndResource = ModeAndResource{Resource: r, MTLSMode: mode} | ||
| return nil | ||
| } | ||
|
|
||
| // MeshPolicy returns the current recognized MeshPolicy (as added by AddMeshPolicy). | ||
| func (pc *PolicyChecker) MeshPolicy() ModeAndResource { | ||
| return pc.meshMTLSModeAndResource | ||
| } | ||
|
|
||
| type NamedPortInPolicyNotFoundError struct { | ||
| PortName string | ||
| PolicyOrigin string | ||
| FQDN string | ||
| } | ||
|
|
||
| func (e NamedPortInPolicyNotFoundError) Error() string { | ||
| return fmt.Sprintf("named port '%s' not found for fqdn '%s', unable to analyze policy '%s'", e.PortName, e.FQDN, e.PolicyOrigin) | ||
| } | ||
|
|
||
| // AddPolicy adds a new policy object to the PolicyChecker to use when later | ||
| // determining if a service is MTLS-enforced. The namespace of the policy is | ||
| // also provided as some policies can target the local namespace. | ||
| // | ||
| // If the Policy uses a named port, and the port cannot be looked up in the map | ||
| // provided to NewPolicyChecker, then an error of type | ||
| // NamedPortInPolicyNotFoundError is returned. | ||
| func (pc *PolicyChecker) AddPolicy(r *resource.Entry, p *v1alpha1.Policy) error { | ||
| mode, err := parsePolicyMTLSMode(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| modeAndResource := ModeAndResource{Resource: r, MTLSMode: mode} | ||
| namespace, _ := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
| if len(p.Targets) == 0 { | ||
| // Rule targets the namespace. | ||
| pc.namespaceToMTLSMode[namespace] = modeAndResource | ||
| return nil | ||
| } | ||
| // Discover the targeted service and take note. Should normalize. | ||
| for _, target := range p.Targets { | ||
| fqdn := util.ConvertHostToFQDN(namespace, target.Name) | ||
|
|
||
| if len(target.Ports) == 0 { | ||
| // Policy targets all ports on service | ||
| pc.serviceToMTLSMode[NewTargetService(fqdn)] = modeAndResource | ||
| } | ||
|
|
||
| for _, port := range target.Ports { | ||
| if port.GetName() != "" { | ||
| // Look up the port number for the name. If we can't find it, we | ||
| // need to complain about a different error | ||
| // TODO handle missing reference error. | ||
| if _, ok := pc.fqdnToPortNameToPortNumber[fqdn]; !ok { | ||
| return NamedPortInPolicyNotFoundError{ | ||
| PortName: port.GetName(), | ||
| PolicyOrigin: r.Origin.FriendlyName(), | ||
| FQDN: fqdn, | ||
| } | ||
| } | ||
| portNumber := pc.fqdnToPortNameToPortNumber[fqdn][port.GetName()] | ||
| if portNumber == 0 { | ||
| return NamedPortInPolicyNotFoundError{ | ||
| PortName: port.GetName(), | ||
| PolicyOrigin: r.Origin.FriendlyName(), | ||
| FQDN: fqdn, | ||
| } | ||
| } | ||
|
|
||
| pc.serviceToMTLSMode[NewTargetServiceWithPortNumber(fqdn, portNumber)] = modeAndResource | ||
| } else if port.GetNumber() != 0 { | ||
| pc.serviceToMTLSMode[NewTargetServiceWithPortNumber(fqdn, port.GetNumber())] = modeAndResource | ||
| } else { | ||
| // Unhandled case! | ||
| return fmt.Errorf("policy has a port with no name/number for target %s", target.Name) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // IsServiceMTLSEnforced returns true if a service requires incoming | ||
| // connections to use MTLS, or false if MTLS is not a hard-requirement (e.g. | ||
| // mode is permissive, peerIsOptional is true, etc). Only call this after adding | ||
| // all policy resources in effect via AddPolicy or AddMeshPolicy. | ||
| func (pc *PolicyChecker) IsServiceMTLSEnforced(w TargetService) (ModeAndResource, error) { | ||
| // TODO support understanding port name -> port number mappings | ||
| var modeAndResource ModeAndResource | ||
| modeAndResource = pc.serviceToMTLSMode[w] | ||
| if modeAndResource.Resource != nil { | ||
| return modeAndResource, nil | ||
| } | ||
|
|
||
| // Try checking if its enforced on any ports | ||
| serviceNoPort := NewTargetService(w.FQDN()) | ||
| modeAndResource = pc.serviceToMTLSMode[serviceNoPort] | ||
| if modeAndResource.Resource != nil { | ||
| return modeAndResource, nil | ||
| } | ||
|
|
||
| // Check if enforced on namespace | ||
| namespace, _ := util.GetResourceNameFromHost("", w.FQDN()).InterpretAsNamespaceAndName() | ||
| if namespace == "" { | ||
| return ModeAndResource{}, fmt.Errorf("unable to extract namespace from fqdn: %s", w.FQDN()) | ||
| } | ||
| modeAndResource = pc.namespaceToMTLSMode[namespace] | ||
| if modeAndResource.Resource != nil { | ||
| return modeAndResource, nil | ||
| } | ||
|
|
||
| // Finally, defer to mesh level policy. No need to check for a nil resource | ||
| // since the default value is correct. | ||
| return pc.meshMTLSModeAndResource, nil | ||
| } | ||
|
|
||
| // TargetServices returns the list of services known to the policy checker (in | ||
| // no particular order). | ||
| func (pc *PolicyChecker) TargetServices() []TargetService { | ||
| tss := make([]TargetService, len(pc.serviceToMTLSMode)) | ||
| i := 0 | ||
| for ts := range pc.serviceToMTLSMode { | ||
| tss[i] = ts | ||
| i++ | ||
| } | ||
|
|
||
| return tss | ||
| } | ||
|
|
||
| // parsePolicyMTLSMode is a helper function to determine what mtls mode a Policy | ||
| // implies. | ||
| func parsePolicyMTLSMode(p *v1alpha1.Policy) (Mode, error) { | ||
| for _, peer := range p.Peers { | ||
| mtlsParams, ok := peer.Params.(*v1alpha1.PeerAuthenticationMethod_Mtls) | ||
| if !ok { | ||
| // Only looking for mtls methods | ||
| continue | ||
| } | ||
|
|
||
| var mode Mode | ||
| if mtlsParams.Mtls == nil { | ||
| mode = ModeStrict | ||
| } else { | ||
| switch mtlsParams.Mtls.GetMode() { | ||
| case v1alpha1.MutualTls_PERMISSIVE: | ||
| mode = ModePermissive | ||
| case v1alpha1.MutualTls_STRICT: | ||
| mode = ModeStrict | ||
| default: | ||
| // Shouldn't happen! | ||
| return mode, fmt.Errorf("unknown MTLS mode when analyzing policy: %s", mtlsParams.Mtls.GetMode().String()) | ||
| } | ||
| } | ||
| // Now check for modifiers that might downgrade strict to permissive. | ||
| if mode == ModeStrict && mtlsParams.Mtls != nil && mtlsParams.Mtls.AllowTls { | ||
| mode = ModePermissive | ||
| } | ||
| if mode == ModeStrict && p.PeerIsOptional { | ||
| mode = ModePermissive | ||
| } | ||
| return mode, nil | ||
| } | ||
|
|
||
| // No MTLS configuration found | ||
| return ModePlaintext, nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // Copyright 2019 Istio Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package auth | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "istio.io/api/rbac/v1alpha1" | ||
|
|
||
| "istio.io/istio/galley/pkg/config/analysis" | ||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/util" | ||
| "istio.io/istio/galley/pkg/config/analysis/msg" | ||
| "istio.io/istio/galley/pkg/config/meta/metadata" | ||
| "istio.io/istio/galley/pkg/config/meta/schema/collection" | ||
| "istio.io/istio/galley/pkg/config/resource" | ||
| ) | ||
|
|
||
| // ServiceRoleServicesAnalyzer checks the validity of services referred in a service role | ||
| type ServiceRoleServicesAnalyzer struct{} | ||
|
|
||
| var _ analysis.Analyzer = &ServiceRoleServicesAnalyzer{} | ||
|
|
||
| // Metadata implements Analyzer | ||
| func (s *ServiceRoleServicesAnalyzer) Metadata() analysis.Metadata { | ||
| return analysis.Metadata{ | ||
| Name: "auth.ServiceRoleServicesAnalyzer", | ||
| Description: "Checks the validity of services referred in a service role", | ||
| Inputs: collection.Names{ | ||
| metadata.IstioRbacV1Alpha1Serviceroles, | ||
| metadata.K8SCoreV1Services, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // Analyze implements Analyzer | ||
| func (s *ServiceRoleServicesAnalyzer) Analyze(ctx analysis.Context) { | ||
| nsm := s.buildNamespaceServiceMap(ctx) | ||
| ctx.ForEach(metadata.IstioRbacV1Alpha1Serviceroles, func(r *resource.Entry) bool { | ||
| s.analyzeServiceRoleServices(r, ctx, nsm) | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| // analyzeRoleBinding apply analysis for the service field of the given ServiceRole | ||
| func (s *ServiceRoleServicesAnalyzer) analyzeServiceRoleServices(r *resource.Entry, ctx analysis.Context, nsm map[string][]util.ScopedFqdn) { | ||
| sr := r.Item.(*v1alpha1.ServiceRole) | ||
| ns, _ := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
|
|
||
| for _, rs := range sr.Rules { | ||
| for _, svc := range rs.Services { | ||
| if svc != "*" && !s.existMatchingService(svc, nsm[ns]) { | ||
| // Report when the specific service doesn't exist | ||
| ctx.Report(metadata.IstioRbacV1Alpha1Serviceroles, | ||
| msg.NewReferencedResourceNotFound(r, "service", svc)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // buildNamespaceServiceMap returns a map where the index is a namespace and the boolean | ||
| func (s *ServiceRoleServicesAnalyzer) buildNamespaceServiceMap(ctx analysis.Context) map[string][]util.ScopedFqdn { | ||
| nsm := map[string][]util.ScopedFqdn{} | ||
|
|
||
| ctx.ForEach(metadata.K8SCoreV1Services, func(r *resource.Entry) bool { | ||
| rns, rs := r.Metadata.Name.InterpretAsNamespaceAndName() | ||
| nsm[rns] = append(nsm[rns], util.NewScopedFqdn(rns, rns, rs)) | ||
| return true | ||
| }) | ||
|
|
||
| return nsm | ||
| } | ||
|
|
||
| func (s *ServiceRoleServicesAnalyzer) existMatchingService(exp string, nsa []util.ScopedFqdn) bool { | ||
| for _, svc := range nsa { | ||
| if serviceMatch(exp, svc) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func serviceMatch(expr string, sfqdn util.ScopedFqdn) bool { | ||
| _, fqdn := sfqdn.GetScopeAndFqdn() | ||
| return expr == fqdn || strings.HasPrefix(fqdn, expr) || strings.HasSuffix(fqdn, expr) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| // Copyright 2019 Istio Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| package deployment | ||
|
|
||
| import ( | ||
| apps_v1 "k8s.io/api/apps/v1" | ||
| core_v1 "k8s.io/api/core/v1" | ||
| k8s_labels "k8s.io/apimachinery/pkg/labels" | ||
|
|
||
| "istio.io/api/annotation" | ||
| "istio.io/istio/galley/pkg/config/analysis" | ||
| "istio.io/istio/galley/pkg/config/analysis/analyzers/injection" | ||
| "istio.io/istio/galley/pkg/config/analysis/msg" | ||
| "istio.io/istio/galley/pkg/config/meta/metadata" | ||
| "istio.io/istio/galley/pkg/config/meta/schema/collection" | ||
| "istio.io/istio/galley/pkg/config/resource" | ||
| ) | ||
|
|
||
| type ServiceAssociationAnalyzer struct{} | ||
|
|
||
| var _ analysis.Analyzer = &ServiceAssociationAnalyzer{} | ||
|
|
||
| type PortMap map[int32]ProtocolMap | ||
| type ProtocolMap map[core_v1.Protocol]ServiceNames | ||
| type ServiceNames []string | ||
| type ServiceSpecWithName struct { | ||
| Name string | ||
| Spec *core_v1.ServiceSpec | ||
| } | ||
|
|
||
| func (s *ServiceAssociationAnalyzer) Metadata() analysis.Metadata { | ||
| return analysis.Metadata{ | ||
| Name: "deployment.MultiServiceAnalyzer", | ||
| Description: "Checks association between services and pods", | ||
| Inputs: collection.Names{ | ||
| metadata.K8SCoreV1Services, | ||
| metadata.K8SAppsV1Deployments, | ||
| metadata.K8SCoreV1Namespaces, | ||
| }, | ||
| } | ||
| } | ||
| func (s *ServiceAssociationAnalyzer) Analyze(c analysis.Context) { | ||
| c.ForEach(metadata.K8SAppsV1Deployments, func(r *resource.Entry) bool { | ||
| if inMesh(r, c) { | ||
| s.analyzeDeployment(r, c) | ||
| } | ||
| return true | ||
| }) | ||
| } | ||
|
|
||
| // analyzeDeployment analyzes the specific service mesh deployment | ||
| func (s *ServiceAssociationAnalyzer) analyzeDeployment(r *resource.Entry, c analysis.Context) { | ||
| d := r.Item.(*apps_v1.Deployment) | ||
|
|
||
| // Find matching services with resulting pod from deployment | ||
| matchingSvcs := s.findMatchingServices(d, c) | ||
|
|
||
| // If there isn't any matching service, generate message: At least one service is needed. | ||
| if len(matchingSvcs) == 0 { | ||
| c.Report(metadata.K8SAppsV1Deployments, msg.NewDeploymentRequiresServiceAssociated(r, d.Name)) | ||
| return | ||
| } | ||
|
|
||
| // Generate a port map from the matching services. | ||
| // It creates a structure that will allow us to detect | ||
| // if there are different protocols for the same port. | ||
| portMap := servicePortMap(matchingSvcs) | ||
|
|
||
| // Determining which ports use more than one protocol. | ||
| for port := range portMap { | ||
| // In case there are two protocols using same port number, generate a message | ||
| protMap := portMap[port] | ||
| if len(protMap) > 1 { | ||
| // Collect names from both protocols | ||
| svcNames := make(ServiceNames, 0) | ||
| for protocol := range protMap { | ||
| svcNames = append(svcNames, protMap[protocol]...) | ||
| } | ||
|
|
||
| // Reporting the message for the deployment, port and conflicting services. | ||
| c.Report(metadata.K8SAppsV1Deployments, msg.NewDeploymentAssociatedToMultipleServices(r, d.Name, port, svcNames)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // findMatchingServices returns an slice of Services that matches with deployment's pods. | ||
| func (s *ServiceAssociationAnalyzer) findMatchingServices(d *apps_v1.Deployment, c analysis.Context) []ServiceSpecWithName { | ||
| matchingSvcs := make([]ServiceSpecWithName, 0) | ||
|
|
||
| c.ForEach(metadata.K8SCoreV1Services, func(r *resource.Entry) bool { | ||
| s := r.Item.(*core_v1.ServiceSpec) | ||
|
|
||
| sSelector := k8s_labels.SelectorFromSet(s.Selector) | ||
| pLabels := k8s_labels.Set(d.Spec.Template.Labels) | ||
| if sSelector.Matches(pLabels) { | ||
| matchingSvcs = append(matchingSvcs, ServiceSpecWithName{r.Metadata.Name.String(), s}) | ||
| } | ||
|
|
||
| return true | ||
| }) | ||
|
|
||
| return matchingSvcs | ||
| } | ||
|
|
||
| // servicePortMap build a map of ports and protocols for each Service. e.g. m[80]["TCP"] -> svcA, svcB, svcC | ||
| func servicePortMap(svcs []ServiceSpecWithName) PortMap { | ||
| portMap := PortMap{} | ||
|
|
||
| for _, swn := range svcs { | ||
| svc := swn.Spec | ||
| for _, sPort := range svc.Ports { | ||
| // If it is the first occurrence of this port, create a ProtocolMap | ||
| if _, ok := portMap[sPort.Port]; !ok { | ||
| portMap[sPort.Port] = ProtocolMap{} | ||
| } | ||
|
|
||
| // Default protocol is TCP | ||
| protocol := sPort.Protocol | ||
| if protocol == "" { | ||
| protocol = core_v1.ProtocolTCP | ||
| } | ||
|
|
||
| // Appending the service information for the Port/Protocol combination | ||
| portMap[sPort.Port][protocol] = append(portMap[sPort.Port][protocol], swn.Name) | ||
| } | ||
| } | ||
|
|
||
| return portMap | ||
| } | ||
|
|
||
| // inMesh returns true if deployment is in the service mesh (has sidecar) | ||
| func inMesh(r *resource.Entry, c analysis.Context) bool { | ||
| d := r.Item.(*apps_v1.Deployment) | ||
|
|
||
| // If Pod has annotation, return the injection annotation value | ||
| if piv, pivok := getPodSidecarInjectionStatus(d); pivok { | ||
| return piv | ||
| } | ||
|
|
||
| // In case the annotation is not present but there is a auto-injection label on the namespace, | ||
| // return the auto-injection label status | ||
| if niv, nivok := getNamesSidecarInjectionStatus(d.Namespace, c); nivok { | ||
| return niv | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| // getPodSidecarInjectionStatus returns two booleans: enabled and ok. | ||
| // enabled is true when deployment d PodSpec has either the annotation 'sidecar.istio.io/inject: "true"' | ||
| // ok is true when the PodSpec doesn't have the 'sidecar.istio.io/inject' annotation present. | ||
| func getPodSidecarInjectionStatus(d *apps_v1.Deployment) (enabled bool, ok bool) { | ||
| v, ok := d.Spec.Template.Annotations[annotation.SidecarInject.Name] | ||
| return v == "true", ok | ||
| } | ||
|
|
||
| // autoInjectionEnabled returns two booleans: enabled and ok. | ||
| // enabled is true when namespace ns has 'istio-injection' label set to 'enabled' | ||
| // ok is true when the namespace doesn't have the label 'istio-injection' | ||
| func getNamesSidecarInjectionStatus(ns string, c analysis.Context) (enabled bool, ok bool) { | ||
| enabled, ok = false, false | ||
|
|
||
| namespace := c.Find(metadata.K8SCoreV1Namespaces, resource.NewName("", ns)) | ||
| if namespace != nil { | ||
| enabled, ok = namespace.Metadata.Labels[injection.InjectionLabelName] == injection.InjectionLabelEnableValue, true | ||
| } | ||
|
|
||
| return enabled, ok | ||
| } |